summaryrefslogtreecommitdiff
path: root/lib/gitlab_shell.rb
blob: bd7b783920f7f0c1b04a87a9fd5bc91e7fa99ed8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
require 'shellwords'
require 'pathname'

require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
require_relative 'actor'

class GitlabShell
  API_2FA_RECOVERY_CODES_COMMAND = '2fa_recovery_codes'.freeze

  GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack'.freeze
  GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'.freeze
  GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'.freeze
  GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate'.freeze

  GIT_COMMANDS = [GIT_UPLOAD_PACK_COMMAND, GIT_RECEIVE_PACK_COMMAND,
                  GIT_UPLOAD_ARCHIVE_COMMAND, GIT_LFS_AUTHENTICATE_COMMAND].freeze

  Struct.new('ParsedCommand', :command, :git_access_command, :repo_name, :args)

  def initialize(who)
    @config = GitlabConfig.new
    @actor = Actor.new_from(who, audit_usernames: @config.audit_usernames)
  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)
    if !origin_cmd || origin_cmd.empty?
      puts "Welcome to GitLab, #{actor.username}!"
      return true
    end

    parsed_command = parse_cmd(origin_cmd)
    action = determine_action(parsed_command)
    action.execute(parsed_command.command, parsed_command.args)
  rescue GitlabNet::ApiUnreachableError
    $stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
    false
  rescue AccessDeniedError, UnknownError => ex
    $logger.warn('Access denied', command: origin_cmd, user: actor.log_username)
    $stderr.puts "GitLab: #{ex.message}"
    false
  rescue DisallowedCommandError
    $logger.warn('Denied disallowed command', command: origin_cmd, user: actor.log_username)
    $stderr.puts 'GitLab: Disallowed command'
    false
  rescue InvalidRepositoryPathError
    $stderr.puts 'GitLab: Invalid repository path'
    false
  end

  private

  attr_reader :config, :actor

  def parse_cmd(cmd)
    args = Shellwords.shellwords(cmd)

    # 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 = command

    if command == API_2FA_RECOVERY_CODES_COMMAND
      return Struct::ParsedCommand.new(command, git_access_command, nil, args)
    end

    raise DisallowedCommandError unless GIT_COMMANDS.include?(command)

    case command
    when 'git-lfs-authenticate'
      raise DisallowedCommandError unless args.count >= 2
      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
    end

    Struct::ParsedCommand.new(command, git_access_command, repo_name, args)
  end

  def determine_action(parsed_command)
    return Action::API2FARecovery.new(actor) if parsed_command.command == API_2FA_RECOVERY_CODES_COMMAND

    GitlabMetrics.measure('verify-access') do
      # GitlabNet#check_access will raise exception in the event of a problem
      initial_action = api.check_access(
        parsed_command.git_access_command,
        nil,
        parsed_command.repo_name,
        actor,
        '_any'
      )

      case parsed_command.command
      when GIT_LFS_AUTHENTICATE_COMMAND
        Action::GitLFSAuthenticate.new(actor, parsed_command.repo_name)
      else
        initial_action
      end
    end
  end

  def api
    @api ||= GitlabNet.new
  end
end