From ff403182b64c0260bfe550e26f411da967bd021e Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck <1494491-nwittstruck@users.noreply.gitlab.com> Date: Fri, 22 Nov 2024 22:39:05 +0000 Subject: [PATCH] Admin Token API: Add support for reading OAuth Application Secrets This commit adds support for oauth application secrets to the Admin Token API. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/443597 Changelog: added --- doc/api/admin/token.md | 2 ++ lib/api/admin/token.rb | 7 ++-- lib/authn/agnostic_token_identifier.rb | 4 ++- lib/authn/tokens/deploy_token.rb | 4 +++ lib/authn/tokens/feed_token.rb | 4 +++ lib/authn/tokens/oauth_application_secret.rb | 32 +++++++++++++++++++ lib/authn/tokens/personal_access_token.rb | 4 +++ .../authn/agnostic_token_identifier_spec.rb | 2 ++ .../tokens/oauth_application_secret_spec.rb | 29 +++++++++++++++++ spec/requests/api/admin/token_spec.rb | 5 ++- .../authn/token_shared_examples.rb | 6 ++++ 11 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 lib/authn/tokens/oauth_application_secret.rb create mode 100644 spec/lib/authn/tokens/oauth_application_secret_spec.rb diff --git a/doc/api/admin/token.md b/doc/api/admin/token.md index e9c109158839e..74c8c31b2302a 100644 --- a/doc/api/admin/token.md +++ b/doc/api/admin/token.md @@ -14,6 +14,7 @@ DETAILS: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165157) in GitLab 17.5 [with a flag](../../administration/feature_flags.md) named `admin_agnostic_token_finder`. Disabled by default. > - [Feed tokens added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169821) in GitLab 17.6. +> - [OAuth application secrets added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172985) in GitLab 17.7. FLAG: The availability of this feature is controlled by a feature flag. @@ -37,6 +38,7 @@ Supported tokens: - [Personal access tokens](../../user/profile/personal_access_tokens.md) - [Deploy tokens](../../user/project/deploy_tokens/index.md) - [Feed tokens](../../security/tokens/index.md#feed-token) +- [OAuth application secrets](../../integration/oauth_provider.md) ```plaintext POST /api/v4/admin/token diff --git a/lib/api/admin/token.rb b/lib/api/admin/token.rb index 9a54c95526f15..2100d2104c7a4 100644 --- a/lib/api/admin/token.rb +++ b/lib/api/admin/token.rb @@ -11,7 +11,7 @@ def identify_token(plaintext) token = ::Authn::AgnosticTokenIdentifier.token_for(plaintext, AUDIT_SOURCE) raise ArgumentError, 'Token type not supported.' if token.blank? - token.revocable + token end end @@ -45,12 +45,11 @@ def identify_token(plaintext) end post 'token' do identified_token = identify_token(params[:token]) - - render_api_error!({ error: 'Not found' }, :not_found) if identified_token.nil? + render_api_error!({ error: 'Not found' }, :not_found) if identified_token.revocable.nil? status :ok - present identified_token, with: "API::Entities::#{identified_token.class.name}".constantize + present identified_token.revocable, with: identified_token.present_with end end end diff --git a/lib/authn/agnostic_token_identifier.rb b/lib/authn/agnostic_token_identifier.rb index 672f03d6eba43..7f04f75e1bcc7 100644 --- a/lib/authn/agnostic_token_identifier.rb +++ b/lib/authn/agnostic_token_identifier.rb @@ -3,10 +3,12 @@ module Authn class AgnosticTokenIdentifier NotFoundError = Class.new(StandardError) + UnsupportedTokenError = Class.new(StandardError) TOKEN_TYPES = [ ::Authn::Tokens::DeployToken, ::Authn::Tokens::FeedToken, - ::Authn::Tokens::PersonalAccessToken + ::Authn::Tokens::PersonalAccessToken, + ::Authn::Tokens::OauthApplicationSecret ].freeze def self.token_for(plaintext, source) diff --git a/lib/authn/tokens/deploy_token.rb b/lib/authn/tokens/deploy_token.rb index 43b694adf172f..7ebaf1d98d482 100644 --- a/lib/authn/tokens/deploy_token.rb +++ b/lib/authn/tokens/deploy_token.rb @@ -14,6 +14,10 @@ def initialize(plaintext, source) @source = source end + def present_with + ::API::Entities::DeployToken + end + def revoke!(current_user) raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank? diff --git a/lib/authn/tokens/feed_token.rb b/lib/authn/tokens/feed_token.rb index 1b425c7640386..b46c35632c956 100644 --- a/lib/authn/tokens/feed_token.rb +++ b/lib/authn/tokens/feed_token.rb @@ -14,6 +14,10 @@ def initialize(plaintext, source) @source = source end + def present_with + ::API::Entities::User + end + def revoke!(current_user) raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank? diff --git a/lib/authn/tokens/oauth_application_secret.rb b/lib/authn/tokens/oauth_application_secret.rb new file mode 100644 index 0000000000000..84432afeaf735 --- /dev/null +++ b/lib/authn/tokens/oauth_application_secret.rb @@ -0,0 +1,32 @@ +# frozen_string_literal:true + +module Authn + module Tokens + class OauthApplicationSecret + def self.prefix?(plaintext) + prefix = + ::Gitlab::DoorkeeperSecretStoring::Token::UniqueApplicationToken::OAUTH_APPLICATION_SECRET_PREFIX_FORMAT + .split('-').first + + plaintext.start_with?(prefix) + end + + attr_reader :revocable, :source + + def initialize(plaintext, source) + @revocable = ::Doorkeeper::Application.find_by_plaintext_token(:secret, plaintext) + @source = source + end + + def present_with + ::API::Entities::Application + end + + def revoke!(_current_user) + raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank? + + raise ::Authn::AgnosticTokenIdentifier::UnsupportedTokenError, 'Revocation not supported for this token type' + end + end + end +end diff --git a/lib/authn/tokens/personal_access_token.rb b/lib/authn/tokens/personal_access_token.rb index 669debaf2b47a..67ef8681d7d16 100644 --- a/lib/authn/tokens/personal_access_token.rb +++ b/lib/authn/tokens/personal_access_token.rb @@ -17,6 +17,10 @@ def initialize(plaintext, source) @source = source end + def present_with + ::API::Entities::PersonalAccessToken + end + def revoke!(current_user) raise ::Authn::AgnosticTokenIdentifier::NotFoundError, 'Not Found' if revocable.blank? diff --git a/spec/lib/authn/agnostic_token_identifier_spec.rb b/spec/lib/authn/agnostic_token_identifier_spec.rb index b3d96cb6a4653..386b91894c219 100644 --- a/spec/lib/authn/agnostic_token_identifier_spec.rb +++ b/spec/lib/authn/agnostic_token_identifier_spec.rb @@ -9,6 +9,7 @@ let_it_be(:deploy_token) { create(:deploy_token).token } let_it_be(:feed_token) { user.feed_token } let_it_be(:personal_access_token) { create(:personal_access_token, user: user).token } + let_it_be(:oauth_application_secret) { create(:oauth_application).plaintext_secret } subject(:token) { described_class.token_for(plaintext, :group_token_revocation_service) } @@ -17,6 +18,7 @@ ref(:personal_access_token) | ::Authn::Tokens::PersonalAccessToken ref(:feed_token) | ::Authn::Tokens::FeedToken ref(:deploy_token) | ::Authn::Tokens::DeployToken + ref(:oauth_application_secret) | ::Authn::Tokens::OauthApplicationSecret 'unsupported' | NilClass end diff --git a/spec/lib/authn/tokens/oauth_application_secret_spec.rb b/spec/lib/authn/tokens/oauth_application_secret_spec.rb new file mode 100644 index 0000000000000..b0857f2708878 --- /dev/null +++ b/spec/lib/authn/tokens/oauth_application_secret_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Authn::Tokens::OauthApplicationSecret, feature_category: :system_access do + let_it_be(:user) { create(:user) } + + let(:oauth_application_secret) { create(:oauth_application) } + + subject(:token) { described_class.new(plaintext, :api_admin_token) } + + context 'with valid oauth application secret' do + let(:plaintext) { oauth_application_secret.plaintext_secret } + let(:valid_revocable) { oauth_application_secret } + + 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, + 'Revocation not supported for this 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 5cc09953497d7..2f411f6aab1a3 100644 --- a/spec/requests/api/admin/token_spec.rb +++ b/spec/requests/api/admin/token_spec.rb @@ -10,6 +10,8 @@ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:deploy_token) { create(:deploy_token) } + let_it_be(:oauth_application) { create(:oauth_application) } + let(:plaintext) { nil } let(:params) { { token: plaintext } } @@ -22,7 +24,8 @@ [ [ref(:personal_access_token), lazy { personal_access_token.token }], [ref(:deploy_token), lazy { deploy_token.token }], - [ref(:user), lazy { user.feed_token }] + [ref(:user), lazy { user.feed_token }], + [ref(:oauth_application), lazy { oauth_application.plaintext_secret }] ] end diff --git a/spec/support/shared_examples/authn/token_shared_examples.rb b/spec/support/shared_examples/authn/token_shared_examples.rb index a0ff50401de40..12c9741151f24 100644 --- a/spec/support/shared_examples/authn/token_shared_examples.rb +++ b/spec/support/shared_examples/authn/token_shared_examples.rb @@ -27,4 +27,10 @@ expect(token.revocable).to eq(valid_revocable) end end + + describe '#present_with' do + it 'returns a constant that is a subclass of Grape::Entity' do + expect(token.present_with).to be <= Grape::Entity + end + end end -- GitLab