auth_finders.rb 7.44 KB
Newer Older
1 2
# frozen_string_literal: true

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

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

20 21
    module AuthFinders
      prepend_if_ee('::EE::Gitlab::Auth::AuthFinders') # rubocop: disable Cop/InjectEnterpriseEditionModule
22

Francisco Lopez's avatar
Francisco Lopez committed
23
      include Gitlab::Utils::StrongMemoize
24
      include ActionController::HttpAuthentication::Basic
Francisco Lopez's avatar
Francisco Lopez committed
25

26
      PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'
27
      PRIVATE_TOKEN_PARAM = :private_token
28
      JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
29
      JOB_TOKEN_PARAM = :job_token
30
      RUNNER_TOKEN_PARAM = :token
31
      RUNNER_JOB_TOKEN_PARAM = :token
32

Francisco Lopez's avatar
Francisco Lopez committed
33
      # Check the Rails session for valid authentication details
Francisco Lopez's avatar
Francisco Lopez committed
34
      def find_user_from_warden
35
        current_request.env['warden']&.authenticate if verified_request?
Francisco Lopez's avatar
Francisco Lopez committed
36 37
      end

38 39 40 41 42 43 44 45 46
      def find_user_from_static_object_token(request_format)
        return unless valid_static_objects_format?(request_format)

        token = current_request.params[:token].presence || current_request.headers['X-Gitlab-Static-Object-Token'].presence
        return unless token

        User.find_by_static_object_token(token) || raise(UnauthorizedError)
      end

47 48
      def find_user_from_feed_token(request_format)
        return unless valid_rss_format?(request_format)
Francisco Lopez's avatar
Francisco Lopez committed
49

50 51 52
        # 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
53
        return unless token
Francisco Lopez's avatar
Francisco Lopez committed
54

55
        User.find_by_feed_token(token) || raise(UnauthorizedError)
Francisco Lopez's avatar
Francisco Lopez committed
56 57
      end

58 59 60
      def find_user_from_job_token
        return unless route_authentication_setting[:job_token_allowed]

61 62 63 64
        token = current_request.params[JOB_TOKEN_PARAM].presence ||
          current_request.params[RUNNER_JOB_TOKEN_PARAM].presence ||
          current_request.env[JOB_TOKEN_HEADER].presence
        return unless token
65 66

        job = ::Ci::Build.find_by_token(token)
67
        raise UnauthorizedError unless job
68 69 70 71 72 73

        @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables

        job.user
      end

74 75 76 77 78 79 80 81 82 83 84 85 86
      def find_user_from_basic_auth_job
        return unless has_basic_credentials?(current_request)

        login, password = user_name_and_password(current_request)
        return unless login.present? && password.present?
        return unless ::Ci::Build::CI_REGISTRY_USER == login

        job = ::Ci::Build.find_by_token(password)
        raise UnauthorizedError unless job

        job.user
      end

87 88 89 90 91 92 93 94 95 96 97
      # 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

Francisco Lopez's avatar
Francisco Lopez committed
98 99
      def find_user_from_access_token
        return unless access_token
Francisco Lopez's avatar
Francisco Lopez committed
100

Francisco Lopez's avatar
Francisco Lopez committed
101
        validate_access_token!
Francisco Lopez's avatar
Francisco Lopez committed
102

103
        access_token.user || raise(UnauthorizedError)
Francisco Lopez's avatar
Francisco Lopez committed
104
      end
Francisco Lopez's avatar
Francisco Lopez committed
105

106 107 108 109 110 111 112 113 114
      def find_runner_from_token
        return unless api_request?

        token = current_request.params[RUNNER_TOKEN_PARAM].presence
        return unless token

        ::Ci::Runner.find_by_token(token) || raise(UnauthorizedError)
      end

Francisco Lopez's avatar
Francisco Lopez committed
115
      def validate_access_token!(scopes: [])
116 117 118 119
        return unless access_token

        case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
        when AccessTokenValidationService::INSUFFICIENT_SCOPE
120
          raise InsufficientScopeError.new(scopes)
121
        when AccessTokenValidationService::EXPIRED
122
          raise ExpiredError
123
        when AccessTokenValidationService::REVOKED
124
          raise RevokedError
125 126
        when AccessTokenValidationService::IMPERSONATION_DISABLED
          raise ImpersonationDisabled
127
        end
Francisco Lopez's avatar
Francisco Lopez committed
128 129
      end

Francisco Lopez's avatar
Francisco Lopez committed
130
      private
Francisco Lopez's avatar
Francisco Lopez committed
131

132 133 134 135 136 137
      def route_authentication_setting
        return {} unless respond_to?(:route_setting)

        route_setting(:authentication) || {}
      end

Francisco Lopez's avatar
Francisco Lopez committed
138
      def access_token
Francisco Lopez's avatar
Francisco Lopez committed
139 140 141
        strong_memoize(:access_token) do
          find_oauth_access_token || find_personal_access_token
        end
Francisco Lopez's avatar
Francisco Lopez committed
142
      end
143

Francisco Lopez's avatar
Francisco Lopez committed
144 145 146
      def find_personal_access_token
        token =
          current_request.params[PRIVATE_TOKEN_PARAM].presence ||
147 148
          current_request.env[PRIVATE_TOKEN_HEADER].presence ||
          parsed_oauth_token
149
        return unless token
150

Francisco Lopez's avatar
Francisco Lopez committed
151
        # Expiration, revocation and scopes are verified in `validate_access_token!`
152
        PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
153 154
      end

Francisco Lopez's avatar
Francisco Lopez committed
155
      def find_oauth_access_token
156
        token = parsed_oauth_token
Francisco Lopez's avatar
Francisco Lopez committed
157
        return unless token
Francisco Lopez's avatar
Francisco Lopez committed
158

159 160 161
        # PATs with OAuth headers are not handled by OauthAccessToken
        return if matches_personal_access_token_length?(token)

Francisco Lopez's avatar
Francisco Lopez committed
162
        # Expiration, revocation and scopes are verified in `validate_access_token!`
163
        oauth_token = OauthAccessToken.by_token(token)
164
        raise UnauthorizedError unless oauth_token
165 166 167

        oauth_token.revoke_previous_refresh_token!
        oauth_token
Francisco Lopez's avatar
Francisco Lopez committed
168 169
      end

170 171 172 173 174 175 176 177
      def parsed_oauth_token
        Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
      end

      def matches_personal_access_token_length?(token)
        token.length == PersonalAccessToken::TOKEN_LENGTH
      end

Francisco Lopez's avatar
Francisco Lopez committed
178 179
      # Check if the request is GET/HEAD, or if CSRF token is valid.
      def verified_request?
180
        Gitlab::RequestForgeryProtection.verified?(current_request.env)
Francisco Lopez's avatar
Francisco Lopez committed
181 182 183
      end

      def ensure_action_dispatch_request(request)
184
        ActionDispatch::Request.new(request.env.dup)
Francisco Lopez's avatar
Francisco Lopez committed
185
      end
186 187 188 189

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

191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
      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

211 212 213 214
      def valid_static_objects_format?(request_format)
        case request_format
        when :archive
          archive_request?
215 216
        when :blob
          blob_request?
217 218 219 220 221
        else
          false
        end
      end

222 223 224 225 226 227 228
      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
229 230

      def api_request?
231
        current_request.path.starts_with?('/api/')
232
      end
233 234 235 236

      def archive_request?
        current_request.path.include?('/-/archive/')
      end
237 238 239 240

      def blob_request?
        current_request.path.include?('/raw/')
      end
Francisco Lopez's avatar
Francisco Lopez committed
241 242 243
    end
  end
end