user_auth_finders.rb 4.7 KB
Newer Older
1 2
# frozen_string_literal: true

Francisco Javier López's avatar
Francisco Javier López committed
3 4
module Gitlab
  module Auth
5 6 7 8 9 10 11 12
    AuthenticationError = Class.new(StandardError)
    MissingTokenError = Class.new(AuthenticationError)
    TokenNotFoundError = Class.new(AuthenticationError)
    ExpiredError = Class.new(AuthenticationError)
    RevokedError = Class.new(AuthenticationError)
    UnauthorizedError = Class.new(AuthenticationError)

    class InsufficientScopeError < AuthenticationError
13 14 15 16 17 18
      attr_reader :scopes
      def initialize(scopes)
        @scopes = scopes.map { |s| s.try(:name) || s }
      end
    end

Francisco Javier López's avatar
Francisco Javier López committed
19
    module UserAuthFinders
20 21
      include Gitlab::Utils::StrongMemoize

22 23 24
      PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'.freeze
      PRIVATE_TOKEN_PARAM = :private_token

Francisco Javier López's avatar
Francisco Javier López committed
25
      # Check the Rails session for valid authentication details
26
      def find_user_from_warden
27
        current_request.env['warden']&.authenticate if verified_request?
Francisco Javier López's avatar
Francisco Javier López committed
28 29
      end

30 31
      def find_user_from_feed_token(request_format)
        return unless valid_rss_format?(request_format)
Francisco Javier López's avatar
Francisco Javier López committed
32

33 34 35
        # NOTE: feed_token was renamed from rss_token but both needs to be supported because
        #       users might have already added the feed to their RSS reader before the rename
        token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence
36
        return unless token
Francisco Javier López's avatar
Francisco Javier López committed
37

38
        User.find_by_feed_token(token) || raise(UnauthorizedError)
Francisco Javier López's avatar
Francisco Javier López committed
39 40
      end

41 42 43 44 45 46 47 48 49 50 51
      # We only allow Private Access Tokens with `api` scope to be used by web
      # requests on RSS feeds or ICS files for backwards compatibility.
      # It is also used by GraphQL/API requests.
      def find_user_from_web_access_token(request_format)
        return unless access_token && valid_web_access_format?(request_format)

        validate_access_token!(scopes: [:api])

        access_token.user || raise(UnauthorizedError)
      end

52 53
      def find_user_from_access_token
        return unless access_token
Francisco Javier López's avatar
Francisco Javier López committed
54

55
        validate_access_token!
Francisco Javier López's avatar
Francisco Javier López committed
56

57
        access_token.user || raise(UnauthorizedError)
58
      end
Francisco Javier López's avatar
Francisco Javier López committed
59

60
      def validate_access_token!(scopes: [])
61 62 63 64
        return unless access_token

        case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
        when AccessTokenValidationService::INSUFFICIENT_SCOPE
65
          raise InsufficientScopeError.new(scopes)
66
        when AccessTokenValidationService::EXPIRED
67
          raise ExpiredError
68
        when AccessTokenValidationService::REVOKED
69
          raise RevokedError
70
        end
Francisco Javier López's avatar
Francisco Javier López committed
71 72
      end

73
      private
Francisco Javier López's avatar
Francisco Javier López committed
74

75 76 77 78 79 80
      def route_authentication_setting
        return {} unless respond_to?(:route_setting)

        route_setting(:authentication) || {}
      end

81
      def access_token
82 83 84
        strong_memoize(:access_token) do
          find_oauth_access_token || find_personal_access_token
        end
85
      end
86

87 88 89
      def find_personal_access_token
        token =
          current_request.params[PRIVATE_TOKEN_PARAM].presence ||
90
          current_request.env[PRIVATE_TOKEN_HEADER].presence
91

92
        return unless token
93

94
        # Expiration, revocation and scopes are verified in `validate_access_token!`
95
        PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
96 97
      end

Francisco Javier López's avatar
Francisco Javier López committed
98 99
      def find_oauth_access_token
        token = Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
100
        return unless token
Francisco Javier López's avatar
Francisco Javier López committed
101

102
        # Expiration, revocation and scopes are verified in `validate_access_token!`
103
        oauth_token = OauthAccessToken.by_token(token)
104
        raise UnauthorizedError unless oauth_token
105 106 107

        oauth_token.revoke_previous_refresh_token!
        oauth_token
Francisco Javier López's avatar
Francisco Javier López committed
108 109 110 111
      end

      # Check if the request is GET/HEAD, or if CSRF token is valid.
      def verified_request?
112
        Gitlab::RequestForgeryProtection.verified?(current_request.env)
Francisco Javier López's avatar
Francisco Javier López committed
113 114 115
      end

      def ensure_action_dispatch_request(request)
116
        ActionDispatch::Request.new(request.env.dup)
Francisco Javier López's avatar
Francisco Javier López committed
117
      end
118 119 120 121

      def current_request
        @current_request ||= ensure_action_dispatch_request(request)
      end
122

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
      def valid_web_access_format?(request_format)
        case request_format
        when :rss
          rss_request?
        when :ics
          ics_request?
        when :api
          api_request?
        end
      end

      def valid_rss_format?(request_format)
        case request_format
        when :rss
          rss_request?
        when :ics
          ics_request?
        end
      end

143 144 145 146 147 148 149
      def rss_request?
        current_request.path.ends_with?('.atom') || current_request.format.atom?
      end

      def ics_request?
        current_request.path.ends_with?('.ics') || current_request.format.ics?
      end
150 151 152 153

      def api_request?
        current_request.path.starts_with?("/api/")
      end
Francisco Javier López's avatar
Francisco Javier López committed
154 155 156
    end
  end
end