summaryrefslogtreecommitdiff
path: root/lib/gitlab_shell.rb
diff options
context:
space:
mode:
authorAsh McKenzie <amckenzie@gitlab.com>2018-07-26 19:06:20 +1000
committerAsh McKenzie <amckenzie@gitlab.com>2018-08-01 00:24:16 +1000
commitecba183b60eb0798ecf1736659ed7d0a995d0f01 (patch)
tree3606b2d25e914f69f6cc9aab6de7864331b49dbf /lib/gitlab_shell.rb
parent252a96d61f52d91486f92f980c2f0e4c7a1b9408 (diff)
downloadgitlab-shell-ecba183b60eb0798ecf1736659ed7d0a995d0f01.tar.gz
Utilise new Actions
* Move gitaly, git-lfs and 2FA logic out from gitlab_shell.rb * Streamline parsing of origin_cmd in GitlabShell * Utilise proper HTTP status codes sent from the API * Also support 200 OK with status of true/false (ideally get rid of this) * Use HTTP status constants * Use attr_reader definitions (var over @var) * Rspec deprecation fixes
Diffstat (limited to 'lib/gitlab_shell.rb')
-rw-r--r--lib/gitlab_shell.rb257
1 files changed, 47 insertions, 210 deletions
diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb
index aaa0570..5c51bc1 100644
--- a/lib/gitlab_shell.rb
+++ b/lib/gitlab_shell.rb
@@ -3,18 +3,11 @@ require 'pathname'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
+require_relative 'user'
-class GitlabShell # rubocop:disable Metrics/ClassLength
+class GitlabShell
+ API_2FA_RECOVERY_CODES_COMMAND = '2fa_recovery_codes'.freeze
- GITALY_MIGRATED_COMMANDS = {
- 'git-upload-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'),
- 'git-upload-archive' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'),
- 'git-receive-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack')
- }.freeze
- API_COMMANDS = %w(2fa_recovery_codes).freeze
-
- attr_accessor :key_id, :gl_repository, :repo_name, :command, :git_access
- attr_reader :repo_path
GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack'.freeze
GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'.freeze
GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'.freeze
@@ -33,29 +26,23 @@ class GitlabShell # rubocop:disable Metrics/ClassLength
# 'evil command'.
def exec(origin_cmd)
if !origin_cmd || origin_cmd.empty?
- puts "Welcome to GitLab, #{username}!"
+ puts "Welcome to GitLab, #{user.username}!"
return true
end
- args = Shellwords.shellwords(origin_cmd)
- args = parse_cmd(args)
-
- if GIT_COMMANDS.include?(args.first)
- GitlabMetrics.measure('verify-access') { verify_access }
- end
+ command, git_access_command, repo_name, args = parse_cmd(origin_cmd)
+ action = determine_action(command, git_access_command, repo_name)
- process_cmd(args)
-
- true
+ action.execute(command, args)
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
false
- rescue AccessDeniedError => ex
- $logger.warn('Access denied', command: origin_cmd, user: log_username)
+ rescue AccessDeniedError, UnknownError => ex
+ $logger.warn('Access denied', command: origin_cmd, user: user.log_username)
$stderr.puts "GitLab: #{ex.message}"
false
rescue DisallowedCommandError
- $logger.warn('Denied disallowed command', command: origin_cmd, user: log_username)
+ $logger.warn('Denied disallowed command', command: origin_cmd, user: user.log_username)
$stderr.puts 'GitLab: Disallowed command'
false
rescue InvalidRepositoryPathError
@@ -65,217 +52,67 @@ class GitlabShell # rubocop:disable Metrics/ClassLength
private
+ attr_reader :config, :key_id
+
+ def user
+ @user ||= User.new(key_id, audit_usernames: config.audit_usernames)
+ end
+
+ def parse_cmd(cmd)
+ args = Shellwords.shellwords(cmd)
- def parse_cmd(args)
# 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]
+ command = "git-#{args[1]}"
+ args = [command, args.last]
else
- @command = args.first
+ command = args.first
end
- @git_access = @command
+ git_access_command = command
- return args if API_COMMANDS.include?(@command)
+ return [command, git_access_command, nil, args] if command == API_2FA_RECOVERY_CODES_COMMAND
- raise DisallowedCommandError unless GIT_COMMANDS.include?(@command)
+ raise DisallowedCommandError unless GIT_COMMANDS.include?(command)
- case @command
+ case command
when 'git-lfs-authenticate'
raise DisallowedCommandError unless args.count >= 2
- @repo_name = args[1]
- case args[2]
- when 'download'
- @git_access = 'git-upload-pack'
- when 'upload'
- @git_access = 'git-receive-pack'
- else
- raise DisallowedCommandError
- end
+ 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
+ repo_name = args.last
end
- args
+ [command, git_access_command, repo_name, args]
end
- def verify_access
- status = api.check_access(@git_access, nil, @repo_name, @key_id, '_any', GitlabNet::GL_PROTOCOL)
-
- raise AccessDeniedError, status.message unless status.allowed?
-
- self.repo_path = status.repository_path
- @gl_repository = status.gl_repository
- @gitaly = status.gitaly
- @username = status.gl_username
- end
+ def determine_action(command, git_access_command, repo_name)
+ return Action::API2FARecovery.new(key_id) if command == API_2FA_RECOVERY_CODES_COMMAND
- def process_cmd(args)
- return send("api_#{@command}") if API_COMMANDS.include?(@command)
+ GitlabMetrics.measure('verify-access') do
+ # GitlatNet#check_access will raise exception in the event of a problem
+ initial_action = api.check_access(git_access_command, nil,
+ repo_name, key_id, '_any')
- if @command == 'git-lfs-authenticate'
- GitlabMetrics.measure('lfs-authenticate') do
- $logger.info('Processing LFS authentication', user: log_username)
- lfs_authenticate
+ case command
+ when GIT_LFS_AUTHENTICATE_COMMAND
+ Action::GitLFSAuthenticate.new(key_id, repo_name)
+ else
+ initial_action
end
- return
end
-
- executable = @command
- args = [repo_path]
-
- if GITALY_MIGRATED_COMMANDS.key?(executable) && @gitaly
- executable = GITALY_MIGRATED_COMMANDS[executable]
-
- gitaly_address = @gitaly['address']
-
- # The entire gitaly_request hash should be built in gitlab-ce and passed
- # on as-is. For now we build a fake one on the spot.
- gitaly_request = {
- 'repository' => @gitaly['repository'],
- 'gl_repository' => @gl_repository,
- 'gl_id' => @key_id,
- 'gl_username' => @username
- }
-
- args = [gitaly_address, JSON.dump(gitaly_request)]
- end
-
- args_string = [File.basename(executable), *args].join(' ')
- $logger.info('executing git command', command: args_string, user: log_username)
- exec_cmd(executable, *args)
- end
-
- # This method is not covered by Rspec because it ends the current Ruby process.
- def exec_cmd(*args)
- # If you want to call a command without arguments, use
- # exec_cmd(['my_command', 'my_command']) . Otherwise use
- # exec_cmd('my_command', 'my_argument', ...).
- if args.count == 1 && !args.first.is_a?(Array)
- raise DisallowedCommandError
- end
-
- env = {
- 'HOME' => ENV['HOME'],
- 'PATH' => ENV['PATH'],
- 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
- 'LANG' => ENV['LANG'],
- 'GL_ID' => @key_id,
- 'GL_PROTOCOL' => GitlabNet::GL_PROTOCOL,
- 'GL_REPOSITORY' => @gl_repository,
- 'GL_USERNAME' => @username
- }
- if @gitaly && @gitaly.include?('token')
- env['GITALY_TOKEN'] = @gitaly['token']
- end
-
- if git_trace_available?
- env.merge!(
- 'GIT_TRACE' => @config.git_trace_log_file,
- 'GIT_TRACE_PACKET' => @config.git_trace_log_file,
- 'GIT_TRACE_PERFORMANCE' => @config.git_trace_log_file
- )
- end
-
- # We use 'chdir: ROOT_PATH' to let the next executable know where config.yml is.
- Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH)
end
def api
GitlabNet.new
end
-
- def user
- return @user if defined?(@user)
-
- begin
- @user = api.discover(@key_id)
- rescue GitlabNet::ApiUnreachableError
- @user = nil
- end
- end
-
- def username_from_discover
- return nil unless user && user['username']
-
- "@#{user['username']}"
- end
-
- def username
- @username ||= username_from_discover || 'Anonymous'
- end
-
- # User identifier to be used in log messages.
- def log_username
- @config.audit_usernames ? username : "user with key #{@key_id}"
- end
-
- def lfs_authenticate
- lfs_access = api.lfs_authenticate(@key_id, @repo_name)
-
- return unless lfs_access
-
- puts lfs_access.authentication_payload
- end
-
- private
-
- def continue?(question)
- puts "#{question} (yes/no)"
- STDOUT.flush # Make sure the question gets output before we wait for input
- continue = STDIN.gets.chomp
- puts '' # Add a buffer in the output
- continue == 'yes'
- end
-
- def api_2fa_recovery_codes
- continue = continue?(
- "Are you sure you want to generate new two-factor recovery codes?\n" \
- "Any existing recovery codes you saved will be invalidated."
- )
-
- unless continue
- puts 'New recovery codes have *not* been generated. Existing codes will remain valid.'
- return
- end
-
- resp = api.two_factor_recovery_codes(key_id)
- if resp['success']
- codes = resp['recovery_codes'].join("\n")
- puts "Your two-factor authentication recovery codes are:\n\n" \
- "#{codes}\n\n" \
- "During sign in, use one of the codes above when prompted for\n" \
- "your two-factor code. Then, visit your Profile Settings and add\n" \
- "a new device so you do not lose access to your account again."
- else
- puts "An error occurred while trying to generate new recovery codes.\n" \
- "#{resp['message']}"
- end
- end
-
- def git_trace_available?
- return false unless @config.git_trace_log_file
-
- if Pathname(@config.git_trace_log_file).relative?
- $logger.warn('git trace log path must be absolute, ignoring', git_trace_log_file: @config.git_trace_log_file)
- return false
- end
-
- begin
- File.open(@config.git_trace_log_file, 'a') { nil }
- return true
- rescue => ex
- $logger.warn('Failed to open git trace log file', git_trace_log_file: @config.git_trace_log_file, error: ex.to_s)
- return false
- end
- end
-
- def repo_path=(repo_path)
- raise ArgumentError, "Repository path not provided. Please make sure you're using GitLab v8.10 or later." unless repo_path
- raise InvalidRepositoryPathError if File.absolute_path(repo_path) != repo_path
-
- @repo_path = repo_path
- end
end