From e505e445d8d6e321547f0aed64c0b65378cfd5fa Mon Sep 17 00:00:00 2001 From: Aboobacker MK Date: Wed, 9 Jul 2025 12:43:37 +0530 Subject: [PATCH 1/5] Database changes for adding oauth to credential inventory Database changes to add oauth tokens to the crendeitla inventory so that instance admin and group owners get better visibility into the oauth access tokens used by the team. Changelog: added --- .../authn/oauth_access_token_finder.rb | 88 ++++++ app/models/doorkeeper/access_token.rb | 51 ++++ ...to_user_details_and_oauth_access_tokens.rb | 16 ++ db/schema_migrations/20250613160154 | 1 + db/structure.sql | 2 + {ee/spec => spec}/factories/doorkeeper.rb | 10 + spec/factories/oauth_access_tokens.rb | 10 + .../authn/oauth_access_token_finder_spec.rb | 271 ++++++++++++++++++ spec/models/doorkeeper/access_token_spec.rb | 132 +++++++++ 9 files changed, 581 insertions(+) create mode 100644 app/finders/authn/oauth_access_token_finder.rb create mode 100644 db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb create mode 100644 db/schema_migrations/20250613160154 rename {ee/spec => spec}/factories/doorkeeper.rb (81%) create mode 100644 spec/finders/authn/oauth_access_token_finder_spec.rb diff --git a/app/finders/authn/oauth_access_token_finder.rb b/app/finders/authn/oauth_access_token_finder.rb new file mode 100644 index 00000000000000..115f6cb16fb261 --- /dev/null +++ b/app/finders/authn/oauth_access_token_finder.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Authn + class OauthAccessTokenFinder + def initialize(params = {}, current_user: nil) + @params = params + @current_user = current_user + end + + def execute + tokens = OauthAccessToken.all + tokens = by_users(tokens) + tokens = by_revoked_state(tokens) + tokens = by_created_before(tokens) + tokens = by_created_after(tokens) + tokens = by_expires_before(tokens) + tokens = by_expires_after(tokens) + tokens = by_state(tokens) + tokens = by_search(tokens) + sort(tokens) + end + + private + + attr_reader :current_user, :params + + def by_users(tokens) + return tokens unless @params[:users] + + tokens.for_users(@params[:users]) + end + + def by_revoked_state(tokens) + return tokens unless params.has_key?(:revoked) + + params[:revoked] == 'true' ? tokens.revoked : tokens.not_revoked + end + + def by_created_before(tokens) + return tokens unless params[:created_before] + + tokens.created_before(params[:created_before]) + end + + def by_created_after(tokens) + return tokens unless params[:created_after] + + tokens.created_after(params[:created_after]) + end + + def by_expires_before(tokens) + return tokens unless params[:expires_before] + + tokens.expires_before(params[:expires_before]) + end + + def by_expires_after(tokens) + return tokens unless params[:expires_after] + + tokens.expires_after(params[:expires_after]) + end + + def by_search(tokens) + return tokens unless params[:search] + + tokens.search(params[:search]) + end + + def by_state(tokens) + case @params[:state] + when 'active' + tokens.active + when 'inactive' + tokens.inactive + else + tokens + end + end + + def sort(tokens) + available_sort_orders = Doorkeeper::AccessToken.simple_sorts.keys + + return tokens unless available_sort_orders.include?(params[:sort]) + + tokens.order_by(params[:sort]) + end + end +end diff --git a/app/models/doorkeeper/access_token.rb b/app/models/doorkeeper/access_token.rb index 25c677874ffe2e..bb684cda75ad5a 100644 --- a/app/models/doorkeeper/access_token.rb +++ b/app/models/doorkeeper/access_token.rb @@ -6,10 +6,61 @@ module Doorkeeper # rubocop:disable Gitlab/BoundedContexts -- Override from a ge class AccessToken < ::ApplicationRecord include Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken include SafelyChangeColumnDefault + include Sortable + include Gitlab::SQL::Pattern + include CreatedAtFilterable columns_changing_default :organization_id belongs_to :organization, class_name: 'Organizations::Organization', optional: false belongs_to :resource_owner, class_name: 'User' + scope :preload_users, -> { preload(:resource_owner) } + scope :for_users, ->(users) { + users = User.where(id: users) if users.is_a?(Array) + joins(:resource_owner).merge(users) + } + + scope :expires_after, ->(time) { + where.not(expires_in: nil) + .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * INTERVAL '1 second' > :time", + time: time) + } + + scope :expires_before, ->(time) { + where.not(expires_in: nil) + .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * INTERVAL '1 second' < :time", + time: time) + } + + scope :expired, -> { + where.not(expires_in: nil) + .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * + INTERVAL '1 second' < ?", Time.current) + } + + scope :not_expired, -> { + where.not(expires_in: nil) + .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * + INTERVAL '1 second' > ?", Time.current) + } + scope :revoked, -> { where.not(revoked_at: nil) } + scope :revoked_after, ->(date) { where(arel_table[:revoked_at].gteq(date)) } + scope :not_revoked, -> { where(revoked_at: nil) } + scope :active, -> { not_expired.not_revoked } + scope :inactive, -> { revoked.or(expired) } + + def self.search(query) + fuzzy_search(query, [:scopes]) + end + + # All oauth tokens will expire after a certain time. + def expires? + true + end + + # All oauth tokens will expire after 2 hours. + def expires_soon? + true + end end end diff --git a/db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb b/db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb new file mode 100644 index 00000000000000..44beb5b2d89669 --- /dev/null +++ b/db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddIndexesToUserDetailsAndOauthAccessTokens < Gitlab::Database::Migration[2.3] + milestone '18.2' + + INDEX_NAME_OAUTH_TOKENS = 'idx_oauth_access_tokens_resource_owner_id' + disable_ddl_transaction! + + def up + add_concurrent_index :oauth_access_tokens, :resource_owner_id, name: INDEX_NAME_OAUTH_TOKENS # rubocop:disable Migration/PreventIndexCreation -- TODO + end + + def down + remove_concurrent_index_by_name :oauth_access_tokens, INDEX_NAME_OAUTH_TOKENS + end +end diff --git a/db/schema_migrations/20250613160154 b/db/schema_migrations/20250613160154 new file mode 100644 index 00000000000000..8d70be49b41904 --- /dev/null +++ b/db/schema_migrations/20250613160154 @@ -0,0 +1 @@ +c393bcee47d731d8d4ef177395e20972e32f1ed53aef01978c6072c2d1d95bc8 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 8a6e735264d67a..2c37624d98646b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -34405,6 +34405,8 @@ CREATE INDEX idx_oauth_access_grants_on_organization_id ON oauth_access_grants U CREATE INDEX idx_oauth_access_tokens_on_organization_id ON oauth_access_tokens USING btree (organization_id); +CREATE INDEX idx_oauth_access_tokens_resource_owner_id ON oauth_access_tokens USING btree (resource_owner_id); + CREATE INDEX idx_oauth_device_grants_on_organization_id ON oauth_device_grants USING btree (organization_id); CREATE INDEX idx_oauth_openid_requests_on_organization_id ON oauth_openid_requests USING btree (organization_id); diff --git a/ee/spec/factories/doorkeeper.rb b/spec/factories/doorkeeper.rb similarity index 81% rename from ee/spec/factories/doorkeeper.rb rename to spec/factories/doorkeeper.rb index 835972ca290a23..c0f095be3db012 100644 --- a/ee/spec/factories/doorkeeper.rb +++ b/spec/factories/doorkeeper.rb @@ -13,10 +13,20 @@ sequence(:resource_owner_id) { |n| n } association :application, factory: :doorkeeper_application expires_in { 2.hours } + organization factory :clientless_access_token do application { nil } end + + trait :revoked do + revoked_at { 1.hour.ago } + end + + trait :expired do + created_at { 1.week.ago } + expires_in { 1.hour.to_i } + end end factory :doorkeeper_application, class: 'Doorkeeper::Application' do diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index 6b011fcfccbc63..77e818b679a501 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -8,5 +8,15 @@ token { Doorkeeper::OAuth::Helpers::UniqueToken.generate } refresh_token { Doorkeeper::OAuth::Helpers::UniqueToken.generate } scopes { application.scopes } + expires_in { 2.hours.to_i } + + trait :revoked do + revoked_at { 1.hour.ago } + end + + trait :expired do + created_at { 1.week.ago } + expires_in { 1.hour.to_i } + end end end diff --git a/spec/finders/authn/oauth_access_token_finder_spec.rb b/spec/finders/authn/oauth_access_token_finder_spec.rb new file mode 100644 index 00000000000000..8f316b49cbae2a --- /dev/null +++ b/spec/finders/authn/oauth_access_token_finder_spec.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Authn::OauthAccessTokenFinder, feature_category: :system_access do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + let_it_be(:application) { create(:oauth_application) } + + let_it_be(:tokens) do + { + active: create(:oauth_access_token, resource_owner: user, application: application), + active_other: create(:oauth_access_token, resource_owner: other_user, application: application), + revoked: create(:oauth_access_token, :revoked, resource_owner: user, application: application), + revoked_other: create(:oauth_access_token, :revoked, resource_owner: other_user, application: application), + expired: create(:oauth_access_token, :expired, resource_owner: user, application: application) + } + end + + let(:params) { {} } + let(:current_user) { admin } + + subject(:finder_result) { described_class.new(params, current_user: current_user).execute } + + describe 'without filters' do + it 'returns all tokens' do + expect(finder_result).to match_array(tokens.values) + end + end + + describe 'by users' do + context 'when users param is nil' do + let(:params) { { users: nil } } + + it 'returns all tokens' do + expect(finder_result).to match_array(tokens.values) + end + end + + context 'when filtering by single user' do + let(:params) { { users: [user] } } + + it 'returns tokens for that user' do + expect(finder_result).to match_array([tokens[:active], tokens[:revoked], tokens[:expired]]) + end + end + + context 'when filtering by multiple users' do + let(:params) { { users: [user, other_user] } } + + it 'returns tokens for all specified users' do + expect(finder_result).to match_array(tokens.values) + end + end + + context 'when users array is empty' do + let(:params) { { users: [] } } + + it 'returns no tokens' do + expect(finder_result).to be_empty + end + end + end + + describe 'by revoked state' do + context 'when filtering for revoked tokens' do + let(:params) { { revoked: 'true' } } + + it 'returns only revoked tokens' do + expect(finder_result).to match_array([tokens[:revoked], tokens[:revoked_other]]) + end + end + + context 'when filtering for non-revoked tokens' do + let(:params) { { revoked: 'false' } } + + it 'returns only non-revoked tokens' do + expect(finder_result).to match_array([tokens[:active], tokens[:active_other], tokens[:expired]]) + end + end + end + + describe 'by created date' do + before do + tokens[:active_other].update!(created_at: 3.hours.ago) + end + + context 'when created_before is 1 hour ago' do + let(:params) { { created_before: 1.hour.ago } } + + it 'returns tokens created before that date' do + expect(finder_result).to match_array([tokens[:active_other], tokens[:expired]]) + end + end + + context 'when created_after is 1 hour ago' do + let(:params) { { created_after: 1.hour.ago } } + + it 'returns tokens created after that date' do + expect(finder_result).to match_array([tokens[:active], tokens[:revoked], tokens[:revoked_other]]) + end + end + end + + describe 'by expires before/after' do + context 'when expires_before is 1 hour from now' do + let(:params) { { expires_before: 1.hour.from_now } } + + it 'returns expired tokens' do + expect(finder_result).to match_array([tokens[:expired]]) + end + end + + context 'when expires_before is 3 hours from now' do + let(:params) { { expires_before: 3.hours.from_now } } + + it 'returns all tokens' do + expect(finder_result).to match_array(tokens.values) + end + end + + context 'when expires_after is 3 hours from now' do + let(:params) { { expires_after: 3.hours.from_now } } + + it 'returns no tokens (all expire in 2 hours)' do + expect(finder_result).to be_empty + end + end + + context 'when expires_after is 1 hour from now' do + let(:params) { { expires_after: 1.hour.from_now } } + + it 'returns all non-expired tokens' do + expect(finder_result).to match_array([tokens[:active], tokens[:active_other], tokens[:revoked], + tokens[:revoked_other]]) + end + end + end + + describe 'by search (scopes)' do + before do + tokens[:active].reload.update!(scopes: %w[api read_user]) + tokens[:active_other].reload.update!(scopes: %w[api write_repository]) + tokens[:revoked].reload.update!(scopes: 'read_user') + end + + context 'when searching for "api"' do + let(:params) { { search: 'api' } } + + it 'returns tokens containing the scope' do + expect(finder_result).to match_array([tokens[:active], tokens[:active_other]]) + end + end + + context 'when searching for "read_user"' do + let(:params) { { search: 'read_user' } } + + it 'returns tokens containing the scope' do + expect(finder_result).to match_array([tokens[:active], tokens[:revoked]]) + end + end + + context 'when searching for "write_repository"' do + let(:params) { { search: 'write_repository' } } + + it 'returns tokens containing the scope' do + expect(finder_result).to match_array([tokens[:active_other]]) + end + end + + context 'when searching for nonexistent scope' do + let(:params) { { search: 'nonexistent_scope' } } + + it 'returns no tokens' do + expect(finder_result).to be_empty + end + end + end + + describe 'by state' do + context 'when filtering for active tokens' do + let(:params) { { state: 'active' } } + + it 'returns only active tokens' do + expect(finder_result).to match_array([tokens[:active], tokens[:active_other]]) + end + end + + context 'when filtering for inactive tokens' do + let(:params) { { state: 'inactive' } } + + it 'returns only inactive tokens' do + expect(finder_result).to match_array([tokens[:revoked], tokens[:revoked_other], tokens[:expired]]) + end + end + + context 'when state param is nil' do + let(:params) { { state: nil } } + + it 'returns all tokens' do + expect(finder_result).to match_array(tokens.values) + end + end + + context 'when state param is an invalid value' do + let(:params) { { state: 'invalid_state' } } + + it 'returns all tokens' do + expect(finder_result).to match_array(tokens.values) + end + end + end + + describe 'sort' do + context 'when sorting by id ascending' do + let(:params) { { sort: 'id_asc' } } + + it 'returns tokens ordered by id ascending' do + expect(finder_result.map(&:id)).to eq(tokens.values.sort_by(&:id).map(&:id)) + end + end + + context 'when sorting by id descending' do + let(:params) { { sort: 'id_desc' } } + + it 'returns tokens ordered by id descending' do + expect(finder_result.map(&:id)).to eq(tokens.values.sort_by(&:id).reverse.map(&:id)) + end + end + end + + describe 'edge cases' do + context 'when revoked key is present but nil' do + let(:params) { { revoked: nil } } + + it 'treats nil as falsy and returns non-revoked tokens' do + expect(finder_result).to match_array([tokens[:active], tokens[:active_other], tokens[:expired]]) + end + end + + context 'when empty users array is provided' do + let(:params) { { users: [] } } + + it 'returns no tokens' do + expect(finder_result).to be_empty + end + end + end + + describe 'initialization' do + context 'with no parameters' do + subject(:finder_with_no_params) { described_class.new } + + it 'can be initialized without params' do + expect { finder_with_no_params.execute }.not_to raise_error + end + end + + context 'with empty params hash' do + subject(:finder_with_empty_params) { described_class.new({}, current_user: current_user) } + + it 'returns all tokens when no filters are applied' do + expect(finder_with_empty_params.execute).to match_array(tokens.values) + end + end + end + end +end diff --git a/spec/models/doorkeeper/access_token_spec.rb b/spec/models/doorkeeper/access_token_spec.rb index 9e1a14e4024343..0ca0127965a247 100644 --- a/spec/models/doorkeeper/access_token_spec.rb +++ b/spec/models/doorkeeper/access_token_spec.rb @@ -5,5 +5,137 @@ RSpec.describe Doorkeeper::AccessToken, type: :model, feature_category: :system_access do describe 'associations' do it { is_expected.to belong_to(:organization).class_name('Organizations::Organization').optional(false) } + it { is_expected.to belong_to(:resource_owner).class_name('User') } + end + + describe 'scopes' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:application) { create(:oauth_application) } + + let_it_be(:active_token) { create(:doorkeeper_access_token, resource_owner: user1, application: application) } + let_it_be(:revoked_token) do + create(:doorkeeper_access_token, :revoked, resource_owner: user1, application: application) + end + + let_it_be(:expired_token) do + create(:doorkeeper_access_token, :expired, resource_owner: user2, application: application) + end + + describe '.preload_users' do + it 'preloads resource_owner association' do + expect { described_class.preload_users.map(&:resource_owner) }.not_to exceed_query_limit(2) + end + end + + describe '.for_users' do + it 'returns tokens for specified users' do + expect(described_class.for_users([user1])).to match_array([active_token, revoked_token]) + end + + it 'returns tokens for multiple users' do + expect(described_class.for_users([user1, user2])).to match_array([active_token, revoked_token, expired_token]) + end + + it 'returns empty result for empty user array' do + expect(described_class.for_users([])).to be_empty + end + end + + describe '.revoked' do + it 'returns only revoked tokens' do + expect(described_class.revoked).to contain_exactly(revoked_token) + end + end + + describe '.not_revoked' do + it 'returns only non-revoked tokens' do + expect(described_class.not_revoked).to match_array([active_token, expired_token]) + end + end + + describe '.revoked_after' do + it 'returns tokens revoked after specified date' do + revoked_token.update!(revoked_at: 1.hour.ago) + expect(described_class.revoked_after(2.hours.ago)).to contain_exactly(revoked_token) + end + end + + describe '.expires_after' do + let_it_be(:token_expires_soon) do + create(:doorkeeper_access_token, resource_owner: user1, expires_in: 30.minutes.to_i) + end + + let_it_be(:token_expires_later) do + create(:doorkeeper_access_token, resource_owner: user1, expires_in: 4.hours.to_i) + end + + it 'returns tokens that expire after specified time' do + expect(described_class.expires_after(2.hours.from_now)).to contain_exactly(token_expires_later) + end + end + + describe '.expires_before' do + let_it_be(:token_expires_soon) do + create(:doorkeeper_access_token, resource_owner: user1, expires_in: 30.minutes.to_i) + end + + let_it_be(:token_expires_later) do + create(:doorkeeper_access_token, resource_owner: user1, expires_in: 4.hours.to_i) + end + + it 'returns tokens that expire before specified time' do + expect(described_class.expires_before(2.hours.from_now)).to include(token_expires_soon) + expect(described_class.expires_before(2.hours.from_now)).not_to include(token_expires_later) + end + + it 'includes expired tokens' do + expect(described_class.expires_before(Time.current)).to contain_exactly(expired_token) + end + end + end + + describe '.search' do + let_it_be(:user) { create(:user) } + let_it_be(:application) { create(:doorkeeper_application) } + let_it_be(:token_api) do + create(:doorkeeper_access_token, resource_owner: user, application: application, scopes: %w[api]) + end + + let_it_be(:token_read_user) do + create(:doorkeeper_access_token, resource_owner: user, application: application, scopes: %w[read_user]) + end + + let_it_be(:token_api_read) do + create(:doorkeeper_access_token, resource_owner: user, application: application, scopes: %w[api read_user]) + end + + it 'searches by scope content' do + expect(described_class.search('api')).to match_array([token_api, token_api_read]) + end + + it 'searches by partial scope content' do + expect(described_class.search('read')).to match_array([token_read_user, token_api_read]) + end + + it 'returns empty result for non-matching search' do + expect(described_class.search('nonexistent')).to be_empty + end + end + + describe '#expires?' do + let(:token) { build(:doorkeeper_access_token) } + + it 'always returns true' do + expect(token.expires?).to be true + end + end + + describe '#expires_soon?' do + let(:token) { build(:oauth_access_token) } + + it 'always returns true' do + expect(token.expires_soon?).to be true + end end end -- GitLab From 203f5d7895579ea78765742fa8d7cda5776e9cb8 Mon Sep 17 00:00:00 2001 From: Aboobacker MK Date: Wed, 9 Jul 2025 16:45:44 +0530 Subject: [PATCH 2/5] Move to post deployment migration --- .../20250709110433_add_index_to_oauth_tokens.rb} | 6 +++--- db/schema_migrations/20250613160154 | 1 - db/schema_migrations/20250709110433 | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) rename db/{migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb => post_migrate/20250709110433_add_index_to_oauth_tokens.rb} (83%) delete mode 100644 db/schema_migrations/20250613160154 create mode 100644 db/schema_migrations/20250709110433 diff --git a/db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb b/db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb similarity index 83% rename from db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb rename to db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb index 44beb5b2d89669..dfd397015e56e9 100644 --- a/db/migrate/20250613160154_add_indexes_to_user_details_and_oauth_access_tokens.rb +++ b/db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -class AddIndexesToUserDetailsAndOauthAccessTokens < Gitlab::Database::Migration[2.3] - milestone '18.2' +class AddIndexToOauthTokens < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.2' INDEX_NAME_OAUTH_TOKENS = 'idx_oauth_access_tokens_resource_owner_id' - disable_ddl_transaction! def up add_concurrent_index :oauth_access_tokens, :resource_owner_id, name: INDEX_NAME_OAUTH_TOKENS # rubocop:disable Migration/PreventIndexCreation -- TODO diff --git a/db/schema_migrations/20250613160154 b/db/schema_migrations/20250613160154 deleted file mode 100644 index 8d70be49b41904..00000000000000 --- a/db/schema_migrations/20250613160154 +++ /dev/null @@ -1 +0,0 @@ -c393bcee47d731d8d4ef177395e20972e32f1ed53aef01978c6072c2d1d95bc8 \ No newline at end of file diff --git a/db/schema_migrations/20250709110433 b/db/schema_migrations/20250709110433 new file mode 100644 index 00000000000000..a3b47d32832a79 --- /dev/null +++ b/db/schema_migrations/20250709110433 @@ -0,0 +1 @@ +88b869b3c3f189fc657bcecd9856e5ccf1d5f9fac76a7939c19adc7dae1f3c8c \ No newline at end of file -- GitLab From 65a3fcf3d445ab2d6d485c6e8f7929b8634f6b9d Mon Sep 17 00:00:00 2001 From: Aboobacker MK Date: Thu, 10 Jul 2025 16:21:15 +0530 Subject: [PATCH 3/5] Apply 1 suggestion(s) to 1 file(s) --- app/models/doorkeeper/access_token.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/doorkeeper/access_token.rb b/app/models/doorkeeper/access_token.rb index bb684cda75ad5a..dd67833dba8f88 100644 --- a/app/models/doorkeeper/access_token.rb +++ b/app/models/doorkeeper/access_token.rb @@ -49,6 +49,7 @@ class AccessToken < ::ApplicationRecord scope :active, -> { not_expired.not_revoked } scope :inactive, -> { revoked.or(expired) } + # Search by scopes def self.search(query) fuzzy_search(query, [:scopes]) end -- GitLab From 13b035e754e722a1a14e363df97a0d6329327ecb Mon Sep 17 00:00:00 2001 From: Aboobacker MK Date: Thu, 10 Jul 2025 20:12:35 +0530 Subject: [PATCH 4/5] Cleanup associations --- app/models/doorkeeper/access_token.rb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/models/doorkeeper/access_token.rb b/app/models/doorkeeper/access_token.rb index dd67833dba8f88..7684c107106687 100644 --- a/app/models/doorkeeper/access_token.rb +++ b/app/models/doorkeeper/access_token.rb @@ -19,35 +19,32 @@ class AccessToken < ::ApplicationRecord users = User.where(id: users) if users.is_a?(Array) joins(:resource_owner).merge(users) } - + scope :not_expired, -> { + has_expiration + .expires_after(Time.current) + } scope :expires_after, ->(time) { - where.not(expires_in: nil) + has_expiration .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * INTERVAL '1 second' > :time", time: time) } - scope :expires_before, ->(time) { - where.not(expires_in: nil) + has_expiration .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * INTERVAL '1 second' < :time", time: time) } - scope :expired, -> { - where.not(expires_in: nil) - .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * - INTERVAL '1 second' < ?", Time.current) + has_expiration.expires_before(Time.current) } - scope :not_expired, -> { - where.not(expires_in: nil) - .where("oauth_access_tokens.created_at + oauth_access_tokens.expires_in * - INTERVAL '1 second' > ?", Time.current) + has_expiration.expires_after(Time.current) } scope :revoked, -> { where.not(revoked_at: nil) } - scope :revoked_after, ->(date) { where(arel_table[:revoked_at].gteq(date)) } + scope :revoked_after, ->(date) { where(revoked_at: date...) } scope :not_revoked, -> { where(revoked_at: nil) } scope :active, -> { not_expired.not_revoked } scope :inactive, -> { revoked.or(expired) } + scope :has_expiration, -> { where.not(expires_in: nil) } # Search by scopes def self.search(query) -- GitLab From 7c081faa3006226f7c7b74e2a7a390577f502cd5 Mon Sep 17 00:00:00 2001 From: Aboobacker MK Date: Thu, 7 Aug 2025 16:18:20 +0400 Subject: [PATCH 5/5] remove db changes --- .../20250709110433_add_index_to_oauth_tokens.rb | 16 ---------------- db/schema_migrations/20250709110433 | 1 - db/structure.sql | 2 -- 3 files changed, 19 deletions(-) delete mode 100644 db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb delete mode 100644 db/schema_migrations/20250709110433 diff --git a/db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb b/db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb deleted file mode 100644 index dfd397015e56e9..00000000000000 --- a/db/post_migrate/20250709110433_add_index_to_oauth_tokens.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class AddIndexToOauthTokens < Gitlab::Database::Migration[2.3] - disable_ddl_transaction! - - milestone '18.2' - INDEX_NAME_OAUTH_TOKENS = 'idx_oauth_access_tokens_resource_owner_id' - - def up - add_concurrent_index :oauth_access_tokens, :resource_owner_id, name: INDEX_NAME_OAUTH_TOKENS # rubocop:disable Migration/PreventIndexCreation -- TODO - end - - def down - remove_concurrent_index_by_name :oauth_access_tokens, INDEX_NAME_OAUTH_TOKENS - end -end diff --git a/db/schema_migrations/20250709110433 b/db/schema_migrations/20250709110433 deleted file mode 100644 index a3b47d32832a79..00000000000000 --- a/db/schema_migrations/20250709110433 +++ /dev/null @@ -1 +0,0 @@ -88b869b3c3f189fc657bcecd9856e5ccf1d5f9fac76a7939c19adc7dae1f3c8c \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 2c37624d98646b..8a6e735264d67a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -34405,8 +34405,6 @@ CREATE INDEX idx_oauth_access_grants_on_organization_id ON oauth_access_grants U CREATE INDEX idx_oauth_access_tokens_on_organization_id ON oauth_access_tokens USING btree (organization_id); -CREATE INDEX idx_oauth_access_tokens_resource_owner_id ON oauth_access_tokens USING btree (resource_owner_id); - CREATE INDEX idx_oauth_device_grants_on_organization_id ON oauth_device_grants USING btree (organization_id); CREATE INDEX idx_oauth_openid_requests_on_organization_id ON oauth_openid_requests USING btree (organization_id); -- GitLab