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
 |