require 'shellwords' require 'pathname' require_relative 'gitlab_net' require_relative 'gitlab_metrics' require_relative 'current_user_helper' require_relative 'api_command_helper' require_relative 'log_helper' class GitlabShell # rubocop:disable Metrics/ClassLength include CurrentUserHelper include APICommandHelper include LogHelper class AccessDeniedError < StandardError; end class DisallowedCommandError < StandardError; end class InvalidRepositoryPathError < StandardError; end GIT_COMMANDS = %w(git-upload-pack git-receive-pack git-upload-archive git-lfs-authenticate).freeze API_COMMANDS = %w(2fa_recovery_codes).freeze def initialize(key_id) @key_id = key_id @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) return send("api_#{command}") if API_COMMANDS.include?(command) action = GitlabMetrics.measure('verify-access') { verify_access } process_action(action, 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) $stderr.puts "GitLab: #{ex.message}" false rescue DisallowedCommandError $logger.warn('Denied disallowed command', command: origin_cmd, user: log_username) $stderr.puts "GitLab: Disallowed command" false rescue InvalidRepositoryPathError $stderr.puts "GitLab: Invalid repository path" false rescue Action::Custom::UnsuccessfulError $stderr.puts "GitLab: A custom action error has occurred" false end private attr_accessor :repo_name, :command, :git_access attr_reader :config, :key_id, :repo_path 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 API_COMMANDS.include?(command) raise DisallowedCommandError unless GIT_COMMANDS.include?(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 else raise DisallowedCommandError unless args.count == 2 @repo_name = args.last end args end def verify_access api.check_access(git_access, nil, repo_name, key_id, '_any') end def process_action(action, args) if command == 'git-lfs-authenticate' GitlabMetrics.measure('lfs-authenticate') do $logger.info('Processing LFS authentication', user: log_username) lfs_authenticate end return true end action.execute(command, args) end def api GitlabNet.new end def lfs_authenticate lfs_access = api.lfs_authenticate(key_id, repo_name) return unless lfs_access puts lfs_access.authentication_payload end end