git_access.rb 7.3 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
    ProjectMovedError = Class.new(NotFoundError)
8 9 10 11

    ERROR_MESSAGES = {
      upload: 'You are not allowed to upload code for this project.',
      download: 'You are not allowed to download code from this project.',
12 13
      deploy_key_upload:
        'This deploy key does not have write access to this project.',
M
Michael Kozono 已提交
14 15 16
      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.',
17
      command_not_allowed: "The command you're trying to execute is not allowed.",
M
Michael Kozono 已提交
18 19
      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 已提交
20
    }.freeze
21

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

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

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

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

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

53
      true
54 55
    end

56
    def guest_can_download_code?
57 58 59
      Guest.can?(:download_code, project)
    end

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

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

68 69 70 71
    def protocol_allowed?
      Gitlab::ProtocolAccess.allowed?(protocol)
    end

72 73
    private

74 75 76 77 78 79 80 81
    def check_valid_actor!
      return unless actor.is_a?(Key)

      unless actor.valid?
        raise UnauthorizedError, "Your SSH key #{actor.errors[:key].first}."
      end
    end

82 83 84 85 86 87 88
    def check_protocol!
      unless protocol_allowed?
        raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
      end
    end

    def check_active_user!
89 90
      return if deploy_key?

91
      if user && !user_access.allowed?
M
Michael Kozono 已提交
92
        raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
93 94 95 96 97
      end
    end

    def check_project_accessibility!
      if project.blank? || !can_read_project?
98
        raise NotFoundError, ERROR_MESSAGES[:project_not_found]
99 100 101
      end
    end

102
    def check_project_moved!
103
      return unless redirected_path
104

105 106 107
      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}'.
108

109
        Please update your Git remote and try again:
110

111 112 113 114
          git remote set-url origin #{url}
      MESSAGE

      raise ProjectMovedError, message
115 116
    end

117
    def check_command_disabled!(cmd)
M
Michael Kozono 已提交
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
      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]
134 135 136
      end
    end

137 138
    def check_command_existence!(cmd)
      unless ALL_COMMANDS.include?(cmd)
M
Michael Kozono 已提交
139
        raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
140 141 142
      end
    end

143 144 145 146 147 148
    def check_repository_existence!
      unless project.repository.exists?
        raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
      end
    end

149 150 151 152
    def check_download_access!
      return if deploy_key?

      passed = user_can_download_code? ||
L
Lin Jen-Shin 已提交
153 154
        build_can_download_code? ||
        guest_can_download_code?
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186

      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

187 188 189 190 191
    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|
192 193 194
        # 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)
195 196 197
      end
    end

198 199
    def check_single_change_access(change)
      Checks::ChangeAccess.new(
200 201 202
        change,
        user_access: user_access,
        project: project,
203 204 205
        skip_authorization: deploy_key?,
        protocol: protocol
      ).exec
206 207
    end

208 209
    def matching_merge_request?(newrev, branch_name)
      Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
210 211
    end

212
    def deploy_key
213 214 215 216 217
      actor if deploy_key?
    end

    def deploy_key?
      actor.is_a?(DeployKey)
218
    end
219

220 221 222 223
    def ci?
      actor == :ci
    end

224
    def can_read_project?
225
      if deploy_key?
226
        deploy_key.has_access_to?(project)
227
      elsif user
228
        user.can?(:read_project, project)
229 230
      elsif ci?
        true # allow CI (build without a user) for backwards compatibility
231
      end || Guest.can?(:read_project, project)
232 233
    end

234 235 236 237 238 239 240 241 242 243 244 245
    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 已提交
246 247 248 249 250 251 252 253
    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

254 255
    protected

256 257 258 259 260 261 262 263
    def user
      return @user if defined?(@user)

      @user =
        case actor
        when User
          actor
        when Key
M
Michael Kozono 已提交
264
          actor.user unless actor.is_a?(DeployKey)
265 266
        when :ci
          nil
267 268
        end
    end
269 270 271 272 273 274 275 276

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