1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
# frozen_string_literal: true
module Gitlab
class AuthorizedKeys
KeyError = Class.new(StandardError)
attr_reader :logger
# Initializes the class
#
# @param [Gitlab::Logger] logger
def initialize(logger = Gitlab::AppLogger)
@logger = logger
end
# Add id and its key to the authorized_keys file
#
# @param [String] id identifier of key prefixed by `key-`
# @param [String] key public key to be added
# @return [Boolean]
def add_key(id, key)
lock do
public_key = strip(key)
logger.info("Adding key (#{id}): #{public_key}")
open_authorized_keys_file('a') { |file| file.puts(key_line(id, public_key)) }
end
true
end
# Atomically add all the keys to the authorized_keys file
#
# @param [Array<::Key>] keys list of Key objects to be added
# @return [Boolean]
def batch_add_keys(keys)
lock(300) do # Allow 300 seconds (5 minutes) for batch_add_keys
open_authorized_keys_file('a') do |file|
keys.each do |key|
public_key = strip(key.key)
logger.info("Adding key (#{key.shell_id}): #{public_key}")
file.puts(key_line(key.shell_id, public_key))
end
end
end
true
rescue Gitlab::AuthorizedKeys::KeyError
false
end
# Remove key by ID from the authorized_keys file
#
# @param [String] id identifier of the key to be removed prefixed by `key-`
# @return [Boolean]
def rm_key(id)
lock do
logger.info("Removing key (#{id})")
open_authorized_keys_file('r+') do |f|
while line = f.gets
next unless line.start_with?("command=\"#{command(id)}\"")
f.seek(-line.length, IO::SEEK_CUR)
# Overwrite the line with #'s. Because the 'line' variable contains
# a terminating '\n', we write line.length - 1 '#' characters.
f.write('#' * (line.length - 1))
end
end
end
true
rescue Errno::ENOENT
false
end
# Clear the authorized_keys file
#
# @return [Boolean]
def clear
open_authorized_keys_file('w') { |file| file.puts '# Managed by gitlab-rails' }
true
end
# Read the authorized_keys file and return IDs of each key
#
# @return [Array<Integer>]
def list_key_ids
logger.info('Listing all key IDs')
[].tap do |a|
open_authorized_keys_file('r') do |f|
f.each_line do |line|
key_id = line.match(/key-(\d+)/)
next unless key_id
a << key_id[1].chomp.to_i
end
end
end
rescue Errno::ENOENT
[]
end
private
def lock(timeout = 10)
File.open("#{authorized_keys_file}.lock", "w+") do |f|
f.flock File::LOCK_EX
Timeout.timeout(timeout) { yield }
ensure
f.flock File::LOCK_UN
end
end
def open_authorized_keys_file(mode)
File.open(authorized_keys_file, mode, 0o600) do |file|
file.chmod(0o600)
yield file
end
end
def key_line(id, key)
key = key.chomp
if key.include?("\n") || key.include?("\t")
raise KeyError, "Invalid public_key: #{key.inspect}"
end
%Q(command="#{command(id)}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{strip(key)})
end
def command(id)
unless /\A[a-z0-9-]+\z/ =~ id
raise KeyError, "Invalid ID: #{id.inspect}"
end
"#{File.join(Gitlab.config.gitlab_shell.path, 'bin', 'gitlab-shell')} #{id}"
end
def strip(key)
key.split(/[ ]+/)[0, 2].join(' ')
end
def authorized_keys_file
Gitlab.config.gitlab_shell.authorized_keys_file
end
end
end
|