git_access.rb 7.1 KB
Newer Older
1 2
# Check a user's access to perform a git action. All public methods in this
# class return an instance of `GitlabAccessStatus`
3 4
module Gitlab
  class GitAccess
5
    UnauthorizedError = Class.new(StandardError)
6
    NotFoundError = Class.new(StandardError)
7 8 9 10

    ERROR_MESSAGES = {
      upload: 'You are not allowed to upload code for this project.',
      download: 'You are not allowed to download code from this project.',
11 12
      deploy_key_upload:
        'This deploy key does not have write access to this project.',
M
Michael Kozono 已提交
13 14 15
      no_repo: 'A repository for this project does not exist yet.',
      project_not_found: 'The project you were looking for could not be found.',
      account_blocked: 'Your account has been blocked.',
16
      command_not_allowed: "The command you're trying to execute is not allowed.",
M
Michael Kozono 已提交
17 18
      upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
      receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.'
D
Douwe Maan 已提交
19
    }.freeze
20

D
Douwe Maan 已提交
21 22
    DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
    PUSH_COMMANDS = %w{ git-receive-pack }.freeze
23
    ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
24

25
    attr_reader :actor, :project, :protocol, :authentication_abilities, :redirected_path
26

27
    def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil)
28 29
      @actor    = actor
      @project  = project
30
      @protocol = protocol
31
      @redirected_path = redirected_path
32
      @authentication_abilities = authentication_abilities
33 34
    end

J
Jacob Vosmaer 已提交
35
    def check(cmd, changes)
36
      check_protocol!
37
      check_active_user!
38
      check_project_accessibility!
39
      check_project_moved!
40
      check_command_disabled!(cmd)
41
      check_command_existence!(cmd)
42
      check_repository_existence!
D
Douwe Maan 已提交
43

44 45
      case cmd
      when *DOWNLOAD_COMMANDS
46
        check_download_access!
47
      when *PUSH_COMMANDS
48
        check_push_access!(changes)
49
      end
50

51
      true
52 53
    end

54
    def guest_can_download_code?
55 56 57
      Guest.can?(:download_code, project)
    end

58
    def user_can_download_code?
59
      authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
60 61
    end

62
    def build_can_download_code?
63
      authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
64 65
    end

66 67 68 69
    def protocol_allowed?
      Gitlab::ProtocolAccess.allowed?(protocol)
    end

70 71
    private

72 73 74 75 76 77 78
    def check_protocol!
      unless protocol_allowed?
        raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
      end
    end

    def check_active_user!
79 80
      return if deploy_key?

81
      if user && !user_access.allowed?
M
Michael Kozono 已提交
82
        raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
83 84 85 86 87
      end
    end

    def check_project_accessibility!
      if project.blank? || !can_read_project?
88
        raise NotFoundError, ERROR_MESSAGES[:project_not_found]
89 90 91
      end
    end

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
    def check_project_moved!
      if redirected_path
        url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
        message = <<-MESSAGE.strip_heredoc
          Project '#{redirected_path}' was moved to '#{project.full_path}'.

          Please update your Git remote and try again:

            git remote set-url origin #{url}
        MESSAGE

        raise NotFoundError, message
      end
    end

107
    def check_command_disabled!(cmd)
M
Michael Kozono 已提交
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
      if upload_pack?(cmd)
        check_upload_pack_disabled!
      elsif receive_pack?(cmd)
        check_receive_pack_disabled!
      end
    end

    def check_upload_pack_disabled!
      if http? && upload_pack_disabled_over_http?
        raise UnauthorizedError, ERROR_MESSAGES[:upload_pack_disabled_over_http]
      end
    end

    def check_receive_pack_disabled!
      if http? && receive_pack_disabled_over_http?
        raise UnauthorizedError, ERROR_MESSAGES[:receive_pack_disabled_over_http]
124 125 126
      end
    end

127 128
    def check_command_existence!(cmd)
      unless ALL_COMMANDS.include?(cmd)
M
Michael Kozono 已提交
129
        raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
130 131 132
      end
    end

133 134 135 136 137 138
    def check_repository_existence!
      unless project.repository.exists?
        raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
      end
    end

139 140 141 142
    def check_download_access!
      return if deploy_key?

      passed = user_can_download_code? ||
L
Lin Jen-Shin 已提交
143 144
        build_can_download_code? ||
        guest_can_download_code?
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176

      unless passed
        raise UnauthorizedError, ERROR_MESSAGES[:download]
      end
    end

    def check_push_access!(changes)
      if deploy_key
        check_deploy_key_push_access!
      elsif user
        check_user_push_access!
      else
        raise UnauthorizedError, ERROR_MESSAGES[:upload]
      end

      return if changes.blank? # Allow access.

      check_change_access!(changes)
    end

    def check_user_push_access!
      unless authentication_abilities.include?(:push_code)
        raise UnauthorizedError, ERROR_MESSAGES[:upload]
      end
    end

    def check_deploy_key_push_access!
      unless deploy_key.can_push_to?(project)
        raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
      end
    end

177 178 179 180 181
    def check_change_access!(changes)
      changes_list = Gitlab::ChangesList.new(changes)

      # Iterate over all changes to find if user allowed all of them to be applied
      changes_list.each do |change|
182 183 184
        # If user does not have access to make at least one change, cancel all
        # push by allowing the exception to bubble up
        check_single_change_access(change)
185 186 187
      end
    end

188 189
    def check_single_change_access(change)
      Checks::ChangeAccess.new(
190 191 192
        change,
        user_access: user_access,
        project: project,
193 194 195
        skip_authorization: deploy_key?,
        protocol: protocol
      ).exec
196 197
    end

198 199
    def matching_merge_request?(newrev, branch_name)
      Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
200 201
    end

202
    def deploy_key
203 204 205 206 207
      actor if deploy_key?
    end

    def deploy_key?
      actor.is_a?(DeployKey)
208
    end
209

210 211 212 213
    def ci?
      actor == :ci
    end

214
    def can_read_project?
215
      if deploy_key?
216
        deploy_key.has_access_to?(project)
217
      elsif user
218
        user.can?(:read_project, project)
219 220
      elsif ci?
        true # allow CI (build without a user) for backwards compatibility
221
      end || Guest.can?(:read_project, project)
222 223
    end

224 225 226 227 228 229 230 231 232 233 234 235
    def http?
      protocol == 'http'
    end

    def upload_pack?(command)
      command == 'git-upload-pack'
    end

    def receive_pack?(command)
      command == 'git-receive-pack'
    end

M
Michael Kozono 已提交
236 237 238 239 240 241 242 243
    def upload_pack_disabled_over_http?
      !Gitlab.config.gitlab_shell.upload_pack
    end

    def receive_pack_disabled_over_http?
      !Gitlab.config.gitlab_shell.receive_pack
    end

244 245
    protected

246 247 248 249 250 251 252 253
    def user
      return @user if defined?(@user)

      @user =
        case actor
        when User
          actor
        when Key
M
Michael Kozono 已提交
254
          actor.user unless actor.is_a?(DeployKey)
255 256
        when :ci
          nil
257 258
        end
    end
259 260 261 262 263 264 265 266

    def user_access
      @user_access ||= if ci?
                         CiAccess.new
                       else
                         UserAccess.new(user, project: project)
                       end
    end
267 268
  end
end