summaryrefslogtreecommitdiff
path: root/lib/gitlab_shell.rb
blob: 0a73e0437c61a8c0697016e520f19ee2c33ce129 (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
123
124
require 'shellwords'
require 'pathname'

require_relative 'errors'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
require_relative 'current_user_helper'
require_relative 'api_command_helper'
require_relative 'log_helper'

class GitlabShell
  include CurrentUserHelper
  include APICommandHelper
  include LogHelper

  class DisallowedCommandError < 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)
    if !origin_cmd || origin_cmd.empty?
      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
  end

  private

  attr_accessor :repo_name, :command, :git_access
  attr_reader :config, :key_id, :repo_path

  def api
    @api ||= GitlabNet.new
  end

  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, 'Expected two arguments' 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 lfs_authenticate
    lfs_access = api.lfs_authenticate(key_id, repo_name)
    return unless lfs_access

    puts lfs_access.authentication_payload
  end
end