git_access.rb 8.4 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
      no_repo: 'A repository for this project does not exist yet.',
      project_not_found: 'The project you were looking for could not be found.',
16
      namespace_not_found: 'The namespace you were looking for could not be found.',
M
Michael Kozono 已提交
17
      account_blocked: 'Your account has been blocked.',
18
      command_not_allowed: "The command you're trying to execute is not allowed.",
M
Michael Kozono 已提交
19
      upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
20
      receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
T
Toon Claes 已提交
21
      read_only: 'The repository is temporarily read-only. Please try again later.',
T
Tiago Botelho 已提交
22
      cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
D
Douwe Maan 已提交
23
    }.freeze
24

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

29
    attr_reader :actor, :project, :protocol, :authentication_abilities, :redirected_path, :target_namespace
30

31
    def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil, target_namespace: nil)
32 33
      @actor    = actor
      @project  = project
34
      @protocol = protocol
35
      @redirected_path = redirected_path
36
      @authentication_abilities = authentication_abilities
37
      @target_namespace = target_namespace
38 39
    end

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

50 51
      case cmd
      when *DOWNLOAD_COMMANDS
52
        check_download_access!
53
      when *PUSH_COMMANDS
54
        check_push_access!(cmd, changes)
T
Tiago Botelho 已提交
55
        check_namespace_accessibility!(cmd)
56
      end
57

58
      true
59 60
    end

61
    def guest_can_download_code?
62 63 64
      Guest.can?(:download_code, project)
    end

65
    def user_can_download_code?
66
      authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
67 68
    end

69
    def build_can_download_code?
70
      authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
71 72
    end

73 74 75 76
    def protocol_allowed?
      Gitlab::ProtocolAccess.allowed?(protocol)
    end

77 78
    private

79 80 81 82 83 84 85 86
    def check_valid_actor!
      return unless actor.is_a?(Key)

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

87 88 89 90 91 92 93
    def check_protocol!
      unless protocol_allowed?
        raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
      end
    end

    def check_active_user!
94 95
      return if deploy_key?

96
      if user && !user_access.allowed?
M
Michael Kozono 已提交
97
        raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
98 99 100
      end
    end

101 102
    def check_project_accessibility!(cmd)
      if (project.blank? || !can_read_project?) && !can_create_project_in_namespace?(cmd)
103
        raise NotFoundError, ERROR_MESSAGES[:project_not_found]
104 105 106
      end
    end

107
    def check_project_moved!
108
      return if redirected_path.nil?
109

110
      project_moved = Checks::ProjectMoved.new(project, user, redirected_path, protocol)
111

112 113 114 115 116
      if project_moved.permanent_redirect?
        project_moved.add_redirect_message
      else
        raise ProjectMovedError, project_moved.redirect_message(rejected: true)
      end
117 118
    end

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

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

145 146
    def check_repository_existence!(cmd)
      if (project.blank? || !project.repository.exists?) && !can_create_project_in_namespace?(cmd)
147 148 149 150
        raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
      end
    end

T
Tiago Botelho 已提交
151
    def check_namespace_accessibility!(cmd)
152
      return unless project.blank?
153

154 155 156
      unless target_namespace
        raise NotFoundError, ERROR_MESSAGES[:namespace_not_found]
      end
157 158
    end

159 160 161 162
    def check_download_access!
      return if deploy_key?

      passed = user_can_download_code? ||
L
Lin Jen-Shin 已提交
163 164
        build_can_download_code? ||
        guest_can_download_code?
165 166 167 168 169 170

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

171 172
    def check_push_access!(cmd, changes)
      return if project.blank? && can_create_project_in_namespace?(cmd)
173

174
      if project.repository_read_only?
T
Toon Claes 已提交
175 176 177 178
        raise UnauthorizedError, ERROR_MESSAGES[:read_only]
      end

      if Gitlab::Database.read_only?
J
James Lopez 已提交
179
        raise UnauthorizedError, push_to_read_only_message
180 181
      end

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
      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

207 208 209 210 211
    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|
212 213 214
        # 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)
215 216 217
      end
    end

218 219
    def check_single_change_access(change)
      Checks::ChangeAccess.new(
220 221 222
        change,
        user_access: user_access,
        project: project,
223 224 225
        skip_authorization: deploy_key?,
        protocol: protocol
      ).exec
226 227
    end

228
    def deploy_key
229 230 231 232 233
      actor if deploy_key?
    end

    def deploy_key?
      actor.is_a?(DeployKey)
234
    end
235

236 237 238 239
    def ci?
      actor == :ci
    end

240
    def can_read_project?
241
      if deploy_key?
242
        deploy_key.has_access_to?(project)
243
      elsif user
244
        user.can?(:read_project, project)
245 246
      elsif ci?
        true # allow CI (build without a user) for backwards compatibility
247
      end || Guest.can?(:read_project, project)
248 249
    end

250 251
    def can_create_project_in_namespace?(cmd)
      return false unless PUSH_COMMANDS.include?(cmd) && target_namespace
252

253
      user.can?(:create_projects, target_namespace)
254 255
    end

256 257 258 259 260 261 262 263 264 265 266 267
    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 已提交
268 269 270 271 272 273 274 275
    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

276 277
    protected

278 279 280 281 282 283 284 285
    def user
      return @user if defined?(@user)

      @user =
        case actor
        when User
          actor
        when Key
M
Michael Kozono 已提交
286
          actor.user unless actor.is_a?(DeployKey)
287 288
        when :ci
          nil
289 290
        end
    end
291 292 293 294 295 296 297 298

    def user_access
      @user_access ||= if ci?
                         CiAccess.new
                       else
                         UserAccess.new(user, project: project)
                       end
    end
J
James Lopez 已提交
299 300 301 302

    def push_to_read_only_message
      ERROR_MESSAGES[:cannot_push_to_read_only]
    end
303 304
  end
end