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 2c829bce3ee1e8f798fd132e72d17d08f0cd0d6c..4073f2ec7d427523b50dca14a38ccf51c1bd89c6 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -247,7 +247,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 8bdcb73ff84c7c6cba1bc2777d1d9913b472b34f..9ebe835af879babd652157a34a499aa4ca42944c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7419,6 +7419,7 @@ Input type: `ProjectCiCdSettingsUpdateInput` | <a id="mutationprojectcicdsettingsupdatemergepipelinesenabled"></a>`mergePipelinesEnabled` | [`Boolean`](#boolean) | Indicates if merged results pipelines are enabled for the project. | | <a id="mutationprojectcicdsettingsupdatemergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Indicates if merge trains are enabled for the project. | | <a id="mutationprojectcicdsettingsupdatemergetrainsskiptrainallowed"></a>`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. | +| <a id="mutationprojectcicdsettingsupdatepushrepositoryforjobtokenallowed"></a>`pushRepositoryForJobTokenAllowed` | [`Boolean`](#boolean) | Indicates the ability to push to the original project repository using a job token. | #### Fields @@ -29392,6 +29393,7 @@ four standard [pagination arguments](#pagination-arguments): | <a id="projectcicdsettingmergetrainsenabled"></a>`mergeTrainsEnabled` | [`Boolean`](#boolean) | Whether merge trains are enabled. | | <a id="projectcicdsettingmergetrainsskiptrainallowed"></a>`mergeTrainsSkipTrainAllowed` | [`Boolean!`](#boolean) | Whether merge immediately is allowed for merge trains. | | <a id="projectcicdsettingproject"></a>`project` | [`Project`](#project) | Project the CI/CD settings belong to. | +| <a id="projectcicdsettingpushrepositoryforjobtokenallowed"></a>`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 983217820cc7770b7976ba81c36a120901314c89..639b96367e0ad7f22751d8ed7248450c3d53f00c 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -249,6 +249,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", @@ -425,6 +427,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, @@ -546,6 +550,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, @@ -1218,6 +1224,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": [ { @@ -1760,6 +1768,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. | @@ -2379,6 +2389,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, @@ -2511,6 +2523,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 1cfb6cb6a2d0d826092f437dd95978e18f78712c..2178f7b3a0cead30eac84bacad4d2dabbf249db8 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -276,3 +276,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 aaa12c8665f8e6e183393b256a85241be2c908bf..93ef6adbb9189c1c177ea5348483f2a7ab367edb 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -131,6 +131,7 @@ class Project < ProjectDetails 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 0126b61043306d3ad9cc9ec4227bc99be8745e40..01425e95d420ad020b9ee20f2920681f8401d965 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -3704,19 +3704,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 @@ -3730,6 +3735,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 2b888e4a177aa1a671257688c07814e2685cc0ca..b12e6afba767c8414613b77bc4254a9f9f73d8b4 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -3323,7 +3323,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 @@ -4074,6 +4075,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