diff options
author | Ash McKenzie <amckenzie@gitlab.com> | 2018-07-26 19:06:20 +1000 |
---|---|---|
committer | Ash McKenzie <amckenzie@gitlab.com> | 2018-08-01 00:24:16 +1000 |
commit | ecba183b60eb0798ecf1736659ed7d0a995d0f01 (patch) | |
tree | 3606b2d25e914f69f6cc9aab6de7864331b49dbf /lib/gitlab_shell.rb | |
parent | 252a96d61f52d91486f92f980c2f0e4c7a1b9408 (diff) | |
download | gitlab-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.rb | 257 |
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 |