require_relative 'spec_helper' require_relative '../lib/gitlab_keys' require 'stringio' describe GitlabKeys do before do $logger = double('logger').as_null_object end describe '.command' do it 'the internal "command" utility function' do command = "#{ROOT_PATH}/bin/gitlab-shell does-not-validate" expect(described_class.command('does-not-validate')).to eq(command) end it 'does not raise a KeyError on invalid input' do command = "#{ROOT_PATH}/bin/gitlab-shell foo\nbar\nbaz\n" expect(described_class.command("foo\nbar\nbaz\n")).to eq(command) end end describe '.command_key' do it 'returns the "command" part of the key line' do command = "#{ROOT_PATH}/bin/gitlab-shell key-123" expect(described_class.command_key('key-123')).to eq(command) end it 'raises KeyError on invalid input' do expect { described_class.command_key("\nssh-rsa AAA") }.to raise_error(described_class::KeyError) end end describe '.key_line' do let(:line) { %(command="#{ROOT_PATH}/bin/gitlab-shell key-741",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E) } it 'returns the key line' do expect(described_class.key_line('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to eq(line) end it 'silently removes a trailing newline' do expect(described_class.key_line('key-741', "ssh-rsa AAAAB3NzaDAxx2E\n")).to eq(line) end it 'raises KeyError on invalid input' do expect { described_class.key_line('key-741', "ssh-rsa AAA\nssh-rsa AAA") }.to raise_error(described_class::KeyError) end end describe '.principal_line' do let(:line) { %(command="#{ROOT_PATH}/bin/gitlab-shell username-someuser",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty sshUsers) } it 'returns the key line' do expect(described_class.principal_line('username-someuser', 'sshUsers')).to eq(line) end it 'silently removes a trailing newline' do expect(described_class.principal_line('username-someuser', "sshUsers\n")).to eq(line) end it 'raises KeyError on invalid input' do expect { described_class.principal_line('username-someuser', "sshUsers\nloginUsers") }.to raise_error(described_class::KeyError) end end describe :initialize do let(:gitlab_keys) { build_gitlab_keys('add-key', 'key-741', 'ssh-rsa AAAAB3NzaDAxx2E') } it { expect(gitlab_keys.key).to eq('ssh-rsa AAAAB3NzaDAxx2E') } it { expect(gitlab_keys.instance_variable_get(:@command)).to eq('add-key') } it { expect(gitlab_keys.instance_variable_get(:@key_id)).to eq('key-741') } end describe :add_key do let(:gitlab_keys) { build_gitlab_keys('add-key', 'key-741', 'ssh-rsa AAAAB3NzaDAxx2E') } it "adds a line at the end of the file" do create_authorized_keys_fixture gitlab_keys.send :add_key auth_line = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n") end context "without file writing" do before { allow(gitlab_keys).to receive(:open) } before { create_authorized_keys_fixture } it "should log an add-key event" do expect($logger).to receive(:info).with("Adding key", {:key_id=>"key-741", :public_key=>"ssh-rsa AAAAB3NzaDAxx2E"}) gitlab_keys.send :add_key end it "should return true" do expect(gitlab_keys.send(:add_key)).to be_truthy end end end describe :list_keys do let(:gitlab_keys) do build_gitlab_keys('add-key', 'key-741', 'ssh-rsa AAAAB3NzaDAxx2E') end it 'adds a key and lists it' do create_authorized_keys_fixture gitlab_keys.send :add_key auth_line1 = 'key-741 AAAAB3NzaDAxx2E' expect(gitlab_keys.send(:list_keys)).to eq("#{auth_line1}\n") end end describe :list_key_ids do let(:gitlab_keys) { build_gitlab_keys('list-key-ids') } before do create_authorized_keys_fixture( existing_content: "key-1\tssh-dsa AAA\nkey-2\tssh-rsa BBB\nkey-3\tssh-rsa CCC\nkey-9000\tssh-rsa DDD\n" ) end it 'outputs the key IDs, separated by newlines' do expect { gitlab_keys.send(:list_key_ids) }.to output("1\n2\n3\n9000\n").to_stdout end end describe :batch_add_keys do let(:gitlab_keys) { build_gitlab_keys('batch-add-keys') } let(:fake_stdin) { StringIO.new("key-12\tssh-dsa ASDFASGADG\nkey-123\tssh-rsa GFDGDFSGSDFG\n", 'r') } before do create_authorized_keys_fixture allow(gitlab_keys).to receive(:stdin).and_return(fake_stdin) end it "adds lines at the end of the file" do gitlab_keys.send :batch_add_keys auth_line1 = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-12\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-dsa ASDFASGADG" auth_line2 = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-123\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa GFDGDFSGSDFG" expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n") end context "with invalid input" do let(:fake_stdin) { StringIO.new("key-12\tssh-dsa ASDFASGADG\nkey-123\tssh-rsa GFDGDFSGSDFG\nfoo\tbar\tbaz\n", 'r') } it "aborts" do expect(gitlab_keys).to receive(:abort) gitlab_keys.send :batch_add_keys end end context "without file writing" do before do expect(gitlab_keys).to receive(:open).and_yield(double(:file, puts: nil, chmod: nil)) end it "should log an add-key event" do expect($logger).to receive(:info).with("Adding key", key_id: 'key-12', public_key: "ssh-dsa ASDFASGADG") expect($logger).to receive(:info).with("Adding key", key_id: 'key-123', public_key: "ssh-rsa GFDGDFSGSDFG") gitlab_keys.send :batch_add_keys end it "should return true" do expect(gitlab_keys.send(:batch_add_keys)).to be_truthy end end end describe :stdin do let(:gitlab_keys) { build_gitlab_keys } subject { gitlab_keys.send :stdin } before { $stdin = 1 } it { is_expected.to equal(1) } end describe :rm_key do let(:gitlab_keys) { build_gitlab_keys('rm-key', 'key-741', 'ssh-rsa AAAAB3NzaDAxx2E') } it "removes the right line" do create_authorized_keys_fixture other_line = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" delete_line = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" open(tmp_authorized_keys_path, 'a') do |auth_file| auth_file.puts delete_line auth_file.puts other_line end gitlab_keys.send :rm_key erased_line = delete_line.gsub(/./, '#') expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") end context "without file writing" do before do allow(gitlab_keys).to receive(:open) allow(gitlab_keys).to receive(:lock).and_yield end it "should log an rm-key event" do expect($logger).to receive(:info).with("Removing key", key_id: "key-741") gitlab_keys.send :rm_key end it "should return true" do expect(gitlab_keys.send(:rm_key)).to be_truthy end end context 'without key content' do let(:gitlab_keys) { build_gitlab_keys('rm-key', 'key-741') } it "removes the right line by key ID" do create_authorized_keys_fixture other_line = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" delete_line = "command=\"#{ROOT_PATH}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" open(tmp_authorized_keys_path, 'a') do |auth_file| auth_file.puts delete_line auth_file.puts other_line end gitlab_keys.send :rm_key erased_line = delete_line.gsub(/./, '#') expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") end end end describe :clear do let(:gitlab_keys) { build_gitlab_keys('clear') } it "should return true" do allow(gitlab_keys).to receive(:open) expect(gitlab_keys.send(:clear)).to be_truthy end end describe :check_permissions do let(:gitlab_keys) { build_gitlab_keys('check-permissions') } it 'returns true when the file can be opened' do create_authorized_keys_fixture expect(gitlab_keys.exec).to eq(true) end it 'returns false if opening raises an exception' do expect(gitlab_keys).to receive(:open_auth_file).and_raise("imaginary error") expect(gitlab_keys.exec).to eq(false) end it 'creates the keys file if it does not exist' do create_authorized_keys_fixture FileUtils.rm(tmp_authorized_keys_path) expect(gitlab_keys.exec).to eq(true) expect(File.exist?(tmp_authorized_keys_path)).to eq(true) end end describe :exec do it 'add-key arg should execute add_key method' do gitlab_keys = build_gitlab_keys('add-key') expect(gitlab_keys).to receive(:add_key) gitlab_keys.exec end it 'batch-add-keys arg should execute batch_add_keys method' do gitlab_keys = build_gitlab_keys('batch-add-keys') expect(gitlab_keys).to receive(:batch_add_keys) gitlab_keys.exec end it 'rm-key arg should execute rm_key method' do gitlab_keys = build_gitlab_keys('rm-key') expect(gitlab_keys).to receive(:rm_key) gitlab_keys.exec end it 'clear arg should execute clear method' do gitlab_keys = build_gitlab_keys('clear') expect(gitlab_keys).to receive(:clear) gitlab_keys.exec end it 'check-permissions arg should execute check_permissions method' do gitlab_keys = build_gitlab_keys('check-permissions') expect(gitlab_keys).to receive(:check_permissions) gitlab_keys.exec end it 'should puts message if unknown command arg' do gitlab_keys = build_gitlab_keys('change-key') expect(gitlab_keys).to receive(:puts).with('not allowed') gitlab_keys.exec end it 'should log a warning on unknown commands' do gitlab_keys = build_gitlab_keys('nooope') allow(gitlab_keys).to receive(:puts).and_return(nil) expect($logger).to receive(:warn).with("Attempt to execute invalid gitlab-keys command", command: '"nooope"') gitlab_keys.exec end end describe :lock do before do allow_any_instance_of(GitlabKeys).to receive(:lock_file).and_return(tmp_lock_file_path) end it "should raise exception if operation lasts more then timeout" do key = GitlabKeys.new expect do key.send :lock, 1 do sleep 2 end end.to raise_error(Timeout::Error, 'execution expired') end it "should actually lock file" do $global = "" key = GitlabKeys.new thr1 = Thread.new do key.send :lock do # Put bigger sleep here to test if main thread will # wait for lock file released before executing code sleep 1 $global << "foo" end end # make sure main thread start lock command after # thread above sleep 0.5 key.send :lock do $global << "bar" end thr1.join expect($global).to eq("foobar") end end def build_gitlab_keys(*args) argv(*args) GitlabKeys.new end def argv(*args) args.each_with_index do |arg, i| ARGV[i] = arg.freeze end end def create_authorized_keys_fixture(existing_content: 'existing content') FileUtils.mkdir_p(File.dirname(tmp_authorized_keys_path)) open(tmp_authorized_keys_path, 'w') { |file| file.puts(existing_content) } allow(gitlab_keys).to receive(:auth_file).and_return(tmp_authorized_keys_path) end def tmp_authorized_keys_path File.join(ROOT_PATH, 'tmp', 'authorized_keys') end def tmp_lock_file_path tmp_authorized_keys_path + '.lock' end def capture_stdout(&blk) old = $stdout $stdout = fake = StringIO.new blk.call fake.string ensure $stdout = old end end