diff --git a/doc/ci/secrets/id_token_authentication.md b/doc/ci/secrets/id_token_authentication.md index 7a7d4e47c595ba44b17d726d0032c82af78b6be9..162b602818e574ea458ab56121f050959e1f18a1 100644 --- a/doc/ci/secrets/id_token_authentication.md +++ b/doc/ci/secrets/id_token_authentication.md @@ -98,7 +98,7 @@ The token also includes custom claims provided by GitLab: | `user_login` | Always | Username of the user executing the job. | | `user_email` | Always | Email of the user executing the job. | | `user_access_level` | Always | Access level of the user executing the job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/432052) in GitLab 16.9. | -| `job_project_id` | Always | ID of the project running the job. Use this to scope to the project by ID. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/563038) in GitLab 18.4. | +| `job_project_id` | Always | ID of the project running the job. Use this to scope to the project by ID. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues../jobs/_index.md#available-job-sources/563038) in GitLab 18.4. | | `job_project_path` | Always | Path of the project running the job. Use this to scope to the project by path. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/563038) in GitLab 18.4. | | `job_namespace_id` | Always | Namespace ID of the project running the job. Use this to scope to group or user level namespace by ID. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/563038) in GitLab 18.4. | | `job_namespace_path` | Always | Namespace path of the project running the job. Use this to scope to group or user level namespace by path. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/563038) in GitLab 18.4. | @@ -121,6 +121,8 @@ The token also includes custom claims provided by GitLab: | `ci_config_ref_uri` | Always | The ref path to the top-level pipeline definition, for example, `gitlab.example.com/my-group/my-project//.gitlab-ci.yml@refs/heads/main`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/404722) in GitLab 16.2. This claim is `null` unless the pipeline definition is located in the same project. | | `ci_config_sha` | Always | Git commit SHA for the `ci_config_ref_uri`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/404722) in GitLab 16.2. This claim is `null` unless the pipeline definition is located in the same project. | | `project_visibility` | Always | The [visibility](../../user/public_access.md) of the project where the pipeline is running. Can be `internal`, `private`, or `public`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/418810) in GitLab 16.3. | +| `job_source` | Always | [Job source](../jobs/_index.md#available-job-sources). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/459001) in GitLab 18.8. | +| `job_source_policy` | Job triggered by a policy | The `config_sha` and `config_uri` for the policy configuration which triggered the job. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/459001) in GitLab 18.8. | ```json { @@ -147,6 +149,11 @@ The token also includes custom claims provided by GitLab: "environment_protected": "false", "deployment_tier": "testing", "environment_action": "start", + "job_source": "push", + "job_source_policy": { + "config_uri": "https://gitlab.example.com/my-group/my-policy-project/.gitlab/security-policies/policy.yml@refs/heads/main", + "config_sha": "ab035e64eca9a7a85bd62e485d3593f52a2804ac" + }, "runner_id": 1, "runner_environment": "self-hosted", "sha": "714a629c0b401fdce83e847fc9589983fc6f46bc", diff --git a/ee/app/models/security/orchestration_policy_configuration.rb b/ee/app/models/security/orchestration_policy_configuration.rb index d4ac0115988d82c54c2f624ed5e4928ba8082c3d..ee4b311651f841450964b113fea9463762f4b4da 100644 --- a/ee/app/models/security/orchestration_policy_configuration.rb +++ b/ee/app/models/security/orchestration_policy_configuration.rb @@ -88,6 +88,11 @@ def self.policy_management_project?(project_id) self.exists?(security_policy_management_project_id: project_id) end + def self.policy_project_configuration_uri(project) + configuration_ref = Gitlab::Git::BRANCH_REF_PREFIX + project.default_branch_or_main + "#{Gitlab.config.gitlab.url}/#{project.full_path}/#{POLICY_PATH}@#{configuration_ref}" + end + def configuration_sha policy_last_commit&.id end diff --git a/ee/lib/ee/gitlab/ci/jwt_v2.rb b/ee/lib/ee/gitlab/ci/jwt_v2.rb new file mode 100644 index 0000000000000000000000000000000000000000..ad9344466e7bbaae88e2ff95230d28cd6f3a6b01 --- /dev/null +++ b/ee/lib/ee/gitlab/ci/jwt_v2.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module EE + module Gitlab + module Ci + module JwtV2 + extend ::Gitlab::Utils::Override + + private + + override :ci_claims + def ci_claims + super.merge(policy_claims) + end + + def policy_claims + policy_options = build.options[:policy] + return {} if policy_options&.dig(:sha).blank? || policy_options&.dig(:project_id).blank? + + policy_project = ::Project.find_by_id(policy_options[:project_id]) + return {} if policy_project.nil? + + { + job_source_policy: { + config_uri: ::Security::OrchestrationPolicyConfiguration.policy_project_configuration_uri(policy_project), + config_sha: policy_options[:sha] + }.compact + }.compact_blank + end + end + end + end +end diff --git a/ee/spec/lib/ee/gitlab/ci/jwt_v2_spec.rb b/ee/spec/lib/ee/gitlab/ci/jwt_v2_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..32ca2bd9ed463e1c9f004131be2730ac95dc48c5 --- /dev/null +++ b/ee/spec/lib/ee/gitlab/ci/jwt_v2_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::JwtV2, feature_category: :secrets_management do + let(:namespace) { build_stubbed(:namespace) } + let(:project) { build_stubbed(:project, namespace: namespace) } + let(:user) do + build_stubbed( + :user, + identities: [build_stubbed(:identity, extern_uid: '1', provider: 'github')] + ) + end + + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'auto-deploy-2020-03-19') } + let(:runner) { build_stubbed(:ci_runner) } + let(:aud) { nil } + let(:sub_components) { [:project_path, :ref_type, :ref] } + let(:target_audience) { nil } + let(:options) { nil } + + let(:build) do + build_stubbed( + :ci_build, + project: project, + user: user, + pipeline: pipeline, + runner: runner, + options: options + ) + end + + subject(:ci_job_jwt_v2) do + described_class.new(build, ttl: 30, aud: aud, sub_components: sub_components, target_audience: target_audience) + end + + describe '#payload' do + subject(:payload) { ci_job_jwt_v2.payload } + + describe 'job_source_policy' do + shared_examples_for 'does not include job_source_policy in the payload' do + it 'does not include job_source_policy in the payload' do + expect(payload).not_to include(:job_source_policy) + end + end + + context 'without options' do + it_behaves_like 'does not include job_source_policy in the payload' + end + + context 'with options but without policy option' do + let(:options) { { job_timeout: 30 } } + + it_behaves_like 'does not include job_source_policy in the payload' + end + + context 'with policy options' do + let_it_be_with_refind(:policy_project) { create(:project, :repository) } + + before do + allow(::Security::OrchestrationPolicyConfiguration).to receive(:policy_project_configuration_uri) + .with(policy_project) + .and_return('config_uri') + end + + context 'with all necessary options' do + let(:options) { { policy: { project_id: policy_project.id, sha: 'config_sha' } } } + + it 'contains job_source_policy' do + expect(payload[:job_source_policy]).to eq({ + config_uri: 'config_uri', + config_sha: 'config_sha' + }) + end + + context 'when project does not exist' do + before do + policy_project.destroy! + end + + it_behaves_like 'does not include job_source_policy in the payload' + end + end + + context 'when sha option is empty' do + let(:options) { { policy: { project_id: policy_project.id } } } + + it_behaves_like 'does not include job_source_policy in the payload' + end + + context 'when project_id option is empty' do + let(:options) { { policy: { sha: 'config_sha' } } } + + it_behaves_like 'does not include job_source_policy in the payload' + end + + context 'when neither sha not project_id is provided' do + let(:options) { { policy: {} } } + + it_behaves_like 'does not include job_source_policy in the payload' + end + end + end + end +end diff --git a/ee/spec/models/security/orchestration_policy_configuration_spec.rb b/ee/spec/models/security/orchestration_policy_configuration_spec.rb index fc7433aa799f3345983333e846125a26b0c88267..7abebdc0ab89d4b1ba95ed8453654f9650de1ea3 100644 --- a/ee/spec/models/security/orchestration_policy_configuration_spec.rb +++ b/ee/spec/models/security/orchestration_policy_configuration_spec.rb @@ -331,6 +331,22 @@ end end + describe '.policy_project_configuration_uri' do + subject(:configuration_uri) { described_class.policy_project_configuration_uri(project) } + + let_it_be(:project) { create(:project, :repository) } + + before do + allow(project).to receive(:default_branch_or_main).and_return('main') + allow(Gitlab.config.gitlab).to receive(:url).and_return('https://gitlab.example.com') + end + + it 'returns the URI to the policy file' do + expected_uri = "https://gitlab.example.com/#{project.full_path}/#{described_class::POLICY_PATH}@refs/heads/main" + expect(configuration_uri).to eq(expected_uri) + end + end + describe '#configuration_sha' do let(:last_commit) { instance_double(Commit, id: 'abc123') } diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb index 228f904b6b044eda7a88b1617519bd7574f30022..7d8edcf23547054aee5ba0c493763081e6540491 100644 --- a/lib/gitlab/ci/jwt_v2.rb +++ b/lib/gitlab/ci/jwt_v2.rb @@ -72,6 +72,12 @@ def predefined_claims super.merge(additional_custom_claims).merge(mapper.to_h) end + def ci_claims + super.merge( + job_source: build.source + ) + end + def user_identities return unless user&.pass_user_identities_to_ci_jwt @@ -107,3 +113,5 @@ def issuer_url end end end + +Gitlab::Ci::JwtV2.prepend_mod diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index cd1ac1c10f965e7d54c030eaeeb6bcc53cd8f7af..f6cfbfa0c25bf296d8d42cbdaffb823238c6bd5b 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -59,6 +59,26 @@ end end + describe 'job_source' do + context 'without build_source' do + it 'includes the job source' do + allow(pipeline).to receive(:source).and_return('api') + + expect(payload[:job_source]).to eq('api') + end + end + + context 'with build_source' do + it 'includes the job source' do + allow(build).to receive(:build_source).and_return( + build_stubbed(:ci_build_source, job: build, source: 'web') + ) + + expect(payload[:job_source]).to eq('web') + end + end + end + describe 'when only project_path provided' do let(:sub_components) { [:project_path] }