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