From 3e6a4b44dbd9285821831466efa888dad690a9f2 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck <1494491-nwittstruck@users.noreply.gitlab.com> Date: Tue, 10 Dec 2024 09:54:26 +0100 Subject: [PATCH 1/5] Admin Token API: Identify CI/CD Job Tokens Adds support for identifying CI/CD Job Tokens Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/508619 Changelog: added --- doc/api/admin/token.md | 2 ++ lib/authn/agnostic_token_identifier.rb | 3 +- lib/authn/tokens/ci_build_token.rb | 31 +++++++++++++++++++ .../authn/agnostic_token_identifier_spec.rb | 26 +++++++++++++--- spec/lib/authn/tokens/ci_build_token_spec.rb | 28 +++++++++++++++++ spec/requests/api/admin/token_spec.rb | 4 ++- 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 lib/authn/tokens/ci_build_token.rb create mode 100644 spec/lib/authn/tokens/ci_build_token_spec.rb diff --git a/doc/api/admin/token.md b/doc/api/admin/token.md index ff2bb1b4c3e998..fd124624adced9 100644 --- a/doc/api/admin/token.md +++ b/doc/api/admin/token.md @@ -27,6 +27,7 @@ Prerequisites: > - [Cluster agent tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172932) in GitLab 17.7. > - [Runner authentication tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173987) in GitLab 17.7. > - [Pipeline trigger tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174030) in GitLab 17.7. +> - [CI/CD Job Tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175234) in GitLab 17.8. Gets information for a given token. This endpoint supports the following tokens: @@ -38,6 +39,7 @@ Gets information for a given token. This endpoint supports the following tokens: - [Cluster agent tokens](../../security/tokens/index.md#gitlab-cluster-agent-tokens) - [Runner authentication tokens](../../security/tokens/index.md#runner-authentication-tokens) - [Pipeline trigger tokens](../../ci/triggers/index.md#create-a-pipeline-trigger-token) +- [CI/CD Job Tokens](../../security/tokens/index.md#cicd-job-tokens) ```plaintext POST /api/v4/admin/token diff --git a/lib/authn/agnostic_token_identifier.rb b/lib/authn/agnostic_token_identifier.rb index 424401443e1791..6f9a575a31023e 100644 --- a/lib/authn/agnostic_token_identifier.rb +++ b/lib/authn/agnostic_token_identifier.rb @@ -11,7 +11,8 @@ class AgnosticTokenIdentifier ::Authn::Tokens::OauthApplicationSecret, ::Authn::Tokens::ClusterAgentToken, ::Authn::Tokens::RunnerAuthenticationToken, - ::Authn::Tokens::CiTriggerToken + ::Authn::Tokens::CiTriggerToken, + ::Authn::Tokens::CiBuildToken ].freeze def self.token_for(plaintext, source) diff --git a/lib/authn/tokens/ci_build_token.rb b/lib/authn/tokens/ci_build_token.rb new file mode 100644 index 00000000000000..8b2c995568c375 --- /dev/null +++ b/lib/authn/tokens/ci_build_token.rb @@ -0,0 +1,31 @@ +# frozen_string_literal:true + +module Authn + module Tokens + class CiBuildToken + def self.prefix?(plaintext) + plaintext.start_with?(::Ci::Build::TOKEN_PREFIX) + end + + attr_reader :revocable, :source + + def initialize(plaintext, source) + # @revocable = ::Ci::Build.find_by_token(plaintext) + + @revocable = ::Ci::AuthJobFinder.new(token: plaintext).execute + + @source = source + end + + def present_with + ::API::Entities::Ci::Job + end + + def revoke!(_current_user) + raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank? + + raise ::Authn::AgnosticTokenIdentifier::UnsupportedTokenError, 'Unsupported token type' + end + end + end +end diff --git a/spec/lib/authn/agnostic_token_identifier_spec.rb b/spec/lib/authn/agnostic_token_identifier_spec.rb index fd21956074480d..3bda071c7d21a1 100644 --- a/spec/lib/authn/agnostic_token_identifier_spec.rb +++ b/spec/lib/authn/agnostic_token_identifier_spec.rb @@ -3,6 +3,14 @@ require 'spec_helper' RSpec.describe Authn::AgnosticTokenIdentifier, feature_category: :system_access do + shared_examples 'supported token type' do + describe '#initialize' do + it 'finds the correct revocable token type' do + expect(token).to be_instance_of(token_type) + end + end + end + using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } @@ -31,11 +39,19 @@ end with_them do - describe '#initialize' do - it 'finds the correct revocable token type' do - expect(token).to be_instance_of(token_type) - end - end + it_behaves_like 'supported token type' end end + + context 'with ci_build tokens' do + let(:plaintext) { create(:ci_build, status: :running).token } + let(:token_type) { ::Authn::Tokens::CiBuildToken } + + before do + rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s + stub_application_setting(ci_jwt_signing_key: rsa_key) + end + + it_behaves_like 'supported token type' + end end diff --git a/spec/lib/authn/tokens/ci_build_token_spec.rb b/spec/lib/authn/tokens/ci_build_token_spec.rb new file mode 100644 index 00000000000000..a50fe3a3266812 --- /dev/null +++ b/spec/lib/authn/tokens/ci_build_token_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Authn::Tokens::CiBuildToken, feature_category: :system_access do + let_it_be(:user) { create(:user) } + let(:ci_build) { create(:ci_build, status: :running) } + let(:token_type) { ::Authn::Tokens::CiBuildToken } + + subject(:token) { described_class.new(plaintext, :api_admin_token) } + + context 'with valid ci trigger token' do + let(:plaintext) { ci_build.token } + let(:valid_revocable) { ci_build } + + it_behaves_like 'finding the valid revocable' + + describe '#revoke!' do + it 'does not support revocation yet' do + expect do + token.revoke!(user) + end.to raise_error(::Authn::AgnosticTokenIdentifier::UnsupportedTokenError, 'Unsupported token type') + end + end + end + + it_behaves_like 'token handling with unsupported token type' +end diff --git a/spec/requests/api/admin/token_spec.rb b/spec/requests/api/admin/token_spec.rb index d38d1cb143e1b7..1b1c19783cde8c 100644 --- a/spec/requests/api/admin/token_spec.rb +++ b/spec/requests/api/admin/token_spec.rb @@ -53,6 +53,7 @@ let(:runner_authentication_token) { create(:ci_runner, registration_type: :authenticated_user) } let(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } let(:ci_trigger) { create(:ci_trigger) } + let(:ci_build) { create(:ci_build, status: :running) } let(:plaintext) { nil } let(:params) { { token: plaintext } } @@ -72,7 +73,8 @@ [ref(:cluster_agent_token), lazy { cluster_agent_token.token }], [ref(:runner_authentication_token), lazy { runner_authentication_token.token }], [ref(:impersonation_token), lazy { impersonation_token.token }], - [ref(:ci_trigger), lazy { ci_trigger.token }] + [ref(:ci_trigger), lazy { ci_trigger.token }], + [ref(:ci_build), lazy { ci_build.token }] ] end -- GitLab From 512fab3fc93badb70f68798bb2c58881750b00ce Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck <1494491-nwittstruck@users.noreply.gitlab.com> Date: Mon, 6 Jan 2025 11:49:55 +0100 Subject: [PATCH 2/5] Admin Token API: Identify CI/CD Job Tokens This commit adds support for identification of jobs that are not running. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/508619 Changelog: added --- app/finders/ci/auth_job_finder.rb | 9 +-- lib/authn/tokens/ci_build_token.rb | 4 +- spec/finders/ci/auth_job_finder_spec.rb | 66 ++++++++++++++------ spec/lib/authn/tokens/ci_build_token_spec.rb | 6 ++ 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/app/finders/ci/auth_job_finder.rb b/app/finders/ci/auth_job_finder.rb index 391c4517656c71..43985b1a736c14 100644 --- a/app/finders/ci/auth_job_finder.rb +++ b/app/finders/ci/auth_job_finder.rb @@ -6,15 +6,18 @@ class AuthJobFinder ErasedJobError = Class.new(AuthError) DeletedProjectError = Class.new(AuthError) - def initialize(token:) + def initialize(token:, should_validate_job: true) @token = token + @should_validate_job = should_validate_job end def execute! find_job_by_token.tap do |job| next unless job - validate_job!(job) + validate_job!(job) if @should_validate_job + + log_successful_job_auth(job) job.user.set_ci_job_token_scope!(job) if job.user end @@ -53,8 +56,6 @@ def validate_job!(job) validate_job_not_erased!(job) validate_project_presence!(job) - log_successful_job_auth(job) - true end diff --git a/lib/authn/tokens/ci_build_token.rb b/lib/authn/tokens/ci_build_token.rb index 8b2c995568c375..802956b0cd5258 100644 --- a/lib/authn/tokens/ci_build_token.rb +++ b/lib/authn/tokens/ci_build_token.rb @@ -10,9 +10,7 @@ def self.prefix?(plaintext) attr_reader :revocable, :source def initialize(plaintext, source) - # @revocable = ::Ci::Build.find_by_token(plaintext) - - @revocable = ::Ci::AuthJobFinder.new(token: plaintext).execute + @revocable = ::Ci::AuthJobFinder.new(token: plaintext, should_validate_job: false).execute @source = source end diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb index 1d8d9a70201af9..1ce93262acc298 100644 --- a/spec/finders/ci/auth_job_finder_spec.rb +++ b/spec/finders/ci/auth_job_finder_spec.rb @@ -41,34 +41,62 @@ end end - it 'raises error if the job is not running' do - job.success! - - expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' + context 'when should_validate_job is not set' do + it 'calls validations' do + expect(finder).to receive(:validate_job!) + finder.execute! + end end - it 'raises error if the job is erased' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:erased?).and_return(true) + context 'when it should not validate the job' do + let(:should_validate_job) { false } - expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' + let(:finder) do + described_class.new(token: token, should_validate_job: should_validate_job) + end + + it 'does not call validations' do + expect(finder).not_to receive(:validate_job!) + finder.execute! + end end - it 'raises error if the the project is missing' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).and_return(nil) + context 'when it should validate the job' do + let(:should_validate_job) { true } - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' - end + let(:finder) do + described_class.new(token: token, should_validate_job: should_validate_job) + end + + it 'raises error if the job is not running' do + job.success! - it 'raises error if the the project is being removed' do - project = instance_double(Project) + expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' + end + + it 'raises error if the job is erased' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:erased?).and_return(true) - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).twice.and_return(project) - expect(project).to receive(:pending_delete?).and_return(true) + expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' + end - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + it 'raises error if the the project is missing' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).and_return(nil) + + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + end + + it 'raises error if the the project is being removed' do + project = instance_double(Project) + + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).twice.and_return(project) + expect(project).to receive(:pending_delete?).and_return(true) + + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + end end context 'with wrong job token' do diff --git a/spec/lib/authn/tokens/ci_build_token_spec.rb b/spec/lib/authn/tokens/ci_build_token_spec.rb index a50fe3a3266812..d46a18c346787a 100644 --- a/spec/lib/authn/tokens/ci_build_token_spec.rb +++ b/spec/lib/authn/tokens/ci_build_token_spec.rb @@ -15,6 +15,12 @@ it_behaves_like 'finding the valid revocable' + context 'when the job is not running' do + let(:ci_build) { create(:ci_build, status: :success) } + + it_behaves_like 'finding the valid revocable' + end + describe '#revoke!' do it 'does not support revocation yet' do expect do -- GitLab From 41487297f3cc168d225713911a6bd20347ea57c4 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck <1494491-nwittstruck@users.noreply.gitlab.com> Date: Wed, 8 Jan 2025 16:53:24 +0100 Subject: [PATCH 3/5] Admin Token API: Identify CI/CD Job Tokens This commit uses a new finder for finding the CI/CD job tokens. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/508619 Changelog: added --- app/finders/ci/auth_job_finder.rb | 9 ++- .../ci/auth_job_including_invalid_finder.rb | 13 ++++ lib/authn/tokens/ci_build_token.rb | 2 +- spec/finders/ci/auth_job_finder_spec.rb | 66 ++++++------------- .../auth_job_including_invalid_finder_spec.rb | 32 +++++++++ 5 files changed, 69 insertions(+), 53 deletions(-) create mode 100644 app/finders/ci/auth_job_including_invalid_finder.rb create mode 100644 spec/finders/ci/auth_job_including_invalid_finder_spec.rb diff --git a/app/finders/ci/auth_job_finder.rb b/app/finders/ci/auth_job_finder.rb index 43985b1a736c14..391c4517656c71 100644 --- a/app/finders/ci/auth_job_finder.rb +++ b/app/finders/ci/auth_job_finder.rb @@ -6,18 +6,15 @@ class AuthJobFinder ErasedJobError = Class.new(AuthError) DeletedProjectError = Class.new(AuthError) - def initialize(token:, should_validate_job: true) + def initialize(token:) @token = token - @should_validate_job = should_validate_job end def execute! find_job_by_token.tap do |job| next unless job - validate_job!(job) if @should_validate_job - - log_successful_job_auth(job) + validate_job!(job) job.user.set_ci_job_token_scope!(job) if job.user end @@ -56,6 +53,8 @@ def validate_job!(job) validate_job_not_erased!(job) validate_project_presence!(job) + log_successful_job_auth(job) + true end diff --git a/app/finders/ci/auth_job_including_invalid_finder.rb b/app/finders/ci/auth_job_including_invalid_finder.rb new file mode 100644 index 00000000000000..f82d9a883aa060 --- /dev/null +++ b/app/finders/ci/auth_job_including_invalid_finder.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ci + class AuthJobIncludingInvalidFinder < AuthJobFinder + private + + def validate_job!(job) + log_successful_job_auth(job) + + true + end + end +end diff --git a/lib/authn/tokens/ci_build_token.rb b/lib/authn/tokens/ci_build_token.rb index 802956b0cd5258..2d25f11a08e8e1 100644 --- a/lib/authn/tokens/ci_build_token.rb +++ b/lib/authn/tokens/ci_build_token.rb @@ -10,7 +10,7 @@ def self.prefix?(plaintext) attr_reader :revocable, :source def initialize(plaintext, source) - @revocable = ::Ci::AuthJobFinder.new(token: plaintext, should_validate_job: false).execute + @revocable = ::Ci::AuthJobIncludingInvalidFinder.new(token: plaintext).execute @source = source end diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb index 1ce93262acc298..1d8d9a70201af9 100644 --- a/spec/finders/ci/auth_job_finder_spec.rb +++ b/spec/finders/ci/auth_job_finder_spec.rb @@ -41,62 +41,34 @@ end end - context 'when should_validate_job is not set' do - it 'calls validations' do - expect(finder).to receive(:validate_job!) - finder.execute! - end - end - - context 'when it should not validate the job' do - let(:should_validate_job) { false } - - let(:finder) do - described_class.new(token: token, should_validate_job: should_validate_job) - end + it 'raises error if the job is not running' do + job.success! - it 'does not call validations' do - expect(finder).not_to receive(:validate_job!) - finder.execute! - end + expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' end - context 'when it should validate the job' do - let(:should_validate_job) { true } - - let(:finder) do - described_class.new(token: token, should_validate_job: should_validate_job) - end - - it 'raises error if the job is not running' do - job.success! - - expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' - end + it 'raises error if the job is erased' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:erased?).and_return(true) - it 'raises error if the job is erased' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:erased?).and_return(true) - - expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' - end + expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' + end - it 'raises error if the the project is missing' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).and_return(nil) + it 'raises error if the the project is missing' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).and_return(nil) - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' - end + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + end - it 'raises error if the the project is being removed' do - project = instance_double(Project) + it 'raises error if the the project is being removed' do + project = instance_double(Project) - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).twice.and_return(project) - expect(project).to receive(:pending_delete?).and_return(true) + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).twice.and_return(project) + expect(project).to receive(:pending_delete?).and_return(true) - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' - end + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' end context 'with wrong job token' do diff --git a/spec/finders/ci/auth_job_including_invalid_finder_spec.rb b/spec/finders/ci/auth_job_including_invalid_finder_spec.rb new file mode 100644 index 00000000000000..fd3ec2f33c24bc --- /dev/null +++ b/spec/finders/ci/auth_job_including_invalid_finder_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::AuthJobIncludingInvalidFinder, :aggregate_failures, feature_category: :continuous_integration do + let_it_be(:user) { create(:user) } + let_it_be(:job) { create(:ci_build, status: :running, user: user) } + + let(:token) { job.token } + + subject(:finder) do + described_class.new(token: token) + end + + describe '#execute!' do + subject(:execute) { finder.execute! } + + it { is_expected.to eq(job) } + + it 'does not call validations' do + expect(finder).not_to receive(:validate_running_job!) + expect(finder).not_to receive(:validate_job_not_erased!) + expect(finder).not_to receive(:validate_project_presence!) + finder.execute! + end + + it 'does not raise for finished jobs' do + job.success! + expect(finder.execute!).to eq(job) + end + end +end -- GitLab From 0f0feb93d14e30b248671cfbf2d521fb0a210025 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck <1494491-nwittstruck@users.noreply.gitlab.com> Date: Sat, 11 Jan 2025 17:35:13 +0100 Subject: [PATCH 4/5] Admin Token API: Identify CI/CD Job Tokens This commit reverts back to changing the existing finder. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/508619 Changelog: added --- app/finders/ci/auth_job_finder.rb | 9 ++-- .../ci/auth_job_including_invalid_finder.rb | 13 ----- doc/api/admin/token.md | 2 +- lib/authn/agnostic_token_identifier.rb | 2 +- .../{ci_build_token.rb => ci_job_token.rb} | 6 +-- spec/finders/ci/auth_job_finder_spec.rb | 54 ++++++++++++------- .../auth_job_including_invalid_finder_spec.rb | 32 ----------- .../authn/agnostic_token_identifier_spec.rb | 18 +++++-- ...ild_token_spec.rb => ci_job_token_spec.rb} | 6 +-- 9 files changed, 63 insertions(+), 79 deletions(-) delete mode 100644 app/finders/ci/auth_job_including_invalid_finder.rb rename lib/authn/tokens/{ci_build_token.rb => ci_job_token.rb} (78%) delete mode 100644 spec/finders/ci/auth_job_including_invalid_finder_spec.rb rename spec/lib/authn/tokens/{ci_build_token_spec.rb => ci_job_token_spec.rb} (82%) diff --git a/app/finders/ci/auth_job_finder.rb b/app/finders/ci/auth_job_finder.rb index 391c4517656c71..8d5c500f7192be 100644 --- a/app/finders/ci/auth_job_finder.rb +++ b/app/finders/ci/auth_job_finder.rb @@ -6,8 +6,9 @@ class AuthJobFinder ErasedJobError = Class.new(AuthError) DeletedProjectError = Class.new(AuthError) - def initialize(token:) + def initialize(token:, skip_validations: false) @token = token + @skip_validations = skip_validations end def execute! @@ -16,6 +17,8 @@ def execute! validate_job!(job) + log_successful_job_auth(job) + job.user.set_ci_job_token_scope!(job) if job.user end end @@ -49,12 +52,12 @@ def link_composite_identity!(jwt) end def validate_job!(job) + return if @skip_validations + validate_running_job!(job) validate_job_not_erased!(job) validate_project_presence!(job) - log_successful_job_auth(job) - true end diff --git a/app/finders/ci/auth_job_including_invalid_finder.rb b/app/finders/ci/auth_job_including_invalid_finder.rb deleted file mode 100644 index f82d9a883aa060..00000000000000 --- a/app/finders/ci/auth_job_including_invalid_finder.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Ci - class AuthJobIncludingInvalidFinder < AuthJobFinder - private - - def validate_job!(job) - log_successful_job_auth(job) - - true - end - end -end diff --git a/doc/api/admin/token.md b/doc/api/admin/token.md index fd124624adced9..7c1e3b2f048ef4 100644 --- a/doc/api/admin/token.md +++ b/doc/api/admin/token.md @@ -27,7 +27,7 @@ Prerequisites: > - [Cluster agent tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172932) in GitLab 17.7. > - [Runner authentication tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173987) in GitLab 17.7. > - [Pipeline trigger tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174030) in GitLab 17.7. -> - [CI/CD Job Tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175234) in GitLab 17.8. +> - [CI/CD Job Tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175234) in GitLab 17.9. Gets information for a given token. This endpoint supports the following tokens: diff --git a/lib/authn/agnostic_token_identifier.rb b/lib/authn/agnostic_token_identifier.rb index 6f9a575a31023e..6d8c7cda707315 100644 --- a/lib/authn/agnostic_token_identifier.rb +++ b/lib/authn/agnostic_token_identifier.rb @@ -12,7 +12,7 @@ class AgnosticTokenIdentifier ::Authn::Tokens::ClusterAgentToken, ::Authn::Tokens::RunnerAuthenticationToken, ::Authn::Tokens::CiTriggerToken, - ::Authn::Tokens::CiBuildToken + ::Authn::Tokens::CiJobToken ].freeze def self.token_for(plaintext, source) diff --git a/lib/authn/tokens/ci_build_token.rb b/lib/authn/tokens/ci_job_token.rb similarity index 78% rename from lib/authn/tokens/ci_build_token.rb rename to lib/authn/tokens/ci_job_token.rb index 2d25f11a08e8e1..f944135384b4c7 100644 --- a/lib/authn/tokens/ci_build_token.rb +++ b/lib/authn/tokens/ci_job_token.rb @@ -2,7 +2,7 @@ module Authn module Tokens - class CiBuildToken + class CiJobToken def self.prefix?(plaintext) plaintext.start_with?(::Ci::Build::TOKEN_PREFIX) end @@ -10,13 +10,13 @@ def self.prefix?(plaintext) attr_reader :revocable, :source def initialize(plaintext, source) - @revocable = ::Ci::AuthJobIncludingInvalidFinder.new(token: plaintext).execute + @revocable = ::Ci::AuthJobFinder.new(token: plaintext, skip_validations: true).execute @source = source end def present_with - ::API::Entities::Ci::Job + ::API::Entities::Ci::JobBasic end def revoke!(_current_user) diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb index 1d8d9a70201af9..6078da83eacfdd 100644 --- a/spec/finders/ci/auth_job_finder_spec.rb +++ b/spec/finders/ci/auth_job_finder_spec.rb @@ -41,34 +41,50 @@ end end - it 'raises error if the job is not running' do - job.success! + context 'when skip_validations is not true' do + it 'raises error if the job is not running' do + job.success! - expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' - end + expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' + end - it 'raises error if the job is erased' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:erased?).and_return(true) + it 'raises error if the job is erased' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:erased?).and_return(true) - expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' - end + expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' + end + + it 'raises error if the the project is missing' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).and_return(nil) - it 'raises error if the the project is missing' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).and_return(nil) + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + end - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + it 'raises error if the the project is being removed' do + project = instance_double(Project) + + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).twice.and_return(project) + expect(project).to receive(:pending_delete?).and_return(true) + + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + end end - it 'raises error if the the project is being removed' do - project = instance_double(Project) + context 'when skip_validations is true' do + let(:finder) do + described_class.new(token: token, skip_validations: true) + end - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).twice.and_return(project) - expect(project).to receive(:pending_delete?).and_return(true) + context 'when job is not running' do + before do + job.success! + end - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' + it { is_expected.to eq(job) } + end end context 'with wrong job token' do diff --git a/spec/finders/ci/auth_job_including_invalid_finder_spec.rb b/spec/finders/ci/auth_job_including_invalid_finder_spec.rb deleted file mode 100644 index fd3ec2f33c24bc..00000000000000 --- a/spec/finders/ci/auth_job_including_invalid_finder_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Ci::AuthJobIncludingInvalidFinder, :aggregate_failures, feature_category: :continuous_integration do - let_it_be(:user) { create(:user) } - let_it_be(:job) { create(:ci_build, status: :running, user: user) } - - let(:token) { job.token } - - subject(:finder) do - described_class.new(token: token) - end - - describe '#execute!' do - subject(:execute) { finder.execute! } - - it { is_expected.to eq(job) } - - it 'does not call validations' do - expect(finder).not_to receive(:validate_running_job!) - expect(finder).not_to receive(:validate_job_not_erased!) - expect(finder).not_to receive(:validate_project_presence!) - finder.execute! - end - - it 'does not raise for finished jobs' do - job.success! - expect(finder.execute!).to eq(job) - end - end -end diff --git a/spec/lib/authn/agnostic_token_identifier_spec.rb b/spec/lib/authn/agnostic_token_identifier_spec.rb index 3bda071c7d21a1..9f28c4931534cf 100644 --- a/spec/lib/authn/agnostic_token_identifier_spec.rb +++ b/spec/lib/authn/agnostic_token_identifier_spec.rb @@ -43,15 +43,25 @@ end end - context 'with ci_build tokens' do - let(:plaintext) { create(:ci_build, status: :running).token } - let(:token_type) { ::Authn::Tokens::CiBuildToken } + context 'with CI Job tokens' do + let(:plaintext) { create(:ci_build, status: status).token } + let(:token_type) { ::Authn::Tokens::CiJobToken } before do rsa_key = OpenSSL::PKey::RSA.generate(3072).to_s stub_application_setting(ci_jwt_signing_key: rsa_key) end - it_behaves_like 'supported token type' + context 'when job is running' do + let(:status) { :running } + + it_behaves_like 'supported token type' + end + + context 'when job is not running' do + let(:status) { :success } + + it_behaves_like 'supported token type' + end end end diff --git a/spec/lib/authn/tokens/ci_build_token_spec.rb b/spec/lib/authn/tokens/ci_job_token_spec.rb similarity index 82% rename from spec/lib/authn/tokens/ci_build_token_spec.rb rename to spec/lib/authn/tokens/ci_job_token_spec.rb index d46a18c346787a..b15e13d906fdac 100644 --- a/spec/lib/authn/tokens/ci_build_token_spec.rb +++ b/spec/lib/authn/tokens/ci_job_token_spec.rb @@ -2,14 +2,14 @@ require 'spec_helper' -RSpec.describe Authn::Tokens::CiBuildToken, feature_category: :system_access do +RSpec.describe Authn::Tokens::CiJobToken, feature_category: :system_access do let_it_be(:user) { create(:user) } let(:ci_build) { create(:ci_build, status: :running) } - let(:token_type) { ::Authn::Tokens::CiBuildToken } + let(:token_type) { ::Authn::Tokens::CiJobToken } subject(:token) { described_class.new(plaintext, :api_admin_token) } - context 'with valid ci trigger token' do + context 'with valid ci build token' do let(:plaintext) { ci_build.token } let(:valid_revocable) { ci_build } -- GitLab From 185f71a7d28350173256a43faf6a5abffff1c290 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck <1494491-nwittstruck@users.noreply.gitlab.com> Date: Tue, 14 Jan 2025 10:24:12 +0100 Subject: [PATCH 5/5] Admin Token API: Identify CI/CD Job Tokens This commit reverts back to changing the existing finder. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/508619 Changelog: added --- app/finders/ci/auth_job_finder.rb | 9 ++-- lib/api/entities/ci/job_token.rb | 15 ++++++ lib/authn/tokens/ci_job_token.rb | 4 +- spec/finders/ci/auth_job_finder_spec.rb | 54 ++++++++-------------- spec/lib/api/entities/ci/job_token_spec.rb | 13 ++++++ spec/lib/authn/tokens/ci_job_token_spec.rb | 4 +- spec/requests/api/admin/token_spec.rb | 15 +++++- 7 files changed, 68 insertions(+), 46 deletions(-) create mode 100644 lib/api/entities/ci/job_token.rb create mode 100644 spec/lib/api/entities/ci/job_token_spec.rb diff --git a/app/finders/ci/auth_job_finder.rb b/app/finders/ci/auth_job_finder.rb index 8d5c500f7192be..391c4517656c71 100644 --- a/app/finders/ci/auth_job_finder.rb +++ b/app/finders/ci/auth_job_finder.rb @@ -6,9 +6,8 @@ class AuthJobFinder ErasedJobError = Class.new(AuthError) DeletedProjectError = Class.new(AuthError) - def initialize(token:, skip_validations: false) + def initialize(token:) @token = token - @skip_validations = skip_validations end def execute! @@ -17,8 +16,6 @@ def execute! validate_job!(job) - log_successful_job_auth(job) - job.user.set_ci_job_token_scope!(job) if job.user end end @@ -52,12 +49,12 @@ def link_composite_identity!(jwt) end def validate_job!(job) - return if @skip_validations - validate_running_job!(job) validate_job_not_erased!(job) validate_project_presence!(job) + log_successful_job_auth(job) + true end diff --git a/lib/api/entities/ci/job_token.rb b/lib/api/entities/ci/job_token.rb new file mode 100644 index 00000000000000..ebe8d0db78a5ab --- /dev/null +++ b/lib/api/entities/ci/job_token.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class JobToken < Grape::Entity + expose :job, with: ::API::Entities::Ci::JobBasic + + def job + object + end + end + end + end +end diff --git a/lib/authn/tokens/ci_job_token.rb b/lib/authn/tokens/ci_job_token.rb index f944135384b4c7..1ccc1050e2334f 100644 --- a/lib/authn/tokens/ci_job_token.rb +++ b/lib/authn/tokens/ci_job_token.rb @@ -10,13 +10,13 @@ def self.prefix?(plaintext) attr_reader :revocable, :source def initialize(plaintext, source) - @revocable = ::Ci::AuthJobFinder.new(token: plaintext, skip_validations: true).execute + @revocable = ::Ci::AuthJobFinder.new(token: plaintext).execute @source = source end def present_with - ::API::Entities::Ci::JobBasic + ::API::Entities::Ci::JobToken end def revoke!(_current_user) diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb index 6078da83eacfdd..1d8d9a70201af9 100644 --- a/spec/finders/ci/auth_job_finder_spec.rb +++ b/spec/finders/ci/auth_job_finder_spec.rb @@ -41,50 +41,34 @@ end end - context 'when skip_validations is not true' do - it 'raises error if the job is not running' do - job.success! - - expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' - end - - it 'raises error if the job is erased' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:erased?).and_return(true) + it 'raises error if the job is not running' do + job.success! - expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' - end + expect { execute }.to raise_error described_class::NotRunningJobError, 'Job is not running' + end - it 'raises error if the the project is missing' do - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).and_return(nil) + it 'raises error if the job is erased' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:erased?).and_return(true) - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' - end - - it 'raises error if the the project is being removed' do - project = instance_double(Project) + expect { execute }.to raise_error described_class::ErasedJobError, 'Job has been erased!' + end - expect(finder).to receive(:find_job_by_token).and_return(job) - expect(job).to receive(:project).twice.and_return(project) - expect(project).to receive(:pending_delete?).and_return(true) + it 'raises error if the the project is missing' do + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).and_return(nil) - expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' - end + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' end - context 'when skip_validations is true' do - let(:finder) do - described_class.new(token: token, skip_validations: true) - end + it 'raises error if the the project is being removed' do + project = instance_double(Project) - context 'when job is not running' do - before do - job.success! - end + expect(finder).to receive(:find_job_by_token).and_return(job) + expect(job).to receive(:project).twice.and_return(project) + expect(project).to receive(:pending_delete?).and_return(true) - it { is_expected.to eq(job) } - end + expect { execute }.to raise_error described_class::DeletedProjectError, 'Project has been deleted!' end context 'with wrong job token' do diff --git a/spec/lib/api/entities/ci/job_token_spec.rb b/spec/lib/api/entities/ci/job_token_spec.rb new file mode 100644 index 00000000000000..020d6bd24fe9f7 --- /dev/null +++ b/spec/lib/api/entities/ci/job_token_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Ci::JobToken, feature_category: :continuous_integration do + let_it_be(:job) { create(:ci_build) } + + subject(:job_token_entity) { described_class.new(job).as_json } + + it "exposes job" do + expect(job_token_entity).to include(:job) + end +end diff --git a/spec/lib/authn/tokens/ci_job_token_spec.rb b/spec/lib/authn/tokens/ci_job_token_spec.rb index b15e13d906fdac..e1049cf44e8b00 100644 --- a/spec/lib/authn/tokens/ci_job_token_spec.rb +++ b/spec/lib/authn/tokens/ci_job_token_spec.rb @@ -18,7 +18,9 @@ context 'when the job is not running' do let(:ci_build) { create(:ci_build, status: :success) } - it_behaves_like 'finding the valid revocable' + it 'is not found' do + expect(token.revocable).to be_nil + end end describe '#revoke!' do diff --git a/spec/requests/api/admin/token_spec.rb b/spec/requests/api/admin/token_spec.rb index 1b1c19783cde8c..f3f464b5c8999f 100644 --- a/spec/requests/api/admin/token_spec.rb +++ b/spec/requests/api/admin/token_spec.rb @@ -73,8 +73,7 @@ [ref(:cluster_agent_token), lazy { cluster_agent_token.token }], [ref(:runner_authentication_token), lazy { runner_authentication_token.token }], [ref(:impersonation_token), lazy { impersonation_token.token }], - [ref(:ci_trigger), lazy { ci_trigger.token }], - [ref(:ci_build), lazy { ci_build.token }] + [ref(:ci_trigger), lazy { ci_trigger.token }] ] end @@ -88,6 +87,18 @@ end end + context 'with valid CI job token' do + let(:token) { ci_build } + let(:plaintext) { ci_build.token } + + it 'contains a job' do + post_token + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['job']['id']).to eq(ci_build.id) + end + end + it_behaves_like 'rejecting invalid requests with admin' end -- GitLab