git_access.rb 9.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 6
    include Gitlab::Utils::StrongMemoize

7
    UnauthorizedError = Class.new(StandardError)
8
    NotFoundError = Class.new(StandardError)
9
    ProjectCreationError = Class.new(StandardError)
10
    ProjectMovedError = Class.new(NotFoundError)
11 12 13 14

    ERROR_MESSAGES = {
      upload: 'You are not allowed to upload code for this project.',
      download: 'You are not allowed to download code from this project.',
15 16 17
      auth_upload: 'You are not allowed to upload code.',
      auth_download: 'You are not allowed to download code.',
      deploy_key_upload: 'This deploy key does not have write access to this project.',
M
Michael Kozono 已提交
18 19 20
      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.',
21
      command_not_allowed: "The command you're trying to execute is not allowed.",
M
Michael Kozono 已提交
22
      upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
23
      receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
T
Toon Claes 已提交
24
      read_only: 'The repository is temporarily read-only. Please try again later.',
25
      cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
D
Douwe Maan 已提交
26
    }.freeze
27

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

32
    attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path
33

34
    def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil)
35 36
      @actor    = actor
      @project  = project
37
      @protocol = protocol
38
      @authentication_abilities = authentication_abilities
39 40 41
      @namespace_path = namespace_path
      @project_path = project_path
      @redirected_path = redirected_path
42 43
    end

J
Jacob Vosmaer 已提交
44
    def check(cmd, changes)
45
      check_protocol!
N
Nick Thomas 已提交
46
      check_valid_actor!
47
      check_active_user!
48
      check_authentication_abilities!(cmd)
49
      check_command_disabled!(cmd)
50
      check_command_existence!(cmd)
51 52 53 54 55
      check_db_accessibility!(cmd)

      ensure_project_on_push!(cmd, changes)

      check_project_accessibility!
B
Bob Van Landuyt 已提交
56
      add_project_moved_message!
57
      check_repository_existence!
D
Douwe Maan 已提交
58

59 60
      case cmd
      when *DOWNLOAD_COMMANDS
61
        check_download_access!
62
      when *PUSH_COMMANDS
63
        check_push_access!(changes)
64
      end
65

66
      true
67 68
    end

69
    def guest_can_download_code?
70 71 72
      Guest.can?(:download_code, project)
    end

73
    def user_can_download_code?
74
      authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_code)
75 76
    end

77
    def build_can_download_code?
78
      authentication_abilities.include?(:build_download_code) && user_access.can_do_action?(:build_download_code)
79 80
    end

81 82 83 84
    def protocol_allowed?
      Gitlab::ProtocolAccess.allowed?(protocol)
    end

85 86
    private

87 88 89 90 91 92 93 94
    def check_valid_actor!
      return unless actor.is_a?(Key)

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

95 96 97 98 99 100 101
    def check_protocol!
      unless protocol_allowed?
        raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
      end
    end

    def check_active_user!
102 103
      return if deploy_key?

104
      if user && !user_access.allowed?
M
Michael Kozono 已提交
105
        raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
106 107 108
      end
    end

109 110 111 112 113 114 115 116 117 118 119 120 121
    def check_authentication_abilities!(cmd)
      case cmd
      when *DOWNLOAD_COMMANDS
        unless authentication_abilities.include?(:download_code) || authentication_abilities.include?(:build_download_code)
          raise UnauthorizedError, ERROR_MESSAGES[:auth_download]
        end
      when *PUSH_COMMANDS
        unless authentication_abilities.include?(:push_code)
          raise UnauthorizedError, ERROR_MESSAGES[:auth_upload]
        end
      end
    end

122 123
    def check_project_accessibility!
      if project.blank? || !can_read_project?
124
        raise NotFoundError, ERROR_MESSAGES[:project_not_found]
125 126 127
      end
    end

B
Bob Van Landuyt 已提交
128
    def add_project_moved_message!
129
      return if redirected_path.nil?
130

131
      project_moved = Checks::ProjectMoved.new(project, user, protocol, redirected_path)
132

B
Bob Van Landuyt 已提交
133
      project_moved.add_message
134 135
    end

136
    def check_command_disabled!(cmd)
M
Michael Kozono 已提交
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
      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]
153 154 155
      end
    end

156 157
    def check_command_existence!(cmd)
      unless ALL_COMMANDS.include?(cmd)
M
Michael Kozono 已提交
158
        raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
159 160 161
      end
    end

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 187 188 189 190 191 192 193 194 195 196
    def check_db_accessibility!(cmd)
      return unless receive_pack?(cmd)

      if Gitlab::Database.read_only?
        raise UnauthorizedError, push_to_read_only_message
      end
    end

    def ensure_project_on_push!(cmd, changes)
      return if project || deploy_key?
      return unless receive_pack?(cmd) && changes == '_any' && authentication_abilities.include?(:push_code)

      namespace = Namespace.find_by_full_path(namespace_path)

      return unless user&.can?(:create_projects, namespace)

      project_params = {
        path: project_path,
        namespace_id: namespace.id,
        visibility_level: Gitlab::VisibilityLevel::PRIVATE
      }

      project = Projects::CreateService.new(user, project_params).execute

      unless project.saved?
        raise ProjectCreationError, "Could not create project: #{project.errors.full_messages.join(', ')}"
      end

      @project = project
      user_access.project = @project

      Checks::ProjectCreated.new(project, user, protocol).add_message
    end

    def check_repository_existence!
197
      unless repository.exists?
198
        raise NotFoundError, ERROR_MESSAGES[:no_repo]
199 200 201
      end
    end

202
    def check_download_access!
203 204
      passed = deploy_key? ||
        user_can_download_code? ||
L
Lin Jen-Shin 已提交
205 206
        build_can_download_code? ||
        guest_can_download_code?
207 208 209 210 211 212

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

213
    def check_push_access!(changes)
214
      if project.repository_read_only?
T
Toon Claes 已提交
215 216 217
        raise UnauthorizedError, ERROR_MESSAGES[:read_only]
      end

218
      if deploy_key
219 220 221
        unless deploy_key.can_push_to?(project)
          raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
        end
222
      elsif user
223
        # User access is verified in check_change_access!
224 225 226 227
      else
        raise UnauthorizedError, ERROR_MESSAGES[:upload]
      end

228
      return if changes.blank? # Allow access this is needed for EE.
229

230
      check_change_access!(changes)
231 232
    end

233 234 235 236
    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
237 238 239
      changes_list.each.with_index do |change, index|
        first_change = index == 0

240 241
        # If user does not have access to make at least one change, cancel all
        # push by allowing the exception to bubble up
242
        check_single_change_access(change, skip_lfs_integrity_check: !first_change)
243 244 245
      end
    end

246
    def check_single_change_access(change, skip_lfs_integrity_check: false)
247
      Checks::ChangeAccess.new(
248 249 250
        change,
        user_access: user_access,
        project: project,
251
        skip_authorization: deploy_key?,
252
        skip_lfs_integrity_check: skip_lfs_integrity_check,
253 254
        protocol: protocol
      ).exec
255 256
    end

257
    def deploy_key
258 259 260 261 262
      actor if deploy_key?
    end

    def deploy_key?
      actor.is_a?(DeployKey)
263
    end
264

265 266 267 268
    def ci?
      actor == :ci
    end

269
    def can_read_project?
270
      if deploy_key?
271
        deploy_key.has_access_to?(project)
272
      elsif user
273
        user.can?(:read_project, project)
274 275
      elsif ci?
        true # allow CI (build without a user) for backwards compatibility
276
      end || Guest.can?(:read_project, project)
277 278
    end

279 280 281 282 283 284 285 286 287 288 289 290
    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 已提交
291 292 293 294 295 296 297 298
    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

299 300
    protected

301 302 303 304 305 306 307 308
    def user
      return @user if defined?(@user)

      @user =
        case actor
        when User
          actor
        when Key
M
Michael Kozono 已提交
309
          actor.user unless actor.is_a?(DeployKey)
310 311
        when :ci
          nil
312 313
        end
    end
314 315 316 317 318 319 320 321

    def user_access
      @user_access ||= if ci?
                         CiAccess.new
                       else
                         UserAccess.new(user, project: project)
                       end
    end
J
James Lopez 已提交
322 323 324 325

    def push_to_read_only_message
      ERROR_MESSAGES[:cannot_push_to_read_only]
    end
326 327 328 329

    def repository
      project.repository
    end
330 331
  end
end