PageRenderTime 57ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/gitlab/backend/grack_auth.rb

https://gitlab.com/axil/gitlab-ee
Ruby | 309 lines | 219 code | 54 blank | 36 comment | 47 complexity | 0d52f0a14f8625467f8888b630beeb2c MD5 | raw file
Possible License(s): CC-BY-3.0
  1. require_relative 'shell_env'
  2. module Grack
  3. class AuthSpawner
  4. def self.call(env)
  5. # Avoid issues with instance variables in Grack::Auth persisting across
  6. # requests by creating a new instance for each request.
  7. Auth.new({}).call(env)
  8. end
  9. end
  10. class Auth < Rack::Auth::Basic
  11. attr_accessor :user, :project, :env
  12. def call(env)
  13. @env = env
  14. @request = Rack::Request.new(env)
  15. @auth = Request.new(env)
  16. @ci = false
  17. # Need this patch due to the rails mount
  18. # Need this if under RELATIVE_URL_ROOT
  19. unless Gitlab.config.gitlab.relative_url_root.empty?
  20. # If website is mounted using relative_url_root need to remove it first
  21. @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'')
  22. else
  23. @env['PATH_INFO'] = @request.path
  24. end
  25. @env['SCRIPT_NAME'] = ""
  26. auth!
  27. lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
  28. return lfs_response unless lfs_response.nil?
  29. if project && authorized_request?
  30. # Tell gitlab-workhorse the request is OK, and what the GL_ID is
  31. render_grack_auth_ok
  32. elsif @user.nil? && !@ci
  33. unauthorized
  34. else
  35. apply_negotiate_final_leg(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. identity ||= ::Identity.find_by(provider: 'kerberos', extern_uid: krb_principal.split("@")[0])
  82. @user = identity.user if identity
  83. else
  84. # Authentication with username and password
  85. login, password = @auth.credentials
  86. # Allow authentication for GitLab CI service
  87. # if valid token passed
  88. if ci_request?(login, password)
  89. @ci = true
  90. return
  91. end
  92. @user = authenticate_user(login, password)
  93. end
  94. if @user
  95. Gitlab::ShellEnv.set_env(@user)
  96. @env['REMOTE_USER'] = @auth.username
  97. end
  98. end
  99. def ci_request?(login, password)
  100. matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
  101. if project && matched_login.present? && git_cmd == 'git-upload-pack'
  102. underscored_service = matched_login['s'].underscore
  103. if Service.available_services_names.include?(underscored_service)
  104. service_method = "#{underscored_service}_service"
  105. service = project.send(service_method)
  106. return service && service.activated? && service.valid_token?(password)
  107. end
  108. end
  109. false
  110. end
  111. def oauth_access_token_check(login, password)
  112. if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present?
  113. token = Doorkeeper::AccessToken.by_token(password)
  114. token && token.accessible? && User.find_by(id: token.resource_owner_id)
  115. end
  116. end
  117. def authenticate_user(login, password)
  118. user = Gitlab::Auth.new.find(login, password)
  119. unless user
  120. user = oauth_access_token_check(login, password)
  121. end
  122. # If the user authenticated successfully, we reset the auth failure count
  123. # from Rack::Attack for that IP. A client may attempt to authenticate
  124. # with a username and blank password first, and only after it receives
  125. # a 401 error does it present a password. Resetting the count prevents
  126. # false positives from occurring.
  127. #
  128. # Otherwise, we let Rack::Attack know there was a failed authentication
  129. # attempt from this IP. This information is stored in the Rails cache
  130. # (Redis) and will be used by the Rack::Attack middleware to decide
  131. # whether to block requests from this IP.
  132. config = Gitlab.config.rack_attack.git_basic_auth
  133. if config.enabled
  134. if user
  135. # A successful login will reset the auth failure count from this IP
  136. Rack::Attack::Allow2Ban.reset(@request.ip, config)
  137. else
  138. banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
  139. # Unless the IP is whitelisted, return true so that Allow2Ban
  140. # increments the counter (stored in Rails.cache) for the IP
  141. if config.ip_whitelist.include?(@request.ip)
  142. false
  143. else
  144. true
  145. end
  146. end
  147. if banned
  148. Rails.logger.info "IP #{@request.ip} failed to login " \
  149. "as #{login} but has been temporarily banned from Git auth"
  150. end
  151. end
  152. end
  153. user
  154. end
  155. def authorized_request?
  156. return true if @ci
  157. case git_cmd
  158. when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
  159. if !Gitlab.config.gitlab_shell.upload_pack
  160. false
  161. elsif user
  162. Gitlab::GitAccess.new(user, project).download_access_check.allowed?
  163. elsif project.public?
  164. # Allow clone/fetch for public projects
  165. true
  166. else
  167. false
  168. end
  169. when *Gitlab::GitAccess::PUSH_COMMANDS
  170. if !Gitlab.config.gitlab_shell.receive_pack
  171. false
  172. elsif user
  173. # Skip user authorization on upload request.
  174. # It will be done by the pre-receive hook in the repository.
  175. true
  176. else
  177. false
  178. end
  179. else
  180. false
  181. end
  182. end
  183. def git_cmd
  184. if @request.get?
  185. @request.params['service']
  186. elsif @request.post?
  187. File.basename(@request.path)
  188. else
  189. nil
  190. end
  191. end
  192. def project
  193. return @project if defined?(@project)
  194. @project = project_by_path(@request.path_info)
  195. end
  196. def project_by_path(path)
  197. if m = /^([\w\.\/-]+)\.git/.match(path).to_a
  198. path_with_namespace = m.last
  199. path_with_namespace.gsub!(/\.wiki$/, '')
  200. path_with_namespace[0] = '' if path_with_namespace.start_with?('/')
  201. Project.find_with_namespace(path_with_namespace)
  202. end
  203. end
  204. def render_grack_auth_ok
  205. repo_path =
  206. if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/
  207. ProjectWiki.new(project).repository.path_to_repo
  208. else
  209. project.repository.path_to_repo
  210. end
  211. [
  212. 200,
  213. { "Content-Type" => "application/json" },
  214. [JSON.dump({
  215. 'GL_ID' => Gitlab::ShellEnv.gl_id(@user),
  216. 'RepoPath' => repo_path,
  217. })]
  218. ]
  219. end
  220. def render_not_found
  221. [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
  222. end
  223. class Request < Rack::Auth::Basic::Request
  224. attr_reader :spnego_response_token
  225. def negotiate?
  226. parts.first && scheme == "negotiate"
  227. end
  228. def spnego_token
  229. ::Base64.strict_decode64(params)
  230. end
  231. def spnego_credentials!
  232. require 'gssapi'
  233. gss = GSSAPI::Simple.new(nil, nil, Gitlab.config.kerberos.keytab)
  234. # the GSSAPI::Simple constructor transforms a nil service name into a default value, so
  235. # pass service name to acquire_credentials explicitly to support the special meaning of nil
  236. gss_service_name =
  237. if Gitlab.config.kerberos.service_principal_name.present?
  238. gss.import_name(Gitlab.config.kerberos.service_principal_name)
  239. else
  240. nil # accept any valid service principal name from keytab
  241. end
  242. gss.acquire_credentials(gss_service_name) # grab credentials from keytab
  243. # Decode token
  244. gss_result = gss.accept_context(spnego_token)
  245. # gss_result will be 'true' if nothing has to be returned to the client
  246. @spnego_response_token = gss_result if gss_result && gss_result != true
  247. # Return user principal name if authentication succeeded
  248. gss.display_name
  249. rescue GSSAPI::GssApiError => ex
  250. Rails.logger.error "#{self.class.name}: failed to process Negotiate/Kerberos authentication: #{ex.message}"
  251. false
  252. end
  253. end
  254. end
  255. end