summaryrefslogtreecommitdiff
path: root/lib/gitlab_shell.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab_shell.rb')
-rw-r--r--lib/gitlab_shell.rb277
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