112 lines
4.0 KiB
Ruby
112 lines
4.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
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
|
|
DEFAULT_TIME_PERIOD = 1.year
|
|
# To prevent inadvertently revoking actively used tokens, we
|
|
# provide a minimum time
|
|
MINIMUM_TIME_PERIOD = 1.day
|
|
|
|
attr_reader :logger, :last_used_before, :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
|
|
|
|
# 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
|
|
|
|
# Use a static revocation time to make correlation of revoked
|
|
# tokens easier, should it be needed.
|
|
@revocation_time = Time.current.utc
|
|
@logger = logger || Gitlab::AppJsonLogger
|
|
|
|
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)
|
|
# rubocop:disable Rails/Output
|
|
if dry_run
|
|
puts "Dry running. No changes will be made"
|
|
else
|
|
puts "Revoking access tokens from before #{last_used_before}..."
|
|
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}..."
|
|
)
|
|
|
|
# rubocop:disable Cop/InBatches
|
|
revocable_tokens.in_batches do |access_tokens|
|
|
revoke_batch(access_tokens, dry_run)
|
|
end
|
|
# rubocop:enable Cop/InBatches
|
|
end
|
|
|
|
private
|
|
|
|
def revocable_tokens
|
|
PersonalAccessToken
|
|
.active
|
|
.owner_is_human
|
|
.last_used_before(last_used_before)
|
|
.for_users(group.users)
|
|
end
|
|
|
|
def revoke_batch(access_tokens, dry_run)
|
|
# Capture a simplified set of attributes for logging and for
|
|
# determining when an error has led some records to not be
|
|
# updated
|
|
attrs = access_tokens.as_json(only: [:id, :user_id])
|
|
|
|
# Use `update_all` to bypass any validations which might
|
|
# prevent revocation. Manually specify updated_at.
|
|
affected_row_count = dry_run ? 0 : access_tokens.update_all(revoked: true, updated_at: @revocation_time)
|
|
|
|
message = {
|
|
dry_run: dry_run,
|
|
message: "Revoke token batch",
|
|
token_count: attrs.size,
|
|
updated_count: affected_row_count,
|
|
tokens: attrs,
|
|
group_full_path: group.full_path
|
|
}
|
|
|
|
# rubocop:disable Rails/Output
|
|
if dry_run
|
|
puts "Dry run complete. #{attrs.size} rows would be affected"
|
|
logger.info(message)
|
|
elsif affected_row_count.eql?(attrs.size)
|
|
puts "Finished. #{attrs.size} rows affected"
|
|
logger.info(message)
|
|
else
|
|
# :nocov:
|
|
puts "ERROR. #{affected_row_count} tokens deleted, #{attrs.size} tokens should have been deleted"
|
|
logger.warn(message)
|
|
# :nocov:
|
|
end
|
|
# rubocop:enable Rails/Output
|
|
end
|
|
end
|
|
end
|
|
end
|