202 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			202 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| module Gitlab
 | |
|   module APIAuthentication
 | |
|     class TokenResolver
 | |
|       include ActiveModel::Validations
 | |
| 
 | |
|       attr_reader :token_type
 | |
| 
 | |
|       validates :token_type, inclusion: {
 | |
|         in: %i[
 | |
|           personal_access_token_with_username
 | |
|           job_token_with_username
 | |
|           deploy_token_with_username
 | |
|           personal_access_token
 | |
|           job_token
 | |
|           deploy_token
 | |
|           personal_access_token_from_jwt
 | |
|           deploy_token_from_jwt
 | |
|           job_token_from_jwt
 | |
|           oauth_token
 | |
|         ]
 | |
|       }
 | |
| 
 | |
|       UsernameAndPassword = ::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword
 | |
| 
 | |
|       def initialize(token_type)
 | |
|         @token_type = token_type
 | |
|         validate!
 | |
|       end
 | |
| 
 | |
|       # Existing behavior is known to be inconsistent across authentication
 | |
|       # methods with regards to whether to silently ignore present but invalid
 | |
|       # credentials or to raise an error/respond with 401.
 | |
|       #
 | |
|       # If a token can be located from the provided credentials, but the token
 | |
|       # or credentials are in some way invalid, this implementation opts to
 | |
|       # raise an error.
 | |
|       #
 | |
|       # For example, if the raw credentials include a username and password, and
 | |
|       # a token is resolved from the password, but the username does not match
 | |
|       # the token, an error will be raised.
 | |
|       #
 | |
|       # See https://gitlab.com/gitlab-org/gitlab/-/issues/246569
 | |
| 
 | |
|       def resolve(raw)
 | |
|         case @token_type
 | |
|         when :personal_access_token
 | |
|           resolve_personal_access_token(raw)
 | |
|         when :job_token
 | |
|           resolve_job_token(raw)
 | |
|         when :deploy_token
 | |
|           resolve_deploy_token(raw)
 | |
|         when :personal_access_token_with_username
 | |
|           resolve_personal_access_token_with_username(raw)
 | |
|         when :job_token_with_username
 | |
|           resolve_job_token_with_username(raw)
 | |
|         when :deploy_token_with_username
 | |
|           resolve_deploy_token_with_username(raw)
 | |
|         when :personal_access_token_from_jwt
 | |
|           resolve_personal_access_token_from_jwt(raw)
 | |
|         when :deploy_token_from_jwt
 | |
|           resolve_deploy_token_from_jwt(raw)
 | |
|         when :job_token_from_jwt
 | |
|           resolve_job_token_from_jwt(raw)
 | |
|         when :oauth_token
 | |
|           resolve_oauth_token(raw)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       private
 | |
| 
 | |
|       def resolve_personal_access_token_with_username(raw)
 | |
|         raise ::Gitlab::Auth::UnauthorizedError unless raw.username
 | |
| 
 | |
|         with_personal_access_token(raw) do |pat|
 | |
|           break unless pat
 | |
| 
 | |
|           # Ensure that the username matches the token. This check is a subtle
 | |
|           # departure from the existing behavior of #find_personal_access_token_from_http_basic_auth.
 | |
|           # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_435907856
 | |
|           raise ::Gitlab::Auth::UnauthorizedError unless pat.user.username == raw.username
 | |
| 
 | |
|           pat
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_job_token_with_username(raw)
 | |
|         # Only look for a job if the username is correct
 | |
|         return if ::Gitlab::Auth::CI_JOB_USER != raw.username
 | |
| 
 | |
|         with_job_token(raw) do |job|
 | |
|           job
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_deploy_token_with_username(raw)
 | |
|         with_deploy_token(raw) do |token|
 | |
|           break unless token
 | |
| 
 | |
|           # Ensure that the username matches the token. This check is a subtle
 | |
|           # departure from the existing behavior of #deploy_token_from_request.
 | |
|           # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_474826205
 | |
|           raise ::Gitlab::Auth::UnauthorizedError unless token.username == raw.username
 | |
| 
 | |
|           token
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_personal_access_token(raw)
 | |
|         with_personal_access_token(raw) do |pat|
 | |
|           pat
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_job_token(raw)
 | |
|         with_job_token(raw) do |job|
 | |
|           job
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_deploy_token(raw)
 | |
|         with_deploy_token(raw) do |token|
 | |
|           token
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_personal_access_token_from_jwt(raw)
 | |
|         with_jwt_token(raw) do |jwt_token|
 | |
|           break unless jwt_token['token'].is_a?(Integer)
 | |
| 
 | |
|           pat = ::PersonalAccessToken.find(jwt_token['token'])
 | |
|           break unless pat
 | |
| 
 | |
|           pat
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_deploy_token_from_jwt(raw)
 | |
|         with_jwt_token(raw) do |jwt_token|
 | |
|           break unless jwt_token['token'].is_a?(String)
 | |
| 
 | |
|           resolve_deploy_token(UsernameAndPassword.new(nil, jwt_token['token']))
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_job_token_from_jwt(raw)
 | |
|         with_jwt_token(raw) do |jwt_token|
 | |
|           break unless jwt_token['token'].is_a?(String)
 | |
| 
 | |
|           resolve_job_token(UsernameAndPassword.new(nil, jwt_token['token']))
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def resolve_oauth_token(raw)
 | |
|         oauth_token = OauthAccessToken.by_token(raw.password)
 | |
|         raise ::Gitlab::Auth::UnauthorizedError unless oauth_token
 | |
| 
 | |
|         oauth_token.revoke_previous_refresh_token!
 | |
| 
 | |
|         ::Gitlab::Auth::Identity.link_from_oauth_token(oauth_token).tap do |identity|
 | |
|           raise ::Gitlab::Auth::UnauthorizedError if identity && !identity.valid?
 | |
|         end
 | |
| 
 | |
|         oauth_token
 | |
|       end
 | |
| 
 | |
|       def with_personal_access_token(raw, &block)
 | |
|         pat = ::PersonalAccessToken.find_by_token(raw.password)
 | |
|         return unless pat
 | |
| 
 | |
|         yield(pat)
 | |
|       end
 | |
| 
 | |
|       def with_deploy_token(raw, &block)
 | |
|         unless Gitlab::ExternalAuthorization.allow_deploy_tokens_and_deploy_keys?
 | |
|           raise ::Gitlab::Auth::UnauthorizedError
 | |
|         end
 | |
| 
 | |
|         token = ::DeployToken.active.find_by_token(raw.password)
 | |
|         return unless token
 | |
| 
 | |
|         yield(token)
 | |
|       end
 | |
| 
 | |
|       def with_job_token(raw, &block)
 | |
|         job = ::Ci::AuthJobFinder.new(token: raw.password).execute
 | |
|         raise ::Gitlab::Auth::UnauthorizedError unless job
 | |
| 
 | |
|         yield(job)
 | |
|       end
 | |
| 
 | |
|       def with_jwt_token(raw, &block)
 | |
|         jwt_token = ::Gitlab::JWTToken.decode(raw.password)
 | |
|         raise ::Gitlab::Auth::UnauthorizedError unless jwt_token
 | |
| 
 | |
|         yield(jwt_token)
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |