git_access.rb 10.2 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.',
Michael Kozono's avatar
Michael Kozono committed
18 19
      no_repo: 'A repository for this project does not exist yet.',
      project_not_found: 'The project you were looking for could not be found.',
20
      command_not_allowed: "The command you're trying to execute is not allowed.",
Michael Kozono's avatar
Michael Kozono committed
21
      upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
22
      receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
23
      read_only: 'The repository is temporarily read-only. Please try again later.',
24
      cannot_push_to_read_only: "You can't push code to a read-only GitLab instance."
Douwe Maan's avatar
Douwe Maan committed
25
    }.freeze
26

Douwe Maan's avatar
Douwe Maan committed
27 28
    DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
    PUSH_COMMANDS = %w{ git-receive-pack }.freeze
29
    ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
30

31
    attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path, :auth_result_type, :changes
32

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

44
    def check(cmd, changes)
45 46
      @changes = changes

47
      check_protocol!
Nick Thomas's avatar
Nick Thomas committed
48
      check_valid_actor!
49
      check_active_user!
50
      check_authentication_abilities!(cmd)
51
      check_command_disabled!(cmd)
52
      check_command_existence!(cmd)
53 54 55 56 57
      check_db_accessibility!(cmd)

      ensure_project_on_push!(cmd, changes)

      check_project_accessibility!
58
      add_project_moved_message!
59
      check_repository_existence!
60

61 62
      case cmd
      when *DOWNLOAD_COMMANDS
63
        check_download_access!
64
      when *PUSH_COMMANDS
65
        check_push_access!
66
      end
67

68
      true
69 70
    end

71
    def guest_can_download_code?
72 73 74
      Guest.can?(:download_code, project)
    end

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

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

83 84 85 86 87 88
    def request_from_ci_build?
      return false unless protocol == 'http'

      auth_result_type == :build || auth_result_type == :ci
    end

89 90 91 92
    def protocol_allowed?
      Gitlab::ProtocolAccess.allowed?(protocol)
    end

93 94
    private

95 96 97 98 99 100 101 102
    def check_valid_actor!
      return unless actor.is_a?(Key)

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

103
    def check_protocol!
104 105
      return if request_from_ci_build?

106 107 108 109 110 111
      unless protocol_allowed?
        raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
      end
    end

    def check_active_user!
112 113 114
      return unless user

      unless user_access.allowed?
115 116
        message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
        raise UnauthorizedError, message
117 118 119
      end
    end

120 121 122 123 124 125 126 127 128 129 130 131 132
    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

133 134
    def check_project_accessibility!
      if project.blank? || !can_read_project?
135
        raise NotFoundError, ERROR_MESSAGES[:project_not_found]
136 137 138
      end
    end

139
    def add_project_moved_message!
140
      return if redirected_path.nil?
141

142
      project_moved = Checks::ProjectMoved.new(project, user, protocol, redirected_path)
143

144
      project_moved.add_message
145 146
    end

147
    def check_command_disabled!(cmd)
Michael Kozono's avatar
Michael Kozono committed
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
      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]
164 165 166
      end
    end

167 168
    def check_command_existence!(cmd)
      unless ALL_COMMANDS.include?(cmd)
Michael Kozono's avatar
Michael Kozono committed
169
        raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
170 171 172
      end
    end

173 174 175 176 177 178 179 180 181 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 207
    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!
208
      unless repository.exists?
209
        raise NotFoundError, ERROR_MESSAGES[:no_repo]
210 211 212
      end
    end

213
    def check_download_access!
214
      passed = deploy_key? ||
215
        deploy_token? ||
216
        user_can_download_code? ||
217 218
        build_can_download_code? ||
        guest_can_download_code?
219 220 221 222 223 224

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

225
    def check_push_access!
226
      if project.repository_read_only?
227 228 229
        raise UnauthorizedError, ERROR_MESSAGES[:read_only]
      end

230
      if deploy_key?
231 232 233
        unless deploy_key.can_push_to?(project)
          raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
        end
234
      elsif user
235
        # User access is verified in check_change_access!
236 237 238 239
      else
        raise UnauthorizedError, ERROR_MESSAGES[:upload]
      end

240
      return if changes.blank? # Allow access this is needed for EE.
241

242
      check_change_access!
243 244
    end

245
    def check_change_access!
246 247 248 249 250
      # If there are worktrees with a HEAD pointing to a non-existent object,
      # calls to `git rev-list --all` will fail in git 2.15+. This should also
      # clear stale lock files.
      project.repository.clean_stale_repository_files

251
      # Iterate over all changes to find if user allowed all of them to be applied
252 253 254
      changes_list.each.with_index do |change, index|
        first_change = index == 0

255 256
        # If user does not have access to make at least one change, cancel all
        # push by allowing the exception to bubble up
257
        check_single_change_access(change, skip_lfs_integrity_check: !first_change)
258 259 260
      end
    end

261
    def check_single_change_access(change, skip_lfs_integrity_check: false)
262
      Checks::ChangeAccess.new(
263 264 265
        change,
        user_access: user_access,
        project: project,
266
        skip_authorization: deploy_key?,
267
        skip_lfs_integrity_check: skip_lfs_integrity_check,
268 269
        protocol: protocol
      ).exec
270 271
    end

272
    def deploy_key
273 274 275 276 277
      actor if deploy_key?
    end

    def deploy_key?
      actor.is_a?(DeployKey)
278
    end
279

280 281 282 283 284 285 286 287
    def deploy_token
      actor if deploy_token?
    end

    def deploy_token?
      actor.is_a?(DeployToken)
    end

288 289 290 291
    def ci?
      actor == :ci
    end

292
    def can_read_project?
293
      if deploy_key?
294
        deploy_key.has_access_to?(project)
295 296
      elsif deploy_token?
        deploy_token.has_access_to?(project)
297
      elsif user
298
        user.can?(:read_project, project)
299 300
      elsif ci?
        true # allow CI (build without a user) for backwards compatibility
301
      end || Guest.can?(:read_project, project)
302 303
    end

304 305 306 307 308 309 310 311 312 313 314 315
    def http?
      protocol == 'http'
    end

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

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

Michael Kozono's avatar
Michael Kozono committed
316 317 318 319 320 321 322 323
    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

324 325
    protected

326 327 328 329
    def changes_list
      @changes_list ||= Gitlab::ChangesList.new(changes)
    end

330 331 332 333 334 335 336
    def user
      return @user if defined?(@user)

      @user =
        case actor
        when User
          actor
337 338
        when DeployKey
          nil
339
        when Key
340
          actor.user
341 342
        when :ci
          nil
343 344
        end
    end
345 346 347 348

    def user_access
      @user_access ||= if ci?
                         CiAccess.new
349 350
                       elsif user && request_from_ci_build?
                         BuildAccess.new(user, project: project)
351 352 353 354
                       else
                         UserAccess.new(user, project: project)
                       end
    end
355 356 357 358

    def push_to_read_only_message
      ERROR_MESSAGES[:cannot_push_to_read_only]
    end
359 360 361 362

    def repository
      project.repository
    end
363 364
  end
end