diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 8fc93e6f954828003e94edca423a0b32009fdf29..ed996589489fc66914666e8ff80e41a983954921 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -24,6 +24,7 @@ class PersonalAccessToken < ApplicationRecord scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) } scope :expired_today_and_not_notified, -> { where(["revoked = false AND expires_at = CURRENT_DATE AND after_expiry_notification_delivered = false"]) } scope :inactive, -> { where("revoked = true OR expires_at < CURRENT_DATE") } + scope :created_before, -> (date) { where("personal_access_tokens.created_at < :date", date: date) } scope :last_used_before, -> (date) { where("personal_access_tokens.created_at < :date AND (last_used_at < :date OR last_used_at IS NULL)", date: date) } scope :with_impersonation, -> { where(impersonation: true) } scope :without_impersonation, -> { where(impersonation: false) } diff --git a/lib/gitlab/cleanup/unused_personal_access_tokens.rb b/lib/gitlab/cleanup/personal_access_tokens.rb similarity index 59% rename from lib/gitlab/cleanup/unused_personal_access_tokens.rb rename to lib/gitlab/cleanup/personal_access_tokens.rb index 97bf808a0ca930d7557981476d283182edd10b03..88665e6ada53161a09c50be6e021cec2a49dd9e9 100644 --- a/lib/gitlab/cleanup/unused_personal_access_tokens.rb +++ b/lib/gitlab/cleanup/personal_access_tokens.rb @@ -2,31 +2,23 @@ module Gitlab module Cleanup - # Unused active Personal Access Tokens pose a risk to organizations - # in that they may have been, or may be, leaked to unauthorized - # individuals. They are likely providing little / no current value - # because they are not actively being used, and should therefore be - # proactively revoked. - class UnusedPersonalAccessTokens - # By default tokens that haven't been used for over 1 year will - # be revoked + class PersonalAccessTokens + # By default tokens that haven't been used for over 1 year will be revoked DEFAULT_TIME_PERIOD = 1.year - # To prevent inadvertently revoking actively used tokens, we - # provide a minimum time + # To prevent inadvertently revoking all tokens, we provide a minimum time MINIMUM_TIME_PERIOD = 1.day - attr_reader :logger, :last_used_before, :revocation_time, :group + attr_reader :logger, :cut_off_date, :revocation_time, :group - def initialize(last_used_before: DEFAULT_TIME_PERIOD.ago.beginning_of_day, logger: nil, group_full_path:) - # binding.pry - # Ensure last_used_before is a Time and far enough in the past - @last_used_before = last_used_before + def initialize(cut_off_date: DEFAULT_TIME_PERIOD.ago.beginning_of_day, logger: nil, group_full_path:) + @cut_off_date = cut_off_date # rubocop: disable CodeReuse/ActiveRecord @group = Group.find_by_full_path(group_full_path) # rubocop: enable CodeReuse/ActiveRecord + raise "Group with full_path #{group_full_path} not found" unless @group - raise "Invalid time: #{@last_used_before}" unless @last_used_before <= MINIMUM_TIME_PERIOD.ago + raise "Invalid time: #{@cut_off_date}" unless @cut_off_date <= MINIMUM_TIME_PERIOD.ago # Use a static revocation time to make correlation of revoked # tokens easier, should it be needed. @@ -36,27 +28,21 @@ def initialize(last_used_before: DEFAULT_TIME_PERIOD.ago.beginning_of_day, logge raise "Invalid logger: #{@logger}" unless @logger.respond_to?(:info) && @logger.respond_to?(:warn) end - # Revokes unused personal access tokens. - # A dry run is performed by default, logging what would be - # revoked. Pass `dry_run: false` explicitly to revoke tokens. - def run!(dry_run: true) + def run!(dry_run: true, revoke_active_tokens: false) # rubocop:disable Rails/Output if dry_run puts "Dry running. No changes will be made" + elsif revoke_active_tokens + puts "Revoking used and unused access tokens created before #{cut_off_date}..." else - puts "Revoking access tokens from before #{last_used_before}..." + puts "Revoking access tokens last used and created before #{cut_off_date}..." end # rubocop:enable Rails/Output - logger.info( - dry_run: dry_run, - group_full_path: group.full_path, - message: "Looking for Personal Access Tokens " \ - "last used before #{last_used_before}..." - ) + tokens_to_revoke = revocable_tokens(revoke_active_tokens) # rubocop:disable Cop/InBatches - revocable_tokens.in_batches do |access_tokens| + tokens_to_revoke.in_batches do |access_tokens| revoke_batch(access_tokens, dry_run) end # rubocop:enable Cop/InBatches @@ -64,12 +50,20 @@ def run!(dry_run: true) private - def revocable_tokens - PersonalAccessToken - .active - .owner_is_human - .last_used_before(last_used_before) - .for_users(group.users) + def revocable_tokens(revoke_active_tokens) + if revoke_active_tokens + PersonalAccessToken + .active + .owner_is_human + .created_before(cut_off_date) + .for_users(group.users) + else + PersonalAccessToken + .active + .owner_is_human + .last_used_before(cut_off_date) + .for_users(group.users) + end end def revoke_batch(access_tokens, dry_run) diff --git a/spec/lib/gitlab/cleanup/unused_personal_access_tokens_spec.rb b/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb similarity index 67% rename from spec/lib/gitlab/cleanup/unused_personal_access_tokens_spec.rb rename to spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb index eb83ced0e21166a2935444fbf53ef7d604d5cf4e..36c5d0e9b0c718f25f0d135ec28ddc7be8efd3f7 100644 --- a/spec/lib/gitlab/cleanup/unused_personal_access_tokens_spec.rb +++ b/spec/lib/gitlab/cleanup/personal_access_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Cleanup::UnusedPersonalAccessTokens do +RSpec.describe Gitlab::Cleanup::PersonalAccessTokens do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:project_bot) { create(:user, :project_bot) } @@ -16,6 +16,10 @@ create(:personal_access_token, created_at: last_used_at - 1.minute) end + let!(:old_actively_used_token) do + create(:personal_access_token, created_at: last_used_at - 1.minute, last_used_at: 1.day.ago) + end + let!(:old_unused_token_for_non_group_member) do create(:personal_access_token, created_at: last_used_at - 1.minute) end @@ -37,6 +41,7 @@ before do group.add_member(old_formerly_used_token.user, Gitlab::Access::DEVELOPER) + group.add_member(old_actively_used_token.user, Gitlab::Access::DEVELOPER) group.add_member(unused_token.user, Gitlab::Access::DEVELOPER) group.add_member(old_unused_token.user, Gitlab::Access::DEVELOPER) group.add_member(project_bot, Gitlab::Access::MAINTAINER) @@ -47,7 +52,7 @@ subject do described_class.new( logger: logger, - last_used_before: last_used_at, + cut_off_date: last_used_at, group_full_path: group_full_path ) end @@ -76,16 +81,42 @@ context 'in a real run' do let(:args) { { dry_run: false } } - it 'revokes only revocable tokens' do - subject.run!(**args) + context 'when revoking unused tokens' do + it 'revokes human-owned tokens created and last used over 1 year ago' do + subject.run!(**args) + + expect(PersonalAccessToken.active).to contain_exactly( + unused_token, + old_actively_used_token, + old_unused_project_access_token, + old_unused_token_for_non_group_member, + old_unused_token_for_subgroup_member + ) + expect(PersonalAccessToken.revoked).to contain_exactly( + old_unused_token, + old_formerly_used_token + ) + end + end + + context 'when revoking used and unused tokens' do + let(:args) { { dry_run: false, revoke_active_tokens: true } } + + it 'revokes human-owned tokens created over 1 year ago' do + subject.run!(**args) - expect(PersonalAccessToken.active).to contain_exactly( - unused_token, - old_unused_project_access_token, - old_unused_token_for_non_group_member, - old_unused_token_for_subgroup_member - ) - expect(PersonalAccessToken.revoked).to contain_exactly(old_unused_token, old_formerly_used_token) + expect(PersonalAccessToken.active).to contain_exactly( + unused_token, + old_unused_project_access_token, + old_unused_token_for_non_group_member, + old_unused_token_for_subgroup_member + ) + expect(PersonalAccessToken.revoked).to contain_exactly( + old_unused_token, + old_actively_used_token, + old_formerly_used_token + ) + end end it 'updates updated_at' do diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 9a2a35c08391b4e29e9d3b218678d5bdcc7a6780..68230e81c50acc50b54be70a977a6c1996a46623 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -65,6 +65,46 @@ end end + describe '.created_before' do + let(:last_used_at) { 1.month.ago.beginning_of_hour } + let!(:new_used_token) do + create(:personal_access_token, + created_at: last_used_at + 1.minute, + last_used_at: last_used_at + 1.minute + ) + end + + let!(:old_unused_token) do + create(:personal_access_token, + created_at: last_used_at - 1.minute + ) + end + + let!(:old_formerly_used_token) do + create(:personal_access_token, + created_at: last_used_at - 1.minute, + last_used_at: last_used_at - 1.minute + ) + end + + let!(:old_still_used_token) do + create(:personal_access_token, + created_at: last_used_at - 1.minute, + last_used_at: 1.minute.ago + ) + end + + subject { described_class.created_before(last_used_at) } + + it do + is_expected.to contain_exactly( + old_unused_token, + old_formerly_used_token, + old_still_used_token + ) + end + end + describe '.last_used_before' do let(:last_used_at) { 1.month.ago.beginning_of_hour } let!(:unused_token) { create(:personal_access_token) }