PageRenderTime 129ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/gitlab/auth/auth_finders.rb

https://gitlab.com/523/gitlab-ce
Ruby | 367 lines | 256 code | 88 blank | 23 comment | 32 complexity | 6b8b6de02498409a1a1ed3be025b224e MD5 | raw file
  1. # frozen_string_literal: true
  2. module Gitlab
  3. module Auth
  4. AuthenticationError = Class.new(StandardError)
  5. MissingTokenError = Class.new(AuthenticationError)
  6. TokenNotFoundError = Class.new(AuthenticationError)
  7. ExpiredError = Class.new(AuthenticationError)
  8. RevokedError = Class.new(AuthenticationError)
  9. ImpersonationDisabled = Class.new(AuthenticationError)
  10. UnauthorizedError = Class.new(AuthenticationError)
  11. class InsufficientScopeError < AuthenticationError
  12. attr_reader :scopes
  13. def initialize(scopes)
  14. @scopes = scopes.map { |s| s.try(:name) || s }
  15. end
  16. end
  17. module AuthFinders
  18. include Gitlab::Utils::StrongMemoize
  19. include ActionController::HttpAuthentication::Basic
  20. include ActionController::HttpAuthentication::Token
  21. PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'
  22. PRIVATE_TOKEN_PARAM = :private_token
  23. JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'
  24. JOB_TOKEN_PARAM = :job_token
  25. DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'
  26. RUNNER_TOKEN_PARAM = :token
  27. RUNNER_JOB_TOKEN_PARAM = :token
  28. # Check the Rails session for valid authentication details
  29. def find_user_from_warden
  30. current_request.env['warden']&.authenticate if verified_request?
  31. end
  32. def find_user_from_static_object_token(request_format)
  33. return unless valid_static_objects_format?(request_format)
  34. token = current_request.params[:token].presence || current_request.headers['X-Gitlab-Static-Object-Token'].presence
  35. return unless token
  36. User.find_by_static_object_token(token) || raise(UnauthorizedError)
  37. end
  38. def find_user_from_feed_token(request_format)
  39. return unless valid_rss_format?(request_format)
  40. return if Gitlab::CurrentSettings.disable_feed_token
  41. # NOTE: feed_token was renamed from rss_token but both needs to be supported because
  42. # users might have already added the feed to their RSS reader before the rename
  43. token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence
  44. return unless token
  45. User.find_by_feed_token(token) || raise(UnauthorizedError)
  46. end
  47. def find_user_from_bearer_token
  48. find_user_from_job_bearer_token ||
  49. find_user_from_access_token
  50. end
  51. def find_user_from_job_token
  52. return unless route_authentication_setting[:job_token_allowed]
  53. return find_user_from_basic_auth_job if route_authentication_setting[:job_token_allowed] == :basic_auth
  54. token = current_request.params[JOB_TOKEN_PARAM].presence ||
  55. current_request.params[RUNNER_JOB_TOKEN_PARAM].presence ||
  56. current_request.env[JOB_TOKEN_HEADER].presence
  57. return unless token
  58. job = find_valid_running_job_by_token!(token)
  59. @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables
  60. job.user
  61. end
  62. def find_user_from_basic_auth_job
  63. return unless has_basic_credentials?(current_request)
  64. login, password = user_name_and_password(current_request)
  65. return unless login.present? && password.present?
  66. return unless ::Gitlab::Auth::CI_JOB_USER == login
  67. job = find_valid_running_job_by_token!(password)
  68. @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables
  69. job.user
  70. end
  71. def find_user_from_basic_auth_password
  72. return unless has_basic_credentials?(current_request)
  73. login, password = user_name_and_password(current_request)
  74. return if ::Gitlab::Auth::CI_JOB_USER == login
  75. Gitlab::Auth.find_with_user_password(login, password)
  76. end
  77. def find_user_from_lfs_token
  78. return unless has_basic_credentials?(current_request)
  79. login, token = user_name_and_password(current_request)
  80. user = User.by_login(login)
  81. user if user && Gitlab::LfsToken.new(user).token_valid?(token)
  82. end
  83. def find_user_from_personal_access_token
  84. return unless access_token
  85. validate_access_token!
  86. access_token&.user || raise(UnauthorizedError)
  87. end
  88. # We allow Private Access Tokens with `api` scope to be used by web
  89. # requests on RSS feeds or ICS files for backwards compatibility.
  90. # It is also used by GraphQL/API requests.
  91. # And to allow accessing /archive programatically as it was a big pain point
  92. # for users https://gitlab.com/gitlab-org/gitlab/-/issues/28978.
  93. def find_user_from_web_access_token(request_format, scopes: [:api])
  94. return unless access_token && valid_web_access_format?(request_format)
  95. validate_access_token!(scopes: scopes)
  96. ::PersonalAccessTokens::LastUsedService.new(access_token).execute
  97. access_token.user || raise(UnauthorizedError)
  98. end
  99. def find_user_from_access_token
  100. return unless access_token
  101. validate_access_token!
  102. ::PersonalAccessTokens::LastUsedService.new(access_token).execute
  103. access_token.user || raise(UnauthorizedError)
  104. end
  105. # This returns a deploy token, not a user since a deploy token does not
  106. # belong to a user.
  107. #
  108. # deploy tokens are accepted with deploy token headers and basic auth headers
  109. def deploy_token_from_request
  110. return unless route_authentication_setting[:deploy_token_allowed]
  111. token = current_request.env[DEPLOY_TOKEN_HEADER].presence || parsed_oauth_token
  112. if has_basic_credentials?(current_request)
  113. _, token = user_name_and_password(current_request)
  114. end
  115. deploy_token = DeployToken.active.find_by_token(token)
  116. @current_authenticated_deploy_token = deploy_token # rubocop:disable Gitlab/ModuleWithInstanceVariables
  117. deploy_token
  118. end
  119. def cluster_agent_token_from_authorization_token
  120. return unless route_authentication_setting[:cluster_agent_token_allowed]
  121. return unless current_request.authorization.present?
  122. authorization_token, _options = token_and_options(current_request)
  123. ::Clusters::AgentToken.active.find_by_token(authorization_token)
  124. end
  125. def find_runner_from_token
  126. return unless api_request?
  127. token = current_request.params[RUNNER_TOKEN_PARAM].presence
  128. return unless token
  129. ::Ci::Runner.find_by_token(token) || raise(UnauthorizedError)
  130. end
  131. def validate_access_token!(scopes: [])
  132. # return early if we've already authenticated via a job token
  133. return if @current_authenticated_job.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
  134. # return early if we've already authenticated via a deploy token
  135. return if @current_authenticated_deploy_token.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
  136. return unless access_token
  137. case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
  138. when AccessTokenValidationService::INSUFFICIENT_SCOPE
  139. raise InsufficientScopeError, scopes
  140. when AccessTokenValidationService::EXPIRED
  141. raise ExpiredError
  142. when AccessTokenValidationService::REVOKED
  143. raise RevokedError
  144. when AccessTokenValidationService::IMPERSONATION_DISABLED
  145. raise ImpersonationDisabled
  146. end
  147. end
  148. private
  149. def find_user_from_job_bearer_token
  150. return unless route_authentication_setting[:job_token_allowed]
  151. token = parsed_oauth_token
  152. return unless token
  153. job = ::Ci::AuthJobFinder.new(token: token).execute
  154. return unless job
  155. @current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables
  156. job.user
  157. end
  158. def route_authentication_setting
  159. return {} unless respond_to?(:route_setting)
  160. route_setting(:authentication) || {}
  161. end
  162. def access_token
  163. strong_memoize(:access_token) do
  164. if try(:namespace_inheritable, :authentication)
  165. access_token_from_namespace_inheritable
  166. else
  167. # The token can be a PAT or an OAuth (doorkeeper) token
  168. # It is also possible that a PAT is encapsulated in a `Bearer` OAuth token
  169. # (e.g. NPM client registry auth), this case will be properly handled
  170. # by find_personal_access_token
  171. find_oauth_access_token || find_personal_access_token
  172. end
  173. end
  174. end
  175. def find_personal_access_token
  176. token =
  177. current_request.params[PRIVATE_TOKEN_PARAM].presence ||
  178. current_request.env[PRIVATE_TOKEN_HEADER].presence ||
  179. parsed_oauth_token
  180. return unless token
  181. # Expiration, revocation and scopes are verified in `validate_access_token!`
  182. PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
  183. end
  184. def find_oauth_access_token
  185. token = parsed_oauth_token
  186. return unless token
  187. # PATs with OAuth headers are not handled by OauthAccessToken
  188. return if matches_personal_access_token_length?(token)
  189. # Expiration, revocation and scopes are verified in `validate_access_token!`
  190. oauth_token = OauthAccessToken.by_token(token)
  191. raise UnauthorizedError unless oauth_token
  192. oauth_token.revoke_previous_refresh_token!
  193. oauth_token
  194. end
  195. def find_personal_access_token_from_http_basic_auth
  196. return unless route_authentication_setting[:basic_auth_personal_access_token]
  197. return unless has_basic_credentials?(current_request)
  198. _username, password = user_name_and_password(current_request)
  199. PersonalAccessToken.find_by_token(password)
  200. end
  201. def parsed_oauth_token
  202. Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
  203. end
  204. def matches_personal_access_token_length?(token)
  205. PersonalAccessToken::TOKEN_LENGTH_RANGE.include?(token.length)
  206. end
  207. # Check if the request is GET/HEAD, or if CSRF token is valid.
  208. def verified_request?
  209. Gitlab::RequestForgeryProtection.verified?(current_request.env)
  210. end
  211. def ensure_action_dispatch_request(request)
  212. ActionDispatch::Request.new(request.env.dup)
  213. end
  214. def current_request
  215. @current_request ||= ensure_action_dispatch_request(request)
  216. end
  217. def valid_web_access_format?(request_format)
  218. case request_format
  219. when :rss
  220. rss_request?
  221. when :ics
  222. ics_request?
  223. when :api
  224. api_request?
  225. when :archive
  226. archive_request?
  227. end
  228. end
  229. def valid_rss_format?(request_format)
  230. case request_format
  231. when :rss
  232. rss_request?
  233. when :ics
  234. ics_request?
  235. end
  236. end
  237. def valid_static_objects_format?(request_format)
  238. case request_format
  239. when :archive
  240. archive_request?
  241. when :blob
  242. blob_request?
  243. else
  244. false
  245. end
  246. end
  247. def rss_request?
  248. current_request.path.ends_with?('.atom') || current_request.format.atom?
  249. end
  250. def ics_request?
  251. current_request.path.ends_with?('.ics') || current_request.format.ics?
  252. end
  253. def api_request?
  254. current_request.path.starts_with?(Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/'))
  255. end
  256. def git_request?
  257. Gitlab::PathRegex.repository_git_route_regex.match?(current_request.path)
  258. end
  259. def git_lfs_request?
  260. Gitlab::PathRegex.repository_git_lfs_route_regex.match?(current_request.path)
  261. end
  262. def git_or_lfs_request?
  263. git_request? || git_lfs_request?
  264. end
  265. def archive_request?
  266. current_request.path.include?('/-/archive/')
  267. end
  268. def blob_request?
  269. current_request.path.include?('/raw/')
  270. end
  271. def find_valid_running_job_by_token!(token)
  272. ::Ci::AuthJobFinder.new(token: token).execute.tap do |job|
  273. raise UnauthorizedError unless job
  274. end
  275. end
  276. end
  277. end
  278. end
  279. Gitlab::Auth::AuthFinders.prepend_mod_with('Gitlab::Auth::AuthFinders')