diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
index 7df277641bf32e412f9b8ce407ec0890f181a176..8875bbacee6c38822485cce8cfbc20cafbcb4a93 100644
--- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
@@ -32,6 +32,11 @@ class ProjectCiCdSettingsUpdate < BaseMutation
description: 'Indicates CI/CD job tokens generated in other projects ' \
'have restricted access to this project.'
+ argument :push_repository_for_job_token_allowed, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Indicates the ability to push to the original project ' \
+ 'repository using a job token'
+
field :ci_cd_settings,
Types::Ci::CiCdSettingType,
null: false,
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index f6f2fe0ef9d0d4c1e054792848e3d8ea85ca8074..3ce1e65d28f95450f2b653ad246996da3bde92c1 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -37,6 +37,13 @@ class CiCdSettingType < BaseObject
null: true,
description: 'Project the CI/CD settings belong to.',
authorize: :admin_project
+ field :push_repository_for_job_token_allowed,
+ GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates the ability to push to the original project ' \
+ 'repository using a job token',
+ method: :push_repository_for_job_token_allowed?,
+ authorize: :admin_project
end
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index bb6f917d42691671f890f79d72c851ec3785e0dd..03f740b1e44b1e08f350dc01bb87e1acd348730e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -251,7 +251,11 @@ class ProjectPolicy < BasePolicy
end
condition(:push_repository_for_job_token_allowed) do
- @user&.from_ci_job_token? && project.ci_push_repository_for_job_token_allowed? && @user.ci_job_token_scope.self_referential?(project)
+ if ::Feature.enabled?(:allow_push_repository_for_job_token, @subject)
+ @user&.from_ci_job_token? && project.ci_push_repository_for_job_token_allowed? && @user.ci_job_token_scope.self_referential?(project)
+ else
+ false
+ end
end
condition(:packages_disabled, scope: :subject) { !@subject.packages_enabled }
diff --git a/config/feature_flags/development/allow_push_repository_for_job_token.yml b/config/feature_flags/development/allow_push_repository_for_job_token.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5c0e1013e7b2c79bf69c36af77db70141cdff16b
--- /dev/null
+++ b/config/feature_flags/development/allow_push_repository_for_job_token.yml
@@ -0,0 +1,8 @@
+---
+name: allow_push_repository_for_job_token
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154111
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468320
+milestone: "17.2"
+type: development
+group: group::pipeline security
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a98d44929ad09b57b4af32c31b0ba77a6849e350..c05557590113ed32b7cddd662dba23c3b6013da4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7328,6 +7328,7 @@ Input type: `ProjectCiCdSettingsUpdateInput`
| `mergePipelinesEnabled` | [`Boolean`](#boolean) | Indicates if merged results pipelines are enabled for the project. |
| `mergeTrainsEnabled` | [`Boolean`](#boolean) | Indicates if merge trains are enabled for the project. |
| `mergeTrainsSkipTrainAllowed` | [`Boolean`](#boolean) | Indicates whether an option is allowed to merge without refreshing the merge train. Ignored unless the `merge_trains_skip_train` feature flag is also enabled. |
+| `pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. |
#### Fields
@@ -29014,6 +29015,7 @@ four standard [pagination arguments](#pagination-arguments):
| `mergeTrainsEnabled` | [`Boolean`](#boolean) | Whether merge trains are enabled. |
| `mergeTrainsSkipTrainAllowed` | [`Boolean!`](#boolean) | Whether merge immediately is allowed for merge trains. |
| `project` | [`Project`](#project) | Project the CI/CD settings belong to. |
+| `pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. |
### `ProjectDataTransfer`
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 1426544408f08e854254a643d4c151c587b5dae4..cc663d93d98f16887b3ea9b195838ea5b014d0fd 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -245,6 +245,8 @@ When the user is authenticated and `simple` is not set this returns something li
"ci_job_token_scope_enabled": false,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
+ "ci_pipeline_variables_minimum_override_role": "maintainer",
+ "ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"build_timeout": 3600,
"auto_cancel_pending_pipelines": "enabled",
@@ -421,6 +423,8 @@ GET /users/:user_id/projects
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
+ "ci_pipeline_variables_minimum_override_role": "maintainer",
+ "ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@@ -542,6 +546,8 @@ GET /users/:user_id/projects
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
+ "ci_pipeline_variables_minimum_override_role": "maintainer",
+ "ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@@ -1214,6 +1220,8 @@ GET /projects/:id
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
+ "ci_pipeline_variables_minimum_override_role": "maintainer",
+ "ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [
{
@@ -1756,6 +1764,8 @@ General project attributes:
| `ci_allow_fork_pipelines_to_run_in_parent_project` | boolean | No | Enable or disable [running pipelines in the parent project for merge requests from forks](../ci/pipelines/merge_request_pipelines.md#run-pipelines-in-the-parent-project). _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325189) in GitLab 15.3.)_ |
| `ci_separated_caches` | boolean | No | Set whether or not caches should be [separated](../ci/caching/index.md#cache-key-names) by branch protection status. |
| `ci_restrict_pipeline_cancellation_role` | string | No | Set the [role required to cancel a pipeline or job](../ci/pipelines/settings.md#restrict-roles-that-can-cancel-pipelines-or-jobs). One of `developer`, `maintainer`, or `no_one`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429921) in GitLab 16.8. Premium and Ultimate only. |
+| `ci_pipeline_variables_minimum_override_role` | string | No | When `restrict_user_defined_variables` is enabled, you can specify which role can override variables. One of `owner`, `maintainer`, `developer` or `no_one_allowed`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/440338) in GitLab 17.1. |
+| `ci_push_repository_for_job_token_allowed` | boolean | No | Enable or disable the ability to push to the project repository using job token. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. |
| `container_expiration_policy_attributes` | hash | No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
| `container_registry_enabled` | boolean | No | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. |
| `default_branch` | string | No | The [default branch](../user/project/repository/branches/default.md) name. |
@@ -2375,6 +2385,8 @@ Example response:
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
+ "ci_pipeline_variables_minimum_override_role": "maintainer",
+ "ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
@@ -2507,6 +2519,8 @@ Example response:
"ci_allow_fork_pipelines_to_run_in_parent_project": true,
"ci_separated_caches": true,
"ci_restrict_pipeline_cancellation_role": "developer",
+ "ci_pipeline_variables_minimum_override_role": "maintainer",
+ "ci_push_repository_for_job_token_allowed": false,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 20914b0d828ce06faa4e54aa9b61c6a85e8eda1f..4c85bb8754c8950c91dd8361a3b713a5519cee96 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -278,3 +278,21 @@ While troubleshooting CI/CD job token authentication issues, be aware that:
- To remove project access.
- The CI job token becomes invalid if the job is no longer running, has been erased,
or if the project is in the process of being deleted.
+
+### Push to a project repository using a job token
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389060) in GitLab 17.2. [with a flag](../../administration/feature_flags.md) named `allow_push_repository_for_job_token`. Disabled by default.
+
+WARNING:
+Pushing via job token is still in development and is not yet optimized for performance.
+If you enable this feature for testing, you must thoroughly test and implement validation measures
+to prevent infinite loops of "push" pipelines triggering more pipelines.
+
+By default, pushing to a project repository by authenticating with a job token is disabled.
+To enable this ability, you can:
+
+- Feature flag named `allow_push_repository_for_job_token` should be enabled.
+- Enable the [`pushRepositoryForJobTokenAllowed`](../../api/graphql/reference/index.md#mutationprojectcicdsettingsupdate) GraphQL endpoint.
+- Enable the [`ci_push_repository_for_job_token_allowed`](../../api/projects.md#edit-project) REST API endpoint.
+
+You are only permitted to push to the repository of the project where the job is running.
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index fcb7dee20b4bba658c579ee25243e2c1f51434cd..d54be30a120ccef020e5d23c779f1c40c1a9ddf8 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -134,6 +134,7 @@ class Project < BasicProjectDetails
expose :auto_devops_deploy_strategy, documentation: { type: 'string', example: 'continuous' } do |project, options|
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end
+ expose :ci_push_repository_for_job_token_allowed, documentation: { type: 'boolean' }
end
expose :ci_config_path, documentation: { type: 'string', example: '' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 4567335757123b7831ceb45668c8265edf1d1811..562af336d57b68924dc987eb440d8d3ad4871cab 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -117,6 +117,7 @@ module ProjectsHelpers
optional :ci_separated_caches, type: Boolean, desc: 'Enable or disable separated caches based on branch protection.'
optional :restrict_user_defined_variables, type: Boolean, desc: 'Restrict use of user-defined variables when triggering a pipeline'
optional :ci_pipeline_variables_minimum_override_role, values: %w[no_one_allowed developer maintainer owner], type: String, desc: 'Limit ability to override CI/CD variables when triggering a pipeline to only users with at least the set minimum role'
+ optional :ci_push_repository_for_job_token_allowed, type: Boolean, desc: "Allow pushing to this project's repository by authenticating with a CI/CD job token generated in this project."
end
params :optional_update_params_ee do
@@ -210,6 +211,7 @@ def self.update_params_at_least_one_of
:model_registry_access_level,
:warn_about_potentially_unwanted_characters,
:ci_pipeline_variables_minimum_override_role,
+ :ci_push_repository_for_job_token_allowed,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
diff --git a/spec/graphql/types/ci/ci_cd_setting_type_spec.rb b/spec/graphql/types/ci/ci_cd_setting_type_spec.rb
index 5fdfb405e239f876e11ae6f7c12c63b282dabd49..cac4a062a731f97c52308752de8d80a7be8adcb2 100644
--- a/spec/graphql/types/ci/ci_cd_setting_type_spec.rb
+++ b/spec/graphql/types/ci/ci_cd_setting_type_spec.rb
@@ -9,6 +9,7 @@
expected_fields = %w[
inbound_job_token_scope_enabled job_token_scope_enabled
keep_latest_artifact merge_pipelines_enabled project
+ push_repository_for_job_token_allowed
]
if Gitlab.ee?
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 93c26afadbecd615a01d2e71aacbf958d82be6ef..00ec396caf929d9c74454264ccfe926c6ea0c38a 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -27,6 +27,12 @@
end
end
+ describe '#push_repository_for_job_token_allowed' do
+ it 'is false by default' do
+ expect(described_class.new.push_repository_for_job_token_allowed).to be_falsey
+ end
+ end
+
describe '#separated_caches' do
it 'is true by default' do
expect(described_class.new.separated_caches).to be_truthy
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 428648e3bed8916f744c9886889bd8a25f5aa659..a78d0afba678404efc8ff5ca3393326743e10355 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -3785,19 +3785,24 @@ def permissions_abilities(role)
let(:policy) { :build_push_code }
- where(:user_role, :project_visibility, :push_repository_for_job_token_allowed, :self_referential_project, :allowed) do
- :maintainer | :public | true | true | true
- :owner | :public | true | true | true
- :maintainer | :private | true | true | true
- :developer | :public | true | true | true
- :reporter | :public | true | true | false
- :guest | :public | true | true | false
- :guest | :private | true | true | false
- :guest | :internal | true | true | false
- :anonymous | :public | true | true | false
- :maintainer | :public | false | true | false
- :maintainer | :public | true | false | false
- :maintainer | :public | false | false | false
+ where(:user_role, :project_visibility, :push_repository_for_job_token_allowed, :self_referential_project, :allowed, :ff_disabled) do
+ :maintainer | :public | true | true | true | false
+ :owner | :public | true | true | true | false
+ :maintainer | :private | true | true | true | false
+ :developer | :public | true | true | true | false
+ :reporter | :public | true | true | false | false
+ :guest | :public | true | true | false | false
+ :guest | :private | true | true | false | false
+ :guest | :internal | true | true | false | false
+ :anonymous | :public | true | true | false | false
+ :maintainer | :public | false | true | false | false
+ :maintainer | :public | true | false | false | false
+ :maintainer | :public | false | false | false | false
+ :maintainer | :public | true | true | false | true
+ :owner | :public | true | true | false | true
+ :maintainer | :private | true | true | false | true
+ :developer | :public | true | true | false | true
+ :reporter | :public | true | true | false | true
end
with_them do
@@ -3811,6 +3816,8 @@ def permissions_abilities(role)
let(:scope_project) { public_send(:private_project) }
before do
+ stub_feature_flags(allow_push_repository_for_job_token: false) if ff_disabled
+
project.add_guest(guest)
project.add_reporter(reporter)
project.add_developer(developer)
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index db9b6bfbf5c2fc4efffa1b666b23aa551a3602ee..2600c818391b755ebb8521cf3976b0eb8eaf7269 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -49,6 +49,8 @@
expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
project.ci_cd_settings.inbound_job_token_scope_enabled?)
+ expect(settings_data['pushRepositoryForJobTokenAllowed']).to eql(
+ project.ci_cd_settings.push_repository_for_job_token_allowed?)
if Gitlab.ee?
expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled?
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 6e101d07b9f20bca6e470e7424a3c0919d6cee68..8e07416a8f5f61376eb73cec97802b844141cc6d 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -18,7 +18,8 @@
full_path: project.full_path,
keep_latest_artifact: false,
job_token_scope_enabled: false,
- inbound_job_token_scope_enabled: false
+ inbound_job_token_scope_enabled: false,
+ push_repository_for_job_token_allowed: false
}
end
@@ -69,6 +70,23 @@
expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
end
+ context 'when push_repository_for_job_token_allowed requested to be true' do
+ let(:variables) do
+ {
+ full_path: project.full_path,
+ push_repository_for_job_token_allowed: true
+ }
+ end
+
+ it 'updates push_repository_for_job_token_allowed' do
+ post_graphql_mutation(mutation, current_user: user)
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_cd_settings.push_repository_for_job_token_allowed).to eq(true)
+ end
+ end
+
context 'when job_token_scope_enabled: true' do
let(:variables) do
{
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index c5d798f004f11dad5ea554946c3be7adc07b0005..5f7597e841eefd5234600224a88f59b8945303bb 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -103,6 +103,7 @@ ci_cd_settings:
- push_repository_for_job_token_allowed
remapped_attributes:
pipeline_variables_minimum_override_role: ci_pipeline_variables_minimum_override_role
+ push_repository_for_job_token_allowed: ci_push_repository_for_job_token_allowed
default_git_depth: ci_default_git_depth
forward_deployment_enabled: ci_forward_deployment_enabled
forward_deployment_rollback_allowed: ci_forward_deployment_rollback_allowed
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 1fe8de1f048719efbcd74fc62b3422fa8f0a0043..856f8de9780acee07d966eda1055c3d085b3d631 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3306,7 +3306,8 @@ def failure_message(diff)
'build_timeout',
'auto_devops_enabled',
'auto_devops_deploy_strategy',
- 'import_error'
+ 'import_error',
+ 'ci_push_repository_for_job_token_allowed'
)
end
end
@@ -4057,6 +4058,20 @@ def failure_message(diff)
let(:failed_status_code) { :not_found }
end
+ describe 'updating ci_push_repository_for_job_token_allowed attribute' do
+ it 'is disabled by default' do
+ expect(project.ci_push_repository_for_job_token_allowed).to be_falsey
+ end
+
+ it 'enables push to repository using job token' do
+ put(api(path, user), params: { ci_push_repository_for_job_token_allowed: true })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(project.reload.ci_push_repository_for_job_token_allowed).to be_truthy
+ expect(json_response['ci_push_repository_for_job_token_allowed']).to eq(true)
+ end
+ end
+
describe 'updating packages_enabled attribute' do
it 'is enabled by default' do
expect(project.packages_enabled).to be true