Skip to content

Commit f8f3148

Browse files
author
Douwe Maan
committed
Merge branch '118-database-authorized-keys' into 'master'
Introduce a more-complete implementation of bin/authorized_keys Closes #118 See merge request gitlab-org/gitlab-shell!178
2 parents 5cad42a + d40383f commit f8f3148

File tree

7 files changed

+201
-40
lines changed

7 files changed

+201
-40
lines changed

.gitlab-ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ before_script:
1010

1111
rspec:
1212
script:
13-
- bundle exec rspec spec
13+
- bundle exec rspec -f d spec
1414
tags:
1515
- ruby
1616
except:
@@ -28,7 +28,7 @@ rubocop:
2828
rspec:ruby2.2:
2929
image: ruby:2.2
3030
script:
31-
- bundle exec rspec spec
31+
- bundle exec rspec -f d spec
3232
tags:
3333
- ruby
3434
except:
@@ -38,7 +38,7 @@ rspec:ruby2.2:
3838
rspec:ruby2.1:
3939
image: ruby:2.1
4040
script:
41-
- bundle exec rspec spec
41+
- bundle exec rspec -f d spec
4242
tags:
4343
- ruby
4444
except:

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
v5.11.0
2+
- Introduce a more-complete implementation of bin/authorized_keys (!178)
3+
14
v5.10.3
25
- Remove unused redis bin configuration
36

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.10.3
1+
5.11.0
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env ruby
2+
3+
#
4+
# GitLab shell authorized_keys helper. Query GitLab API to get the authorized
5+
# command for a given ssh key fingerprint
6+
#
7+
# Ex.
8+
# bin/gitlab-shell-authorized-keys-check <username> <public-key>
9+
#
10+
# Returns
11+
# command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA...
12+
#
13+
# Expects to be called by the SSH daemon, via configuration like:
14+
# AuthorizedKeysCommandUser git
15+
# AuthorizedKeysCommand /bin/gitlab-shell-authorized-keys-check git %u %k
16+
17+
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
18+
# gitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>" unless ARGV.size == 3
19+
20+
expected_username = ARGV[0]
21+
abort '# No username provided' if expected_username.nil? || expected_username == ''
22+
23+
actual_username = ARGV[1]
24+
abort '# No username provided' if actual_username.nil? || actual_username == ''
25+
26+
# Only check access if the requested username matches the configured username.
27+
# Normally, these would both be 'git', but it can be configured by the user
28+
exit 0 unless expected_username == actual_username
29+
30+
key = ARGV[2]
31+
abort "# No key provided" if key.nil? || key == ''
32+
33+
require_relative '../lib/gitlab_init'
34+
require_relative '../lib/gitlab_net'
35+
require_relative '../lib/gitlab_keys'
36+
37+
authorized_key = GitlabNet.new.authorized_key(key)
38+
if authorized_key.nil?
39+
puts "# No key was found for #{key}"
40+
else
41+
puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key'])
42+
end
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
require_relative 'spec_helper'
2+
3+
describe 'bin/gitlab-shell-authorized-keys-check' do
4+
def config_path
5+
File.join(ROOT_PATH, 'config.yml')
6+
end
7+
8+
def tmp_config_path
9+
config_path + ".#{$$}"
10+
end
11+
12+
def tmp_socket_path
13+
File.join(ROOT_PATH, 'tmp', 'gitlab-shell-authorized-keys-check-socket')
14+
end
15+
16+
before(:all) do
17+
FileUtils.mkdir_p(File.dirname(tmp_socket_path))
18+
FileUtils.touch(File.join(ROOT_PATH, '.gitlab_shell_secret'))
19+
20+
@server = HTTPUNIXServer.new(BindAddress: tmp_socket_path)
21+
@server.mount_proc('/api/v4/internal/authorized_keys') do |req, res|
22+
if req.query['key'] == 'known-rsa-key'
23+
res.status = 200
24+
res.content_type = 'application/json'
25+
res.body = '{"key":"known-rsa-key", "id": 1}'
26+
else
27+
res.status = 404
28+
end
29+
end
30+
31+
@webrick_thread = Thread.new { @server.start }
32+
33+
sleep(0.1) while @webrick_thread.alive? && @server.status != :Running
34+
raise "Couldn't start stub GitlabNet server" unless @server.status == :Running
35+
36+
FileUtils.mv(config_path, tmp_config_path) if File.exist?(config_path)
37+
File.open(config_path, 'w') do |f|
38+
f.write("---\ngitlab_url: http+unix://#{CGI.escape(tmp_socket_path)}\n")
39+
end
40+
end
41+
42+
after(:all) do
43+
@server.shutdown if @server
44+
@webrick_thread.join if @webrick_thread
45+
FileUtils.rm_f(config_path)
46+
FileUtils.mv(tmp_config_path, config_path) if File.exist?(tmp_config_path)
47+
end
48+
49+
let(:gitlab_shell_path) { File.join(ROOT_PATH, 'bin', 'gitlab-shell') }
50+
let(:authorized_keys_check_path) { File.join(ROOT_PATH, 'bin', 'gitlab-shell-authorized-keys-check') }
51+
52+
it 'succeeds when a valid key is given' do
53+
output, status = run!
54+
55+
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")
56+
expect(status).to be_success
57+
end
58+
59+
it 'returns nothing when an unknown key is given' do
60+
output, status = run!(key: 'unknown-key')
61+
62+
expect(output).to eq("# No key was found for unknown-key\n")
63+
expect(status).to be_success
64+
end
65+
66+
it' fails when not enough arguments are given' do
67+
output, status = run!(key: nil)
68+
69+
expect(output).to eq('')
70+
expect(status).not_to be_success
71+
end
72+
73+
it' fails when too many arguments are given' do
74+
output, status = run!(key: ['a', 'b'])
75+
76+
expect(output).to eq('')
77+
expect(status).not_to be_success
78+
end
79+
80+
it 'skips when run as the wrong user' do
81+
output, status = run!(expected_user: 'unknown-user')
82+
83+
expect(output).to eq('')
84+
expect(status).to be_success
85+
end
86+
87+
it 'skips when the wrong users connects' do
88+
output, status = run!(actual_user: 'unknown-user')
89+
90+
expect(output).to eq('')
91+
expect(status).to be_success
92+
end
93+
94+
def run!(expected_user: 'git', actual_user: 'git', key: 'known-rsa-key')
95+
cmd = [
96+
authorized_keys_check_path,
97+
expected_user,
98+
actual_user,
99+
key
100+
].flatten.compact
101+
102+
output = IO.popen(cmd, &:read)
103+
104+
[output, $?]
105+
end
106+
end

spec/httpunix_spec.rb

Lines changed: 17 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
require_relative 'spec_helper'
22
require_relative '../lib/httpunix'
3-
require 'webrick'
43

54
describe URI::HTTPUNIX do
65
describe :parse do
@@ -14,47 +13,29 @@
1413
end
1514
end
1615

17-
18-
# like WEBrick::HTTPServer, but listens on UNIX socket
19-
class HTTPUNIXServer < WEBrick::HTTPServer
20-
def listen(address, port)
21-
socket = Socket.unix_server_socket(address)
22-
socket.autoclose = false
23-
server = UNIXServer.for_fd(socket.fileno)
24-
socket.close
25-
@listeners << server
16+
describe Net::HTTPUNIX do
17+
def tmp_socket_path
18+
File.join(ROOT_PATH, 'tmp/test-socket')
2619
end
2720

28-
# Workaround:
29-
# https://bugs.ruby-lang.org/issues/10956
30-
# Affecting Ruby 2.2
31-
# Fix for 2.2 is at https://github.com/ruby/ruby/commit/ab0a64e1
32-
# However, this doesn't work with 2.1. The following should work for both:
33-
def start(&block)
34-
@shutdown_pipe = IO.pipe
35-
shutdown_pipe = @shutdown_pipe
36-
super(&block)
37-
end
21+
before(:all) do
22+
# "hello world" over unix socket server in background thread
23+
FileUtils.mkdir_p(File.dirname(tmp_socket_path))
24+
@server = HTTPUNIXServer.new(BindAddress: tmp_socket_path)
25+
@server.mount_proc '/' do |req, resp|
26+
resp.body = "Hello World (at #{req.path})"
27+
end
3828

39-
def cleanup_shutdown_pipe(shutdown_pipe)
40-
@shutdown_pipe = nil
41-
return if !shutdown_pipe
42-
super(shutdown_pipe)
43-
end
44-
end
29+
@webrick_thread = Thread.new { @server.start }
4530

46-
def tmp_socket_path
47-
'tmp/test-socket'
48-
end
31+
sleep(0.1) while @webrick_thread.alive? && @server.status != :Running
32+
raise "Couldn't start HTTPUNIXServer" unless @server.status == :Running
33+
end
4934

50-
describe Net::HTTPUNIX do
51-
# "hello world" over unix socket server in background thread
52-
FileUtils.mkdir_p(File.dirname(tmp_socket_path))
53-
server = HTTPUNIXServer.new(:BindAddress => tmp_socket_path)
54-
server.mount_proc '/' do |req, resp|
55-
resp.body = "Hello World (at #{req.path})"
35+
after(:all) do
36+
@server.shutdown if @server
37+
@webrick_thread.join if @webrick_thread
5638
end
57-
Thread.start { server.start }
5839

5940
it "talks via HTTP ok" do
6041
VCR.turned_off do

spec/spec_helper.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,38 @@
1010

1111
require 'vcr'
1212
require 'webmock'
13+
require 'webrick'
1314

1415
VCR.configure do |c|
1516
c.cassette_library_dir = 'spec/vcr_cassettes'
1617
c.hook_into :webmock
1718
c.configure_rspec_metadata!
1819
end
20+
21+
# like WEBrick::HTTPServer, but listens on UNIX socket
22+
class HTTPUNIXServer < WEBrick::HTTPServer
23+
def listen(address, port)
24+
socket = Socket.unix_server_socket(address)
25+
socket.autoclose = false
26+
server = UNIXServer.for_fd(socket.fileno)
27+
socket.close
28+
@listeners << server
29+
end
30+
31+
# Workaround:
32+
# https://bugs.ruby-lang.org/issues/10956
33+
# Affecting Ruby 2.2
34+
# Fix for 2.2 is at https://github.com/ruby/ruby/commit/ab0a64e1
35+
# However, this doesn't work with 2.1. The following should work for both:
36+
def start(&block)
37+
@shutdown_pipe = IO.pipe
38+
shutdown_pipe = @shutdown_pipe
39+
super(&block)
40+
end
41+
42+
def cleanup_shutdown_pipe(shutdown_pipe)
43+
@shutdown_pipe = nil
44+
return if !shutdown_pipe
45+
super(shutdown_pipe)
46+
end
47+
end

0 commit comments

Comments
 (0)