169 lines
4.8 KiB
Ruby
169 lines
4.8 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
require 'spec_helper'
|
||
|
|
||
|
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) }
|
||
|
|
||
|
let(:group_full_path) { group.full_path }
|
||
|
let(:logger) { instance_double(Gitlab::AppJsonLogger, info: nil, warn: nil) }
|
||
|
let(:last_used_at) { 1.month.ago.beginning_of_hour }
|
||
|
let!(:unused_token) { create(:personal_access_token) }
|
||
|
|
||
|
let!(:old_unused_token) do
|
||
|
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
|
||
|
|
||
|
let!(:old_unused_token_for_subgroup_member) do
|
||
|
create(:personal_access_token, created_at: last_used_at - 1.minute)
|
||
|
end
|
||
|
|
||
|
let!(:old_unused_project_access_token) do
|
||
|
create(:personal_access_token, user: project_bot, 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
|
||
|
|
||
|
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)
|
||
|
|
||
|
subgroup.add_member(old_unused_token_for_subgroup_member.user, Gitlab::Access::DEVELOPER)
|
||
|
end
|
||
|
|
||
|
subject do
|
||
|
described_class.new(
|
||
|
logger: logger,
|
||
|
cut_off_date: last_used_at,
|
||
|
group_full_path: group_full_path
|
||
|
)
|
||
|
end
|
||
|
|
||
|
context 'when initialized with an invalid logger' do
|
||
|
let(:logger) { "not a logger" }
|
||
|
|
||
|
it 'raises error' do
|
||
|
expect do
|
||
|
subject.run!
|
||
|
end.to raise_error('Invalid logger: not a logger')
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe '#run!' do
|
||
|
context 'when invalid group path passed' do
|
||
|
let(:group_full_path) { 'notagroup' }
|
||
|
|
||
|
it 'raises error' do
|
||
|
expect do
|
||
|
subject.run!
|
||
|
end.to raise_error("Group with full_path notagroup not found")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
context 'in a real run' do
|
||
|
let(:args) { { dry_run: false } }
|
||
|
|
||
|
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_actively_used_token,
|
||
|
old_formerly_used_token
|
||
|
)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
it 'updates updated_at' do
|
||
|
expect do
|
||
|
subject.run!(**args)
|
||
|
end.to change {
|
||
|
old_unused_token.reload.updated_at
|
||
|
}
|
||
|
end
|
||
|
|
||
|
it 'logs action as done' do
|
||
|
message = {
|
||
|
dry_run: false,
|
||
|
token_count: 2,
|
||
|
updated_count: 2,
|
||
|
tokens: instance_of(Array),
|
||
|
group_full_path: group_full_path
|
||
|
}
|
||
|
expect(logger).to receive(:info).with(include(message))
|
||
|
subject.run!(**args)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
context 'in a dry run' do
|
||
|
# Dry run is the default
|
||
|
let(:args) { {} }
|
||
|
|
||
|
it 'does not revoke any tokens' do
|
||
|
expect do
|
||
|
subject.run!(**args)
|
||
|
end.to not_change {
|
||
|
PersonalAccessToken.active.count
|
||
|
}
|
||
|
end
|
||
|
|
||
|
it 'logs what could be revoked' do
|
||
|
message = {
|
||
|
dry_run: true,
|
||
|
token_count: 2,
|
||
|
updated_count: 0,
|
||
|
tokens: instance_of(Array),
|
||
|
group_full_path: group_full_path
|
||
|
}
|
||
|
expect(logger).to receive(:info).with(include(message))
|
||
|
subject.run!(**args)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|