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
|
require 'shellwords'
require 'pathname'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
require_relative 'actor'
class GitlabShell
API_2FA_RECOVERY_CODES_COMMAND = '2fa_recovery_codes'.freeze
GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack'.freeze
GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'.freeze
GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'.freeze
GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate'.freeze
GIT_COMMANDS = [GIT_UPLOAD_PACK_COMMAND, GIT_RECEIVE_PACK_COMMAND,
GIT_UPLOAD_ARCHIVE_COMMAND, GIT_LFS_AUTHENTICATE_COMMAND].freeze
Struct.new('ParsedCommand', :command, :git_access_command, :repo_name, :args)
def initialize(who)
@config = GitlabConfig.new
@actor = Actor.new_from(who, audit_usernames: @config.audit_usernames)
end
# The origin_cmd variable contains UNTRUSTED input. If the user ran
# ssh git@gitlab.example.com 'evil command', then origin_cmd contains
# 'evil command'.
def exec(origin_cmd)
if !origin_cmd || origin_cmd.empty?
puts "Welcome to GitLab, #{actor.username}!"
return true
end
parsed_command = parse_cmd(origin_cmd)
action = determine_action(parsed_command)
action.execute(parsed_command.command, parsed_command.args)
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
false
rescue AccessDeniedError, UnknownError => ex
$logger.warn('Access denied', command: origin_cmd, user: actor.log_username)
$stderr.puts "GitLab: #{ex.message}"
false
rescue DisallowedCommandError
$logger.warn('Denied disallowed command', command: origin_cmd, user: actor.log_username)
$stderr.puts 'GitLab: Disallowed command'
false
rescue InvalidRepositoryPathError
$stderr.puts 'GitLab: Invalid repository path'
false
end
private
attr_reader :config, :actor
def parse_cmd(cmd)
args = Shellwords.shellwords(cmd)
# Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
if args.length == 3 && args.first == 'git'
command = "git-#{args[1]}"
args = [command, args.last]
else
command = args.first
end
git_access_command = command
if command == API_2FA_RECOVERY_CODES_COMMAND
return Struct::ParsedCommand.new(command, git_access_command, nil, args)
end
raise DisallowedCommandError unless GIT_COMMANDS.include?(command)
case command
when 'git-lfs-authenticate'
raise DisallowedCommandError unless args.count >= 2
repo_name = args[1]
git_access_command = case args[2]
when 'download'
GIT_UPLOAD_PACK_COMMAND
when 'upload'
GIT_RECEIVE_PACK_COMMAND
else
raise DisallowedCommandError
end
else
raise DisallowedCommandError unless args.count == 2
repo_name = args.last
end
Struct::ParsedCommand.new(command, git_access_command, repo_name, args)
end
def determine_action(parsed_command)
return Action::API2FARecovery.new(actor) if parsed_command.command == API_2FA_RECOVERY_CODES_COMMAND
GitlabMetrics.measure('verify-access') do
# GitlabNet#check_access will raise exception in the event of a problem
initial_action = api.check_access(
parsed_command.git_access_command,
nil,
parsed_command.repo_name,
actor,
'_any'
)
case parsed_command.command
when GIT_LFS_AUTHENTICATE_COMMAND
Action::GitLFSAuthenticate.new(actor, parsed_command.repo_name)
else
initial_action
end
end
end
def api
@api ||= GitlabNet.new
end
end
|