diff options
Diffstat (limited to 'lib/gitlab_shell.rb')
-rw-r--r-- | lib/gitlab_shell.rb | 277 |
1 files changed, 0 insertions, 277 deletions
diff --git a/lib/gitlab_shell.rb b/lib/gitlab_shell.rb deleted file mode 100644 index 303f4d5..0000000 --- a/lib/gitlab_shell.rb +++ /dev/null @@ -1,277 +0,0 @@ -# frozen_string_literal: true - -require 'shellwords' -require 'pathname' - -require_relative 'gitlab_net' -require_relative 'gitlab_metrics' -require_relative 'action' -require_relative 'console_helper' - -class GitlabShell # rubocop:disable Metrics/ClassLength - include ConsoleHelper - - class AccessDeniedError < StandardError; end - class DisallowedCommandError < StandardError; end - class InvalidRepositoryPathError < StandardError; end - - GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack' - GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack' - GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive' - GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate' - - GITALY_COMMANDS = { - GIT_UPLOAD_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'), - GIT_UPLOAD_ARCHIVE_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'), - GIT_RECEIVE_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack') - }.freeze - - GIT_COMMANDS = (GITALY_COMMANDS.keys + [GIT_LFS_AUTHENTICATE_COMMAND]).freeze - TWO_FACTOR_RECOVERY_COMMAND = '2fa_recovery_codes' - GL_PROTOCOL = 'ssh' - - attr_accessor :gl_id, :gl_repository, :gl_project_path, :repo_name, :command, :git_access, :git_protocol - - def initialize(who) - who_sym, = GitlabNet.parse_who(who) - if who_sym == :username - @who = who - else - @gl_id = who - end - @config = GitlabConfig.new - 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) - unless origin_cmd - puts "Welcome to GitLab, #{username}!" - return true - end - - args = Shellwords.shellwords(origin_cmd) - args = parse_cmd(args) - - access_status = nil - - if GIT_COMMANDS.include?(args.first) - access_status = GitlabMetrics.measure('verify-access') { verify_access } - - @gl_repository = access_status.gl_repository - @git_protocol = ENV['GIT_PROTOCOL'] - @gl_project_path = access_status.gl_project_path - @gitaly = access_status.gitaly - @username = access_status.gl_username - @git_config_options = access_status.git_config_options - @gl_id = access_status.gl_id if defined?(@who) - - write_stderr(access_status.gl_console_messages) - elsif !defined?(@gl_id) - # We're processing an API command like 2fa_recovery_codes, but - # don't have a @gl_id yet, that means we're in the "username" - # mode and need to materialize it, calling the "user" method - # will do that and call the /discover method. - user - end - - if @command == GIT_RECEIVE_PACK_COMMAND && access_status.custom_action? - # If the response from /api/v4/allowed is a HTTP 300, we need to perform - # a Custom Action and therefore should return and not call process_cmd() - # - return process_custom_action(access_status) - end - - process_cmd(args) - - true - rescue GitlabNet::ApiUnreachableError - write_stderr('Failed to authorize your Git request: internal API unreachable') - false - rescue AccessDeniedError => ex - $logger.warn('Access denied', command: origin_cmd, user: log_username) - write_stderr(ex.message) - false - rescue DisallowedCommandError - $logger.warn('Denied disallowed command', command: origin_cmd, user: log_username) - write_stderr('Disallowed command') - false - rescue InvalidRepositoryPathError - write_stderr('Invalid repository path') - false - rescue Action::Custom::BaseError => ex - $logger.warn('Custom action error', exception: ex.class, message: ex.message, - command: origin_cmd, user: log_username) - $stderr.puts ex.message - false - end - - protected - - 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] - else - @command = args.first - end - - @git_access = @command - - return args if TWO_FACTOR_RECOVERY_COMMAND == @command - - raise DisallowedCommandError unless GIT_COMMANDS.include?(@command) - - case @command - when GIT_LFS_AUTHENTICATE_COMMAND - raise DisallowedCommandError unless args.count >= 2 - @repo_name = args[1] - case args[2] - when 'download' - @git_access = GIT_UPLOAD_PACK_COMMAND - when 'upload' - @git_access = GIT_RECEIVE_PACK_COMMAND - else - raise DisallowedCommandError - end - else - raise DisallowedCommandError unless args.count == 2 - @repo_name = args.last - end - - args - end - - def verify_access - status = api.check_access(@git_access, nil, @repo_name, @who || @gl_id, '_any', GL_PROTOCOL) - - raise AccessDeniedError, status.message unless status.allowed? - - status - end - - def process_custom_action(access_status) - Action::Custom.new(@gl_id, access_status.payload).execute - end - - def process_cmd(args) - return api_2fa_recovery_codes if TWO_FACTOR_RECOVERY_COMMAND == @command - - if @command == GIT_LFS_AUTHENTICATE_COMMAND - GitlabMetrics.measure('lfs-authenticate') do - operation = args[2] - $logger.info('Processing LFS authentication', operation: operation, user: log_username) - lfs_authenticate(operation) - end - return - end - - # TODO: instead of building from pieces here in gitlab-shell, build the - # entire gitaly_request in gitlab-ce and pass on as-is here. - args = JSON.dump( - 'repository' => @gitaly['repository'], - 'gl_repository' => @gl_repository, - 'gl_project_path' => @gl_project_path, - 'gl_id' => @gl_id, - 'gl_username' => @username, - 'git_config_options' => @git_config_options, - 'git_protocol' => @git_protocol - ) - - gitaly_address = @gitaly['address'] - executable = GITALY_COMMANDS.fetch(@command) - gitaly_bin = File.basename(executable) - args_string = [gitaly_bin, gitaly_address, args].join(' ') - $logger.info('executing git command', command: args_string, user: log_username) - - exec_cmd(executable, gitaly_address: gitaly_address, token: @gitaly['token'], json_args: args) - end - - # This method is not covered by Rspec because it ends the current Ruby process. - def exec_cmd(executable, gitaly_address:, token:, json_args:) - env = { 'GITALY_TOKEN' => token } - - args = [executable, gitaly_address, json_args] - # 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 - if defined?(@who) - @user = api.discover(@who) - @gl_id = "user-#{@user['id']}" if @user && @user.key?('id') - else - @user = api.discover(@gl_id) - end - 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 id #{@gl_id}" - end - - def lfs_authenticate(operation) - lfs_access = api.lfs_authenticate(@gl_id, @repo_name, operation) - - 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(@gl_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 -end |