diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e01263738641446aa2f3342338a3bd366e0d4300..c2d27685baefc8af6977285f893eb31a494c5764 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1165,7 +1165,10 @@ def job_jwt_variables break variables unless Feature.enabled?(:ci_job_jwt, project, default_enabled: true) jwt = Gitlab::Ci::Jwt.for_build(self) + jwt_v2 = Gitlab::Ci::JwtV2.for_build(self) variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true) + variables.append(key: 'CI_JOB_JWT_V1', value: jwt, public: false, masked: true) + variables.append(key: 'CI_JOB_JWT_V2', value: jwt_v2, public: false, masked: true) rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e Gitlab::ErrorTracking.track_exception(e) end diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index 45fa1994342af11e2712d996533d777dab0572de..5cfceec927b724cf48eafeb16a9037b25b0486e1 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -60,6 +60,8 @@ There are also [Kubernetes-specific deployment variables](../../user/project/clu | `CI_JOB_ID` | 9.0 | all | The internal ID of the job, unique across all jobs in the GitLab instance. | | `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the Docker image running the job. | | `CI_JOB_JWT` | 12.10 | all | A RS256 JSON web token to authenticate with third party systems that support JWT authentication, for example [HashiCorp's Vault](../secrets/index.md). | +| `CI_JOB_JWT_V1` | 14.6 | all | The same value as `CI_JOB_JWT`. | +| `CI_JOB_JWT_V2` | 14.6 | all | [**alpha:**](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga) A newly formatted RS256 JSON web token to increase compatibility. Similar to `CI_JOB_JWT`, except the issuer (`iss`) claim is changed from `gitlab.com` to `https://gitlab.com`, `sub` has changed from `job_id` to a string that contains the project path, and an `aud` claim is added. Format is subject to change. | | `CI_JOB_MANUAL` | 8.12 | all | `true` if a job was started manually. | | `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job. | | `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the job's stage. | diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb new file mode 100644 index 0000000000000000000000000000000000000000..278353220e42c2615267e0040f8208345c7f1dba --- /dev/null +++ b/lib/gitlab/ci/jwt_v2.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class JwtV2 < Jwt + private + + def reserved_claims + super.merge( + iss: Settings.gitlab.base_url, + sub: "project_path:#{project.full_path}:ref_type:#{ref_type}:ref:#{source_ref}", + aud: Settings.gitlab.base_url + ) + end + end + end +end diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..33aaa145a3935a0776390588721f4365608e9fe5 --- /dev/null +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::JwtV2 do + let(:namespace) { build_stubbed(:namespace) } + let(:project) { build_stubbed(:project, namespace: namespace) } + let(:user) { build_stubbed(:user) } + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'auto-deploy-2020-03-19') } + let(:build) do + build_stubbed( + :ci_build, + project: project, + user: user, + pipeline: pipeline + ) + end + + subject(:ci_job_jwt_v2) { described_class.new(build, ttl: 30) } + + it { is_expected.to be_a Gitlab::Ci::Jwt } + + describe '#payload' do + subject(:payload) { ci_job_jwt_v2.payload } + + it 'has correct values for the standard JWT attributes' do + aggregate_failures do + expect(payload[:iss]).to eq(Settings.gitlab.base_url) + expect(payload[:aud]).to eq(Settings.gitlab.base_url) + expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index dcecaa3243efc0499615bf81c0dd10df927d9217..a741adbd227113e2427223eb4f4988c216404f3a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2645,6 +2645,8 @@ { key: 'CI_DEPENDENCY_PROXY_USER', value: 'gitlab-ci-token', public: true, masked: false }, { key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: 'my-token', public: false, masked: true }, { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true }, + { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true }, + { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true }, { key: 'CI_JOB_NAME', value: 'test', public: true, masked: false }, { key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false }, { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false }, @@ -2712,6 +2714,7 @@ before do allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt') + allow(Gitlab::Ci::JwtV2).to receive(:for_build).and_return('ci.job.jwtv2') build.set_token('my-token') build.yaml_variables = [] end @@ -2763,6 +2766,8 @@ let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } } let(:dependency_proxy_var) { { key: 'dependency_proxy', value: 'value', public: true, masked: false } } let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } } + let(:job_jwt_var_v1) { { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true } } + let(:job_jwt_var_v2) { { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true } } let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } } before do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index fed4ee3f3a48a059bb84b7f9535baf6d907ecca4..8ebf2efde43900fda7884de49a5ee8b841b27b0b 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -63,6 +63,7 @@ describe 'validations' do it { is_expected.to validate_presence_of :name } + it { is_expected.not_to allow_value('colon:in:path').for(:path) } # This is to validate that a specially crafted name cannot bypass a pattern match. See !72555 it { is_expected.to allow_value('group test_4').for(:name) } it { is_expected.not_to allow_value('test/../foo').for(:name) } it { is_expected.not_to allow_value('').for(:name) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 20fec9c21a2962b3f086524c4281a7067c4d015f..69e96ca2f9966f7612aed43e3ed0690ad3ae426b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -379,6 +379,7 @@ it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } + it { is_expected.not_to allow_value('colon:in:path').for(:path) } # This is to validate that a specially crafted name cannot bypass a pattern match. See !72555 it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(2000) }