diff --git a/doc/api/admin/token.md b/doc/api/admin/token.md index ff2bb1b4c3e9980db91c66a9a264331031b7a5ef..7c1e3b2f048ef47bd6289b9eb83b0047b08ff163 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.9. 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/api/entities/ci/job_token.rb b/lib/api/entities/ci/job_token.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebe8d0db78a5ab1f39845af6a3350f8895231906 --- /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/agnostic_token_identifier.rb b/lib/authn/agnostic_token_identifier.rb index 424401443e1791938ab95340926512582e8703e1..6d8c7cda707315ecff0bb819e2ea9fbc94925e5e 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::CiJobToken ].freeze def self.token_for(plaintext, source) diff --git a/lib/authn/tokens/ci_job_token.rb b/lib/authn/tokens/ci_job_token.rb new file mode 100644 index 0000000000000000000000000000000000000000..1ccc1050e2334f87ce9649fcb14d92904ad75048 --- /dev/null +++ b/lib/authn/tokens/ci_job_token.rb @@ -0,0 +1,29 @@ +# frozen_string_literal:true + +module Authn + module Tokens + class CiJobToken + def self.prefix?(plaintext) + plaintext.start_with?(::Ci::Build::TOKEN_PREFIX) + end + + attr_reader :revocable, :source + + def initialize(plaintext, source) + @revocable = ::Ci::AuthJobFinder.new(token: plaintext).execute + + @source = source + end + + def present_with + ::API::Entities::Ci::JobToken + 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/api/entities/ci/job_token_spec.rb b/spec/lib/api/entities/ci/job_token_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..020d6bd24fe9f7f92ea620ef382ca8e171852952 --- /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/agnostic_token_identifier_spec.rb b/spec/lib/authn/agnostic_token_identifier_spec.rb index fd21956074480d97e9784953ae09ee7d5a812ba1..9f28c4931534cf990ede4d0c32beee70d4703e4d 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,29 @@ 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 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 + + 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_job_token_spec.rb b/spec/lib/authn/tokens/ci_job_token_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e1049cf44e8b00df706a08f40620a0928df61d07 --- /dev/null +++ b/spec/lib/authn/tokens/ci_job_token_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +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::CiJobToken } + + subject(:token) { described_class.new(plaintext, :api_admin_token) } + + context 'with valid ci build token' do + let(:plaintext) { ci_build.token } + let(:valid_revocable) { ci_build } + + it_behaves_like 'finding the valid revocable' + + context 'when the job is not running' do + let(:ci_build) { create(:ci_build, status: :success) } + + it 'is not found' do + expect(token.revocable).to be_nil + end + end + + 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 d38d1cb143e1b78d73e363945e3d47e960ed44f3..f3f464b5c8999f5001051501e727d73eccb707e5 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 } } @@ -86,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