diff --git a/src/api/Gemfile b/src/api/Gemfile index 1738355b014..27a875999bd 100644 --- a/src/api/Gemfile +++ b/src/api/Gemfile @@ -74,6 +74,8 @@ group :development, :production do gem 'clockwork', '>= 0.7' # as interface to LDAP gem 'ruby-ldap', require: false + # to enable GSSAPI / Kerberos authentication + gem "gssapi", require: false end group :production do diff --git a/src/api/Gemfile.lock b/src/api/Gemfile.lock index 4a79d4d3205..cc00776d591 100644 --- a/src/api/Gemfile.lock +++ b/src/api/Gemfile.lock @@ -116,16 +116,19 @@ GEM faker (1.7.3) i18n (~> 0.5) feature (1.4.0) - flog (4.6.1) + ffi (1.9.17) path_expander (~> 1.0) ruby_parser (~> 3.1, > 3.1.0) sexp_processor (~> 4.8) + flog (4.6.1) flot-rails (0.0.7) jquery-rails font-awesome-rails (4.7.0.1) railties (>= 3.2, < 5.1) globalid (0.3.7) activesupport (>= 4.1.0) + gssapi (1.2.0) + ffi (>= 1.0.1) haml (4.0.7) tilt haml_lint (0.23.2) @@ -398,6 +401,7 @@ DEPENDENCIES flog (> 4.1.0) flot-rails font-awesome-rails + gssapi haml haml_lint hoptoad_notifier (~> 2.3) diff --git a/src/api/app/controllers/application_controller.rb b/src/api/app/controllers/application_controller.rb index 4f7b027e99e..7ccacd17c9e 100644 --- a/src/api/app/controllers/application_controller.rb +++ b/src/api/app/controllers/application_controller.rb @@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base to: :authenticator def authenticator - @authenticator ||= Authenticator.new(request, session) + @authenticator ||= Authenticator.new(request, session, response) end def pundit_user @@ -303,7 +303,13 @@ def gather_exception_defaults(opt) end if @status == 401 - response.headers["WWW-Authenticate"] = 'basic realm="API login"' + unless response.headers["WWW-Authenticate"] + if CONFIG['kerberos_service_principal'] + response.headers["WWW-Authenticate"] = 'Negotiate, basic realm="API login"' + else + response.headers["WWW-Authenticate"] = 'basic realm="API login"' + end + end end if @status == 404 @summary ||= "Not found" diff --git a/src/api/config/options.yml.example b/src/api/config/options.yml.example index 2f0cd26496b..1a669da585b 100644 --- a/src/api/config/options.yml.example +++ b/src/api/config/options.yml.example @@ -35,6 +35,10 @@ frontend_protocol: https #external_frontend_port: 443 #external_frontend_protocol: https +# Kerberos authentication +#kerberos_keytab: "/etc/krb5.keytab" +#kerberos_service_principal: "HTTP/hostname.example.com@EXAMPLE.COM" +#kerberos_realm: "EXAMPLE.COM" extended_backend_log: false diff --git a/src/api/lib/authenticator.rb b/src/api/lib/authenticator.rb index 83ae5e4eef1..530e3f94084 100644 --- a/src/api/lib/authenticator.rb +++ b/src/api/lib/authenticator.rb @@ -1,3 +1,7 @@ +if CONFIG['kerberos_service_principal'] + require_dependency 'gssapi' +end + class Authenticator class AuthenticationRequiredError < APIException setup 401, "Authentication required" @@ -25,7 +29,8 @@ class AdminUserRequiredError < APIException attr_reader :request, :session, :user_permissions, :http_user - def initialize(request, session) + def initialize(request, session, response) + @response = response @request = request @session = session @http_user = nil @@ -40,9 +45,8 @@ def extract_user if proxy_mode? extract_proxy_user else - extract_basic_auth_user - - @http_user = User.find_with_credentials @login, @passwd if @login + extract_auth_user + @http_user = User.find_with_credentials @login, @passwd if @login && @passwd end if !@http_user && session[:login] @@ -80,6 +84,65 @@ def require_admin private + def initialize_krb_session + principal = CONFIG['kerberos_service_principal'] + + unless CONFIG['kerberos_realm'] + CONFIG['kerberos_realm'] = principal.rpartition("@")[2] + end + + krb = GSSAPI::Simple.new( + principal.partition("/")[2].rpartition("@")[0], + principal.partition("/")[0], + CONFIG['kerberos_keytab'] || "/etc/krb5.keytab" + ) + krb.acquire_credentials + + return krb + end + + def extract_krb_user(authorization) + unless authorization[1] + Rails.logger.debug "Didn't receive any negotiation data." + @response.headers["WWW-Authenticate"] = authorization.join(' ') + raise AuthenticationRequiredError.new "GSSAPI negotiation failed." + end + + begin + krb = initialize_krb_session + + begin + tok = krb.accept_context(Base64.strict_decode64(authorization[1])) + rescue GSSAPI::GssApiError + @response.headers["WWW-Authenticate"] = authorization.join(' ') + raise AuthenticationRequiredError.new "Received invalid GSSAPI context." + end + + unless krb.display_name.match("@#{CONFIG['kerberos_realm']}$") + @response.headers["WWW-Authenticate"] = authorization.join(' ') + raise AuthenticationRequiredError.new "User authenticated in wrong Kerberos realm." + end + + unless tok == true + tok = Base64.strict_encode64(tok) + @response.headers["WWW-Authenticate"] = "Negotiate #{tok}" + end + + @login = krb.display_name.partition("@")[0] + @http_user = User.find_by_login @login + raise AuthenticationRequiredError.new "User '#{@login}' has no account on the server." unless @http_user + rescue GSSAPI::GssApiError => err + raise AuthenticationRequiredError.new, "Received a GSSAPI exception; #{err.message}." + end + end + + def extract_basic_user(authorization) + @login, @passwd = Base64.decode64(authorization[1]).split(':', 2)[0..1] + + # set password to the empty string in case no password is transmitted in the auth string + @passwd ||= "" + end + def extract_proxy_user proxy_user = request.env['HTTP_X_USERNAME'] if proxy_user @@ -115,19 +178,21 @@ def extract_proxy_user end end - def extract_basic_auth_user + def extract_auth_user authorization = authorization_infos - # privacy! Rails.logger.debug( "AUTH: #{authorization.inspect}" ) - - if authorization && authorization[0] == "Basic" - # Rails.logger.debug( "AUTH2: #{authorization}" ) - @login, @passwd = Base64.decode64(authorization[1]).split(':', 2)[0..1] - - # set password to the empty string in case no password is transmitted in the auth string - @passwd ||= "" + # privacy! logger.debug( "AUTH: #{authorization.inspect}" ) + if authorization + # logger.debug( "AUTH2: #{authorization}" ) + if authorization[0] == "Basic" + extract_basic_user authorization + elsif authorization[0] == "Negotiate" && CONFIG['kerberos_service_principal'] + extract_krb_user authorization + else + Rails.logger.debug "Unsupported authentication string '#{authorization[0]}' received." + end else - Rails.logger.debug "no authentication string was sent" + Rails.logger.debug "No authentication string was received." end end