283 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
require 'spec_helper'
 | 
						|
 | 
						|
describe Gitlab::Gpg do
 | 
						|
  describe '.fingerprints_from_key' do
 | 
						|
    before do
 | 
						|
      # make sure that each method is using the temporary keychain
 | 
						|
      expect(described_class).to receive(:using_tmp_keychain).and_call_original
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns CurrentKeyChain.fingerprints_from_key' do
 | 
						|
      expect(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(GpgHelpers::User1.public_key)
 | 
						|
 | 
						|
      described_class.fingerprints_from_key(GpgHelpers::User1.public_key)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.primary_keyids_from_key' do
 | 
						|
    it 'returns the keyid' do
 | 
						|
      expect(
 | 
						|
        described_class.primary_keyids_from_key(GpgHelpers::User1.public_key)
 | 
						|
      ).to eq [GpgHelpers::User1.primary_keyid]
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns an empty array when the key is invalid' do
 | 
						|
      expect(
 | 
						|
        described_class.primary_keyids_from_key('bogus')
 | 
						|
      ).to eq []
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.subkeys_from_key' do
 | 
						|
    it 'returns the subkeys by primary key' do
 | 
						|
      all_subkeys = described_class.subkeys_from_key(GpgHelpers::User1.public_key)
 | 
						|
      subkeys = all_subkeys[GpgHelpers::User1.primary_keyid]
 | 
						|
 | 
						|
      expect(subkeys).to be_present
 | 
						|
      expect(subkeys.first[:keyid]).to be_present
 | 
						|
      expect(subkeys.first[:fingerprint]).to be_present
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns an empty array when there are not subkeys' do
 | 
						|
      all_subkeys = described_class.subkeys_from_key(GpgHelpers::User4.public_key)
 | 
						|
 | 
						|
      expect(all_subkeys[GpgHelpers::User4.primary_keyid]).to be_empty
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.user_infos_from_key' do
 | 
						|
    it 'returns the names and emails' do
 | 
						|
      user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key)
 | 
						|
      expect(user_infos).to eq([{
 | 
						|
        name: GpgHelpers::User1.names.first,
 | 
						|
        email: GpgHelpers::User1.emails.first
 | 
						|
      }])
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns an empty array when the key is invalid' do
 | 
						|
      expect(
 | 
						|
        described_class.user_infos_from_key('bogus')
 | 
						|
      ).to eq []
 | 
						|
    end
 | 
						|
 | 
						|
    it 'downcases the email' do
 | 
						|
      public_key = double(:key)
 | 
						|
      fingerprints = double(:fingerprints)
 | 
						|
      uid = double(:uid, name: +'Nannie Bernhard', email: +'NANNIE.BERNHARD@EXAMPLE.COM')
 | 
						|
      raw_key = double(:raw_key, uids: [uid])
 | 
						|
      allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
 | 
						|
      allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
 | 
						|
 | 
						|
      user_infos = described_class.user_infos_from_key(public_key)
 | 
						|
      expect(user_infos).to eq([{
 | 
						|
        name: 'Nannie Bernhard',
 | 
						|
        email: 'nannie.bernhard@example.com'
 | 
						|
      }])
 | 
						|
    end
 | 
						|
 | 
						|
    it 'rejects non UTF-8 names and addresses' do
 | 
						|
      public_key = double(:key)
 | 
						|
      fingerprints = double(:fingerprints)
 | 
						|
      email = (+"\xEEch@test.com").force_encoding('ASCII-8BIT')
 | 
						|
      uid = double(:uid, name: +'Test User', email: email)
 | 
						|
      raw_key = double(:raw_key, uids: [uid])
 | 
						|
      allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
 | 
						|
      allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
 | 
						|
 | 
						|
      user_infos = described_class.user_infos_from_key(public_key)
 | 
						|
      expect(user_infos).to eq([])
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.current_home_dir' do
 | 
						|
    let(:default_home_dir) { GPGME::Engine.dirinfo('homedir') }
 | 
						|
 | 
						|
    it 'returns the default value when no explicit home dir has been set' do
 | 
						|
      expect(described_class.current_home_dir).to eq default_home_dir
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns the explicitly set home dir' do
 | 
						|
      GPGME::Engine.home_dir = '/tmp/gpg'
 | 
						|
 | 
						|
      expect(described_class.current_home_dir).to eq '/tmp/gpg'
 | 
						|
 | 
						|
      GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir')
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns the default value when explicitly setting the home dir to nil' do
 | 
						|
      GPGME::Engine.home_dir = nil
 | 
						|
 | 
						|
      expect(described_class.current_home_dir).to eq default_home_dir
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.using_tmp_keychain' do
 | 
						|
    it "the second thread does not change the first thread's directory" do
 | 
						|
      thread1 = Thread.new do
 | 
						|
        described_class.using_tmp_keychain do
 | 
						|
          dir = described_class.current_home_dir
 | 
						|
          sleep 0.1
 | 
						|
          expect(described_class.current_home_dir).to eq dir
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      thread2 = Thread.new do
 | 
						|
        described_class.using_tmp_keychain do
 | 
						|
          sleep 0.2
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      thread1.join
 | 
						|
      thread2.join
 | 
						|
    end
 | 
						|
 | 
						|
    it 'allows recursive execution in the same thread' do
 | 
						|
      expect do
 | 
						|
        described_class.using_tmp_keychain do
 | 
						|
          described_class.using_tmp_keychain do
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end.not_to raise_error
 | 
						|
    end
 | 
						|
 | 
						|
    it 'keeps track of created and removed keychains in counters' do
 | 
						|
      created = Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total, 'The number of temporary GPG keychains')
 | 
						|
      removed = Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total, 'The number of temporary GPG keychains')
 | 
						|
 | 
						|
      initial_created = created.get
 | 
						|
      initial_removed = removed.get
 | 
						|
 | 
						|
      described_class.using_tmp_keychain do
 | 
						|
        expect(created.get).to eq(initial_created + 1)
 | 
						|
        expect(removed.get).to eq(initial_removed)
 | 
						|
      end
 | 
						|
 | 
						|
      expect(removed.get).to eq(initial_removed + 1)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'cleans up the tmp directory after finishing' do
 | 
						|
      tmp_directory = nil
 | 
						|
 | 
						|
      described_class.using_tmp_keychain do
 | 
						|
        tmp_directory = described_class.current_home_dir
 | 
						|
        expect(File.exist?(tmp_directory)).to be true
 | 
						|
      end
 | 
						|
 | 
						|
      expect(tmp_directory).not_to be_nil
 | 
						|
      expect(File.exist?(tmp_directory)).to be false
 | 
						|
    end
 | 
						|
 | 
						|
    it 'does not fail if the homedir was deleted while running' do
 | 
						|
      expect do
 | 
						|
        described_class.using_tmp_keychain do
 | 
						|
          FileUtils.remove_entry(described_class.current_home_dir)
 | 
						|
        end
 | 
						|
      end.not_to raise_error
 | 
						|
    end
 | 
						|
 | 
						|
    it 'tracks an exception when cleaning up the tmp dir fails' do
 | 
						|
      expected_exception = described_class::CleanupError.new('cleanup failed')
 | 
						|
      expected_tmp_dir = nil
 | 
						|
 | 
						|
      expect(described_class).to receive(:cleanup_tmp_dir).and_raise(expected_exception)
 | 
						|
      allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
 | 
						|
 | 
						|
      described_class.using_tmp_keychain do
 | 
						|
        expected_tmp_dir = described_class.current_home_dir
 | 
						|
        FileUtils.touch(File.join(expected_tmp_dir, 'dummy.file'))
 | 
						|
      end
 | 
						|
 | 
						|
      expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(
 | 
						|
        expected_exception,
 | 
						|
        issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918',
 | 
						|
        tmp_dir: expected_tmp_dir, contents: ['dummy.file']
 | 
						|
      )
 | 
						|
    end
 | 
						|
 | 
						|
    shared_examples 'multiple deletion attempts of the tmp-dir' do |seconds|
 | 
						|
      let(:tmp_dir) do
 | 
						|
        tmp_dir = Dir.mktmpdir
 | 
						|
        allow(Dir).to receive(:mktmpdir).and_return(tmp_dir)
 | 
						|
        tmp_dir
 | 
						|
      end
 | 
						|
 | 
						|
      before do
 | 
						|
        # Stub all the other calls for `remove_entry`
 | 
						|
        allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
 | 
						|
      end
 | 
						|
 | 
						|
      it "tries for #{seconds} or 15 times" do
 | 
						|
        expect(Retriable).to receive(:retriable).with(a_hash_including(max_elapsed_time: seconds, tries: 15))
 | 
						|
 | 
						|
        described_class.using_tmp_keychain {}
 | 
						|
      end
 | 
						|
 | 
						|
      it 'tries at least 2 times to remove the tmp dir before raising', :aggregate_failures do
 | 
						|
        expect(Retriable).to receive(:sleep).at_least(:twice)
 | 
						|
        expect(FileUtils).to receive(:remove_entry).with(tmp_dir).at_least(:twice).and_raise('Deletion failed')
 | 
						|
 | 
						|
        expect { described_class.using_tmp_keychain { } }.to raise_error(described_class::CleanupError)
 | 
						|
      end
 | 
						|
 | 
						|
      it 'does not attempt multiple times when the deletion succeeds' do
 | 
						|
        expect(Retriable).to receive(:sleep).once
 | 
						|
        expect(FileUtils).to receive(:remove_entry).with(tmp_dir).once.and_raise('Deletion failed')
 | 
						|
        expect(FileUtils).to receive(:remove_entry).with(tmp_dir).and_call_original
 | 
						|
 | 
						|
        expect { described_class.using_tmp_keychain { } }.not_to raise_error
 | 
						|
 | 
						|
        expect(File.exist?(tmp_dir)).to be false
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    it_behaves_like 'multiple deletion attempts of the tmp-dir', described_class::FG_CLEANUP_RUNTIME_S
 | 
						|
 | 
						|
    context 'when running in Sidekiq' do
 | 
						|
      before do
 | 
						|
        allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
 | 
						|
      end
 | 
						|
 | 
						|
      it_behaves_like 'multiple deletion attempts of the tmp-dir', described_class::BG_CLEANUP_RUNTIME_S
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
describe Gitlab::Gpg::CurrentKeyChain do
 | 
						|
  around do |example|
 | 
						|
    Gitlab::Gpg.using_tmp_keychain do
 | 
						|
      example.run
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.add' do
 | 
						|
    it 'stores the key in the keychain' do
 | 
						|
      expect(GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)).to eq []
 | 
						|
 | 
						|
      described_class.add(GpgHelpers::User1.public_key)
 | 
						|
 | 
						|
      keys = GPGME::Key.find(:public, GpgHelpers::User1.fingerprint)
 | 
						|
      expect(keys.count).to eq 1
 | 
						|
      expect(keys.first).to have_attributes(
 | 
						|
        email: GpgHelpers::User1.emails.first,
 | 
						|
        fingerprint: GpgHelpers::User1.fingerprint
 | 
						|
      )
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.fingerprints_from_key' do
 | 
						|
    it 'returns the fingerprint' do
 | 
						|
      expect(
 | 
						|
        described_class.fingerprints_from_key(GpgHelpers::User1.public_key)
 | 
						|
      ).to eq [GpgHelpers::User1.fingerprint]
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns an empty array when the key is invalid' do
 | 
						|
      expect(
 | 
						|
        described_class.fingerprints_from_key('bogus')
 | 
						|
      ).to eq []
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |