PageRenderTime 27ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/gitlab/backend/grack_auth.rb

https://gitlab.com/svansteenis/gitlab-ee
Ruby | 257 lines | 175 code | 49 blank | 33 comment | 43 complexity | 739f8a6af6a0b9ef436ab49f3f598348 MD5 | raw file
  1. module Grack
  2. class AuthSpawner
  3. def self.call(env)
  4. # Avoid issues with instance variables in Grack::Auth persisting across
  5. # requests by creating a new instance for each request.
  6. Auth.new({}).call_with_kerberos_support(env)
  7. end
  8. end
  9. class Auth < Rack::Auth::Basic
  10. attr_accessor :user, :project, :env
  11. def call_with_kerberos_support(env)
  12. # Make sure the final leg of Kerberos authentication is applied as per RFC4559
  13. apply_negotiate_final_leg(call(env))
  14. end
  15. def call(env)
  16. @env = env
  17. @request = Rack::Request.new(env)
  18. @auth = Request.new(env)
  19. @ci = false
  20. # Need this patch due to the rails mount
  21. # Need this if under RELATIVE_URL_ROOT
  22. unless Gitlab.config.gitlab.relative_url_root.empty?
  23. # If website is mounted using relative_url_root need to remove it first
  24. @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'')
  25. else
  26. @env['PATH_INFO'] = @request.path
  27. end
  28. @env['SCRIPT_NAME'] = ""
  29. auth!
  30. lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
  31. return lfs_response unless lfs_response.nil?
  32. if @user.nil? && !@ci
  33. unauthorized
  34. else
  35. render_not_found
  36. end
  37. end
  38. private
  39. def allow_basic_auth?
  40. return true unless Gitlab.config.kerberos.enabled &&
  41. Gitlab.config.kerberos.use_dedicated_port &&
  42. @env['SERVER_PORT'] == Gitlab.config.kerberos.port.to_s
  43. end
  44. def allow_kerberos_auth?
  45. return false unless Gitlab.config.kerberos.enabled
  46. return true unless Gitlab.config.kerberos.use_dedicated_port
  47. # When using a dedicated port, allow Kerberos auth only if port matches the configured one
  48. @env['SERVER_PORT'] == Gitlab.config.kerberos.port.to_s
  49. end
  50. def spnego_challenge
  51. return "Negotiate" unless @auth.spnego_response_token
  52. "Negotiate #{::Base64.strict_encode64(@auth.spnego_response_token)}"
  53. end
  54. def challenge
  55. challenges = []
  56. challenges << super if allow_basic_auth?
  57. challenges << spnego_challenge if allow_kerberos_auth?
  58. # Use \n separator to generate multiple WWW-Authenticate headers in case of multiple challenges
  59. challenges.join("\n")
  60. end
  61. def apply_negotiate_final_leg(response)
  62. return response unless allow_kerberos_auth? && @auth.spnego_response_token
  63. # As per RFC4559, we may have a final WWW-Authenticate header to send in
  64. # the response even if it's not a 401 status
  65. status, headers, body = response
  66. headers['WWW-Authenticate'] = spnego_challenge
  67. [status, headers, body]
  68. end
  69. def valid_auth_method?
  70. (allow_basic_auth? && @auth.basic?) || (allow_kerberos_auth? && @auth.negotiate?)
  71. end
  72. def auth!
  73. return unless @auth.provided?
  74. return bad_request unless valid_auth_method?
  75. if @auth.negotiate?
  76. # Authentication with Kerberos token
  77. krb_principal = @auth.spnego_credentials!
  78. return unless krb_principal
  79. # Set @user if authentication succeeded
  80. identity = ::Identity.find_by(provider: :kerberos, extern_uid: krb_principal)
  81. @user = identity.user if identity
  82. else
  83. # Authentication with username and password
  84. login, password = @auth.credentials
  85. # Allow authentication for GitLab CI service
  86. # if valid token passed
  87. if ci_request?(login, password)
  88. @ci = true
  89. return
  90. end
  91. @user = authenticate_user(login, password)
  92. end
  93. end
  94. def ci_request?(login, password)
  95. matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
  96. if project && matched_login.present? && git_cmd == 'git-upload-pack'
  97. underscored_service = matched_login['s'].underscore
  98. if underscored_service == 'gitlab_ci'
  99. return project && project.valid_build_token?(password)
  100. elsif Service.available_services_names.include?(underscored_service)
  101. service_method = "#{underscored_service}_service"
  102. service = project.send(service_method)
  103. return service && service.activated? && service.valid_token?(password)
  104. end
  105. end
  106. false
  107. end
  108. def oauth_access_token_check(login, password)
  109. if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present?
  110. token = Doorkeeper::AccessToken.by_token(password)
  111. token && token.accessible? && User.find_by(id: token.resource_owner_id)
  112. end
  113. end
  114. def authenticate_user(login, password)
  115. user = Gitlab::Auth.find_with_user_password(login, password)
  116. unless user
  117. user = oauth_access_token_check(login, password)
  118. end
  119. # If the user authenticated successfully, we reset the auth failure count
  120. # from Rack::Attack for that IP. A client may attempt to authenticate
  121. # with a username and blank password first, and only after it receives
  122. # a 401 error does it present a password. Resetting the count prevents
  123. # false positives from occurring.
  124. #
  125. # Otherwise, we let Rack::Attack know there was a failed authentication
  126. # attempt from this IP. This information is stored in the Rails cache
  127. # (Redis) and will be used by the Rack::Attack middleware to decide
  128. # whether to block requests from this IP.
  129. config = Gitlab.config.rack_attack.git_basic_auth
  130. if config.enabled
  131. if user
  132. # A successful login will reset the auth failure count from this IP
  133. Rack::Attack::Allow2Ban.reset(@request.ip, config)
  134. else
  135. banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
  136. # Unless the IP is whitelisted, return true so that Allow2Ban
  137. # increments the counter (stored in Rails.cache) for the IP
  138. if config.ip_whitelist.include?(@request.ip)
  139. false
  140. else
  141. true
  142. end
  143. end
  144. if banned
  145. Rails.logger.info "IP #{@request.ip} failed to login " \
  146. "as #{login} but has been temporarily banned from Git auth"
  147. end
  148. end
  149. end
  150. user
  151. end
  152. def git_cmd
  153. if @request.get?
  154. @request.params['service']
  155. elsif @request.post?
  156. File.basename(@request.path)
  157. else
  158. nil
  159. end
  160. end
  161. def project
  162. return @project if defined?(@project)
  163. @project = project_by_path(@request.path_info)
  164. end
  165. def project_by_path(path)
  166. if m = /^([\w\.\/-]+)\.git/.match(path).to_a
  167. path_with_namespace = m.last
  168. path_with_namespace.gsub!(/\.wiki$/, '')
  169. path_with_namespace[0] = '' if path_with_namespace.start_with?('/')
  170. Project.find_with_namespace(path_with_namespace)
  171. end
  172. end
  173. def render_not_found
  174. [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
  175. end
  176. class Request < Rack::Auth::Basic::Request
  177. attr_reader :spnego_response_token
  178. def negotiate?
  179. parts.first && scheme == "negotiate"
  180. end
  181. def spnego_token
  182. ::Base64.strict_decode64(params)
  183. end
  184. def spnego_credentials!
  185. require 'gssapi'
  186. gss = GSSAPI::Simple.new(nil, nil, Gitlab.config.kerberos.keytab)
  187. # the GSSAPI::Simple constructor transforms a nil service name into a default value, so
  188. # pass service name to acquire_credentials explicitly to support the special meaning of nil
  189. gss_service_name =
  190. if Gitlab.config.kerberos.service_principal_name.present?
  191. gss.import_name(Gitlab.config.kerberos.service_principal_name)
  192. else
  193. nil # accept any valid service principal name from keytab
  194. end
  195. gss.acquire_credentials(gss_service_name) # grab credentials from keytab
  196. # Decode token
  197. gss_result = gss.accept_context(spnego_token)
  198. # gss_result will be 'true' if nothing has to be returned to the client
  199. @spnego_response_token = gss_result if gss_result && gss_result != true
  200. # Return user principal name if authentication succeeded
  201. gss.display_name
  202. rescue GSSAPI::GssApiError => ex
  203. Rails.logger.error "#{self.class.name}: failed to process Negotiate/Kerberos authentication: #{ex.message}"
  204. false
  205. end
  206. end
  207. end
  208. end