summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2017-12-19 11:42:09 +0000
committerNick Thomas <nick@gitlab.com>2017-12-19 14:09:12 +0000
commit5c61b2ef7a3b016cfe41f9569679ad7ad6c57112 (patch)
tree659b54a730d34aa56bb8ac091eca690d0e8a8bb1
parent5cad42aadaa1bea02f3edb3bfffc95cb0799c259 (diff)
downloadgitlab-shell-5c61b2ef7a3b016cfe41f9569679ad7ad6c57112.tar.gz
Introduce a more-complete implementation of bin/authorized_keys
bin/authorized_keys doesn't check that the requesting user matches the expected user, so to enable database authorized keys lookups, we currently ask the admin to create a custom script for that purpose. Better is to have a complete script that can perform the whole task. This commit introduces bin/gitlab-shell-authorized-keys-check which does so.
-rw-r--r--.gitlab-ci.yml6
-rwxr-xr-xbin/gitlab-shell-authorized-keys-check42
-rw-r--r--spec/gitlab_shell_authorized_keys_check_spec.rb106
-rw-r--r--spec/httpunix_spec.rb53
-rw-r--r--spec/spec_helper.rb29
5 files changed, 197 insertions, 39 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ad511bd..c04e7f8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -10,7 +10,7 @@ before_script:
rspec:
script:
- - bundle exec rspec spec
+ - bundle exec rspec -f d spec
tags:
- ruby
except:
@@ -28,7 +28,7 @@ rubocop:
rspec:ruby2.2:
image: ruby:2.2
script:
- - bundle exec rspec spec
+ - bundle exec rspec -f d spec
tags:
- ruby
except:
@@ -38,7 +38,7 @@ rspec:ruby2.2:
rspec:ruby2.1:
image: ruby:2.1
script:
- - bundle exec rspec spec
+ - bundle exec rspec -f d spec
tags:
- ruby
except:
diff --git a/bin/gitlab-shell-authorized-keys-check b/bin/gitlab-shell-authorized-keys-check
new file mode 100755
index 0000000..2ea1a74
--- /dev/null
+++ b/bin/gitlab-shell-authorized-keys-check
@@ -0,0 +1,42 @@
+#!/usr/bin/env ruby
+
+#
+# GitLab shell authorized_keys helper. Query GitLab API to get the authorized
+# command for a given ssh key fingerprint
+#
+# Ex.
+# bin/gitlab-shell-authorized-keys-check <username> <public-key>
+#
+# Returns
+# command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA...
+#
+# Expects to be called by the SSH daemon, via configuration like:
+# AuthorizedKeysCommandUser git
+# AuthorizedKeysCommand /bin/gitlab-shell-authorized-keys-check git %u %k
+
+abort "# Wrong number of arguments. #{ARGV.size}. Usage:
+# gitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>" unless ARGV.size == 3
+
+expected_username = ARGV[0]
+abort '# No username provided' if expected_username.nil? || expected_username == ''
+
+actual_username = ARGV[1]
+abort '# No username provided' if actual_username.nil? || actual_username == ''
+
+# Only check access if the requested username matches the configured username.
+# Normally, these would both be 'git', but it can be configured by the user
+exit 0 unless expected_username == actual_username
+
+key = ARGV[2]
+abort "# No key provided" if key.nil? || key == ''
+
+require_relative '../lib/gitlab_init'
+require_relative '../lib/gitlab_net'
+require_relative '../lib/gitlab_keys'
+
+authorized_key = GitlabNet.new.authorized_key(key)
+if authorized_key.nil?
+ puts "# No key was found for #{key}"
+else
+ puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key'])
+end
diff --git a/spec/gitlab_shell_authorized_keys_check_spec.rb b/spec/gitlab_shell_authorized_keys_check_spec.rb
new file mode 100644
index 0000000..166e675
--- /dev/null
+++ b/spec/gitlab_shell_authorized_keys_check_spec.rb
@@ -0,0 +1,106 @@
+require_relative 'spec_helper'
+
+describe 'bin/gitlab-shell-authorized-keys-check' do
+ def config_path
+ File.join(ROOT_PATH, 'config.yml')
+ end
+
+ def tmp_config_path
+ config_path + ".#{$$}"
+ end
+
+ def tmp_socket_path
+ File.join(ROOT_PATH, 'tmp', 'gitlab-shell-authorized-keys-check-socket')
+ end
+
+ before(:all) do
+ FileUtils.mkdir_p(File.dirname(tmp_socket_path))
+ FileUtils.touch(File.join(ROOT_PATH, '.gitlab_shell_secret'))
+
+ @server = HTTPUNIXServer.new(BindAddress: tmp_socket_path)
+ @server.mount_proc('/api/v4/internal/authorized_keys') do |req, res|
+ if req.query['key'] == 'known-rsa-key'
+ res.status = 200
+ res.content_type = 'application/json'
+ res.body = '{"key":"known-rsa-key", "id": 1}'
+ else
+ res.status = 404
+ end
+ end
+
+ @webrick_thread = Thread.new { @server.start }
+
+ sleep(0.1) while @webrick_thread.alive? && @server.status != :Running
+ raise "Couldn't start stub GitlabNet server" unless @server.status == :Running
+
+ FileUtils.mv(config_path, tmp_config_path) if File.exist?(config_path)
+ File.open(config_path, 'w') do |f|
+ f.write("---\ngitlab_url: http+unix://#{CGI.escape(tmp_socket_path)}\n")
+ end
+ end
+
+ after(:all) do
+ @server.shutdown if @server
+ @webrick_thread.join if @webrick_thread
+ FileUtils.rm_f(config_path)
+ FileUtils.mv(tmp_config_path, config_path) if File.exist?(tmp_config_path)
+ end
+
+ let(:gitlab_shell_path) { File.join(ROOT_PATH, 'bin', 'gitlab-shell') }
+ let(:authorized_keys_check_path) { File.join(ROOT_PATH, 'bin', 'gitlab-shell-authorized-keys-check') }
+
+ it 'succeeds when a valid key is given' do
+ output, status = run!
+
+ expect(output).to eq("command=\"#{gitlab_shell_path} key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty known-rsa-key\n")
+ expect(status).to be_success
+ end
+
+ it 'returns nothing when an unknown key is given' do
+ output, status = run!(key: 'unknown-key')
+
+ expect(output).to eq("# No key was found for unknown-key\n")
+ expect(status).to be_success
+ end
+
+ it' fails when not enough arguments are given' do
+ output, status = run!(key: nil)
+
+ expect(output).to eq('')
+ expect(status).not_to be_success
+ end
+
+ it' fails when too many arguments are given' do
+ output, status = run!(key: ['a', 'b'])
+
+ expect(output).to eq('')
+ expect(status).not_to be_success
+ end
+
+ it 'skips when run as the wrong user' do
+ output, status = run!(expected_user: 'unknown-user')
+
+ expect(output).to eq('')
+ expect(status).to be_success
+ end
+
+ it 'skips when the wrong users connects' do
+ output, status = run!(actual_user: 'unknown-user')
+
+ expect(output).to eq('')
+ expect(status).to be_success
+ end
+
+ def run!(expected_user: 'git', actual_user: 'git', key: 'known-rsa-key')
+ cmd = [
+ authorized_keys_check_path,
+ expected_user,
+ actual_user,
+ key
+ ].flatten.compact
+
+ output = IO.popen(cmd, &:read)
+
+ [output, $?]
+ end
+end
diff --git a/spec/httpunix_spec.rb b/spec/httpunix_spec.rb
index 77235e8..4c59340 100644
--- a/spec/httpunix_spec.rb
+++ b/spec/httpunix_spec.rb
@@ -1,6 +1,5 @@
require_relative 'spec_helper'
require_relative '../lib/httpunix'
-require 'webrick'
describe URI::HTTPUNIX do
describe :parse do
@@ -14,47 +13,29 @@ describe URI::HTTPUNIX do
end
end
-
-# like WEBrick::HTTPServer, but listens on UNIX socket
-class HTTPUNIXServer < WEBrick::HTTPServer
- def listen(address, port)
- socket = Socket.unix_server_socket(address)
- socket.autoclose = false
- server = UNIXServer.for_fd(socket.fileno)
- socket.close
- @listeners << server
+describe Net::HTTPUNIX do
+ def tmp_socket_path
+ File.join(ROOT_PATH, 'tmp/test-socket')
end
- # Workaround:
- # https://bugs.ruby-lang.org/issues/10956
- # Affecting Ruby 2.2
- # Fix for 2.2 is at https://github.com/ruby/ruby/commit/ab0a64e1
- # However, this doesn't work with 2.1. The following should work for both:
- def start(&block)
- @shutdown_pipe = IO.pipe
- shutdown_pipe = @shutdown_pipe
- super(&block)
- end
+ before(:all) do
+ # "hello world" over unix socket server in background thread
+ FileUtils.mkdir_p(File.dirname(tmp_socket_path))
+ @server = HTTPUNIXServer.new(BindAddress: tmp_socket_path)
+ @server.mount_proc '/' do |req, resp|
+ resp.body = "Hello World (at #{req.path})"
+ end
- def cleanup_shutdown_pipe(shutdown_pipe)
- @shutdown_pipe = nil
- return if !shutdown_pipe
- super(shutdown_pipe)
- end
-end
+ @webrick_thread = Thread.new { @server.start }
-def tmp_socket_path
- 'tmp/test-socket'
-end
+ sleep(0.1) while @webrick_thread.alive? && @server.status != :Running
+ raise "Couldn't start HTTPUNIXServer" unless @server.status == :Running
+ end
-describe Net::HTTPUNIX do
- # "hello world" over unix socket server in background thread
- FileUtils.mkdir_p(File.dirname(tmp_socket_path))
- server = HTTPUNIXServer.new(:BindAddress => tmp_socket_path)
- server.mount_proc '/' do |req, resp|
- resp.body = "Hello World (at #{req.path})"
+ after(:all) do
+ @server.shutdown if @server
+ @webrick_thread.join if @webrick_thread
end
- Thread.start { server.start }
it "talks via HTTP ok" do
VCR.turned_off do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 612af76..aeebd98 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -10,9 +10,38 @@ end
require 'vcr'
require 'webmock'
+require 'webrick'
VCR.configure do |c|
c.cassette_library_dir = 'spec/vcr_cassettes'
c.hook_into :webmock
c.configure_rspec_metadata!
end
+
+# like WEBrick::HTTPServer, but listens on UNIX socket
+class HTTPUNIXServer < WEBrick::HTTPServer
+ def listen(address, port)
+ socket = Socket.unix_server_socket(address)
+ socket.autoclose = false
+ server = UNIXServer.for_fd(socket.fileno)
+ socket.close
+ @listeners << server
+ end
+
+ # Workaround:
+ # https://bugs.ruby-lang.org/issues/10956
+ # Affecting Ruby 2.2
+ # Fix for 2.2 is at https://github.com/ruby/ruby/commit/ab0a64e1
+ # However, this doesn't work with 2.1. The following should work for both:
+ def start(&block)
+ @shutdown_pipe = IO.pipe
+ shutdown_pipe = @shutdown_pipe
+ super(&block)
+ end
+
+ def cleanup_shutdown_pipe(shutdown_pipe)
+ @shutdown_pipe = nil
+ return if !shutdown_pipe
+ super(shutdown_pipe)
+ end
+end