Commit ebb6a075 authored by Jacob Vosmaer's avatar Jacob Vosmaer

Support Kerberos SPNEGO in GitHttpController

parent 4f5478fd
class Projects::GitHttpController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper
attr_reader :user
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
skip_before_action :repository
before_action :authenticate_user
around_action :authenticate_user
before_action :ensure_project_found!
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
......@@ -40,9 +43,13 @@ class Projects::GitHttpController < Projects::ApplicationController
private
def authenticate_user
return if project && project.public? && upload_pack?
if project && project.public? && upload_pack?
yield
return
end
authenticate_or_request_with_http_basic do |login, password|
if allow_basic_auth? && has_basic_credentials?(request)
login, password = user_name_and_password(request)
auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
if auth_result.type == :ci && upload_pack?
......@@ -53,8 +60,32 @@ class Projects::GitHttpController < Projects::ApplicationController
@user = auth_result.user
end
ci? || user
if ci? || user
yield
return
end
elsif allow_kerberos_spnego_auth? && has_spnego_credentials?(request)
spnego_token = Base64.strict_decode64(auth_param(request))
@user = find_kerberos_user(spnego_token)
if user
set_www_authenticate(spnego_challenge) if spnego_response_token
yield
return
end
end
# Authentication failed. Challenge the client to provide credentials.
challenges = []
challenges << 'Basic realm="GitLab"' if allow_basic_auth?
challenges << spnego_challenge if allow_kerberos_spnego_auth?
set_www_authenticate(challenges.join("\n"))
render plain: "HTTP Basic: Access denied\n", status: 401
end
def set_www_authenticate(value)
headers['Www-Authenticate'] = value
end
def ensure_project_found!
......
module KerberosSpnegoHelper
attr_reader :spnego_response_token
def allow_basic_auth?
if Gitlab.config.kerberos.enabled && Gitlab.config.kerberos.use_dedicated_port
!request_uses_kerberos_dedicated_port?
else
true
end
end
def allow_kerberos_spnego_auth?
return false unless Gitlab.config.kerberos.enabled
if Gitlab.config.kerberos.use_dedicated_port
request_uses_kerberos_dedicated_port?
else
true
end
end
def request_uses_kerberos_dedicated_port?
request.env['SERVER_PORT'] == Gitlab.config.kerberos.port.to_s
end
def spnego_challenge
if spnego_response_token
"Negotiate #{::Base64.strict_encode64(spnego_response_token)}"
else
'Negotiate'
end
end
def has_spnego_credentials?(request)
request.authorization.present? && (auth_scheme(request) == 'Negotiate')
end
def find_kerberos_user(spnego_token)
krb_principal = spnego_credentials!(spnego_token)
return unless krb_principal
identity = ::Identity.find_by(provider: :kerberos, extern_uid: krb_principal)
identity.user if identity
end
# The Kerberos backend will translate spnego_token into a Kerberos
# principal and/or provide a value for @spnego_response_token.
def spnego_credentials!(spnego_token)
require 'gssapi'
gss = GSSAPI::Simple.new(nil, nil, Gitlab.config.kerberos.keytab)
# the GSSAPI::Simple constructor transforms a nil service name into a default value, so
# pass service name to acquire_credentials explicitly to support the special meaning of nil
gss_service_name =
if Gitlab.config.kerberos.service_principal_name.present?
gss.import_name(Gitlab.config.kerberos.service_principal_name)
else
nil # accept any valid service principal name from keytab
end
gss.acquire_credentials(gss_service_name) # grab credentials from keytab
# Decode token
gss_result = gss.accept_context(spnego_token)
# gss_result will be 'true' if nothing has to be returned to the client
@spnego_response_token = gss_result if gss_result && gss_result != true
# Return user principal name if authentication succeeded
gss.display_name
rescue GSSAPI::GssApiError => ex
Rails.logger.error "#{self.class.name}: failed to process Negotiate/Kerberos authentication: #{ex.message}"
false
end
end
......@@ -93,8 +93,8 @@ Rails.application.routes.draw do
# Health check
get 'health_check(/:checks)' => 'health_check#index', as: :health_check
# Enable Grack support
mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put]
# Enable Grack support (for LFS only)
mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put]
# Help
get 'help' => 'help#index'
......@@ -502,6 +502,13 @@ Rails.application.routes.draw do
end
scope module: :projects do
# Git HTTP clients ('git clone' etc.)
scope constraints: { id: /.+\.git/, format: nil } do
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
......
......@@ -97,7 +97,7 @@ describe 'Git HTTP requests', lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, {}) do |response|
expect(response.status).to eq(401)
expect(response.status).to eq(404)
end
end
end
......@@ -126,12 +126,12 @@ describe 'Git HTTP requests', lib: true do
let(:env) { {spnego_request_token: 'opaque_request_token'} }
before do
allow_any_instance_of(Grack::Auth).to receive(:allow_kerberos_auth?).and_return(true)
allow_any_instance_of(Projects::GitHttpController).to receive(:allow_kerberos_spnego_auth?).and_return(true)
end
context "when authentication fails because of invalid Kerberos token" do
before do
allow_any_instance_of(Grack::Auth::Request).to receive(:spnego_credentials!).and_return(nil)
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_credentials!).and_return(nil)
end
it "responds with status 401" do
......@@ -143,7 +143,7 @@ describe 'Git HTTP requests', lib: true do
context "when authentication fails because of unknown Kerberos identity" do
before do
allow_any_instance_of(Grack::Auth::Request).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM")
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM")
end
it "responds with status 401" do
......@@ -156,8 +156,8 @@ describe 'Git HTTP requests', lib: true do
context "when authentication succeeds" do
before do
allow_any_instance_of(Grack::Auth::Request).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM")
user.identities.build(provider: "kerberos", extern_uid:"mylogin@FOO.COM").save
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM")
user.identities.create!(provider: "kerberos", extern_uid:"mylogin@FOO.COM")
end
context "when the user has access to the project" do
......@@ -187,7 +187,7 @@ describe 'Git HTTP requests', lib: true do
end
it "complies with RFC4559" do
allow_any_instance_of(Grack::Auth::Request).to receive(:spnego_response_token).and_return("opaque_response_token")
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_response_token).and_return("opaque_response_token")
download(path, env) do |response|
expect(response.headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}")
end
......@@ -202,7 +202,7 @@ describe 'Git HTTP requests', lib: true do
end
it "complies with RFC4559" do
allow_any_instance_of(Grack::Auth::Request).to receive(:spnego_response_token).and_return("opaque_response_token")
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_response_token).and_return("opaque_response_token")
download(path, env) do |response|
expect(response.headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}")
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment