gitlab_shell.rb 4.45 KB
Newer Older
1
require 'shellwords'
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
2

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
3
require_relative 'gitlab_net'
4

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
5
class GitlabShell
6
  class AccessDeniedError < StandardError; end
7
  class DisallowedCommandError < StandardError; end
8
  class InvalidRepositoryPathError < StandardError; end
9

10
  GIT_COMMANDS = %w(git-upload-pack git-receive-pack git-upload-archive git-annex-shell git-lfs-authenticate).freeze
Douwe Maan's avatar
Douwe Maan committed
11

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
12
  attr_accessor :key_id, :repo_name, :git_cmd, :repos_path, :repo_name
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
13

14 15 16
  def initialize(key_id, origin_cmd)
    @key_id = key_id
    @origin_cmd = origin_cmd
17 18
    @config = GitlabConfig.new
    @repos_path = @config.repos_path
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
19 20 21
  end

  def exec
22 23 24 25
    unless @origin_cmd
      puts "Welcome to GitLab, #{username}!"
      return true
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
26

27
    parse_cmd
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
28

Douwe Maan's avatar
Douwe Maan committed
29
    verify_access
30 31 32 33

    process_cmd

    true
34
  rescue GitlabNet::ApiUnreachableError => ex
35 36 37 38 39 40 41 42
    $stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
    false
  rescue AccessDeniedError => ex
    message = "gitlab-shell: Access denied for git command <#{@origin_cmd}> by #{log_username}."
    $logger.warn message

    $stderr.puts "GitLab: #{ex.message}"
    false
43 44 45
  rescue DisallowedCommandError => ex
    message = "gitlab-shell: Attempt to execute disallowed command <#{@origin_cmd}> by #{log_username}."
    $logger.warn message
46 47 48 49 50 51

    $stderr.puts "GitLab: Disallowed command"
    false
  rescue InvalidRepositoryPathError => ex
    $stderr.puts "GitLab: Invalid repository path"
    false
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
52 53 54 55 56
  end

  protected

  def parse_cmd
57
    args = Shellwords.shellwords(@origin_cmd)
58
    @git_cmd = args.first
59
    @git_access = @git_cmd
60

Douwe Maan's avatar
Douwe Maan committed
61 62
    raise DisallowedCommandError unless GIT_COMMANDS.include?(@git_cmd)

63 64
    case @git_cmd
    when 'git-annex-shell'
65
      raise DisallowedCommandError unless @config.git_annex_enabled?
66

67 68 69 70
      @repo_name = escape_path(args[2].sub(/\A\/~\//, ''))

      # Make sure repository has git-annex enabled
      init_git_annex(@repo_name)
71 72 73 74 75 76 77 78 79 80 81
    when 'git-lfs-authenticate'
      raise DisallowedCommandError unless args.count >= 2
      @repo_name = escape_path(args[1])
      case args[2]
      when 'download'
        @git_access = 'git-upload-pack'
      when 'upload'
        @git_access = 'git-receive-pack'
      else
        raise DisallowedCommandError
      end
82
    else
83
      raise DisallowedCommandError unless args.count == 2
84 85
      @repo_name = escape_path(args.last)
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
86 87
  end

Douwe Maan's avatar
Douwe Maan committed
88
  def verify_access
89
    status = api.check_access(@git_access, @repo_name, @key_id, '_any')
Douwe Maan's avatar
Douwe Maan committed
90 91

    raise AccessDeniedError, status.message unless status.allowed?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
92
  end
93 94 95

  def process_cmd
    repo_full_path = File.join(repos_path, repo_name)
GitLab's avatar
GitLab committed
96

97
    if @git_cmd == 'git-annex-shell'
98 99 100 101 102 103 104 105 106 107 108
      raise DisallowedCommandError unless @config.git_annex_enabled?

      args = Shellwords.shellwords(@origin_cmd)
      parsed_args =
        args.map do |arg|
          # Convert /~/group/project.git to group/project.git
          # to make git annex path compatible with gitlab-shell
          if arg =~ /\A\/~\/.*\.git\Z/
            repo_full_path
          else
            arg
109
          end
110
        end
111

112 113
      $logger.info "gitlab-shell: executing git-annex command <#{parsed_args.join(' ')}> for #{log_username}."
      exec_cmd(*parsed_args)
GitLab's avatar
GitLab committed
114
    else
115
      $logger.info "gitlab-shell: executing git command <#{@git_cmd} #{repo_full_path}> for #{log_username}."
116 117
      exec_cmd(@git_cmd, repo_full_path)
    end
118
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
119

120
  # This method is not covered by Rspec because it ends the current Ruby process.
121
  def exec_cmd(*args)
122
    Kernel::exec({ 'PATH' => ENV['PATH'], 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'], 'GL_ID' => @key_id }, *args, unsetenv_others: true)
123 124
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
125 126
  def api
    GitlabNet.new
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
127
  end
128 129

  def user
130 131 132
    return @user if defined?(@user)

    begin
133
      @user = api.discover(@key_id)
134 135
    rescue GitlabNet::ApiUnreachableError
      @user = nil
136 137 138 139 140 141 142 143 144 145 146
    end
  end

  def username
    user && user['name'] || 'Anonymous'
  end

  # User identifier to be used in log messages.
  def log_username
    @config.audit_usernames ? username : "user with key #{@key_id}"
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
147 148

  def escape_path(path)
149 150 151
    full_repo_path = File.join(repos_path, path)

    if File.absolute_path(full_repo_path) == full_repo_path
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
152 153
      path
    else
154
      raise InvalidRepositoryPathError
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
155 156
    end
  end
157

158
  def init_git_annex(path)
159 160
    full_repo_path = File.join(repos_path, path)

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
161
    unless File.exists?(File.join(full_repo_path, 'annex'))
162
      cmd = %W(git --git-dir=#{full_repo_path} annex init "GitLab")
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
163
      system(*cmd, err: '/dev/null', out: '/dev/null')
164 165 166
      $logger.info "Enable git-annex for repository: #{path}."
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
167
end