Commit 6e87c3f9 authored by Alex Lossent's avatar Alex Lossent

Canonicalization of Kerberos identities to always include realm

This resolve inconsistences in Kerberos identities, as sometimes
the realm would be included and sometimes not. Cf. gitlab-org/gitlab-ee#41
parent baa733c6
...@@ -18,6 +18,7 @@ v 8.2.2 ...@@ -18,6 +18,7 @@ v 8.2.2
v 8.2.1 v 8.2.1
- Forcefully update builds that didn't want to update with state machine - Forcefully update builds that didn't want to update with state machine
- Fix: saving GitLabCiService as Admin Template - Fix: saving GitLabCiService as Admin Template
- Canonicalization of Kerberos identities to always include realm (Alex Lossent)
v 8.2.0 v 8.2.0
- Invalidate stored jira password if the endpoint URL is changed - Invalidate stored jira password if the endpoint URL is changed
...@@ -46,6 +47,8 @@ v 8.1.0 ...@@ -46,6 +47,8 @@ v 8.1.0
- Added an issues template (Hannes Rosenögger) - Added an issues template (Hannes Rosenögger)
- Add documentation for "Share project with group" API call - Add documentation for "Share project with group" API call
- Ability to disable 'Share with Group' feature (via UI and API) - Ability to disable 'Share with Group' feature (via UI and API)
- Add documentation for "Share project with group" API call
- Abiliy to disable 'Share with Group' feature (via UI and API)
v 8.0.6 v 8.0.6
- No EE-specific changes - No EE-specific changes
......
class CanonicalizeKerberosIdentities < ActiveRecord::Migration
# This migration can be performed online without errors.
# It makes sure that all Kerberos identities are in canonical form
# with a realm name (`username` => `username@DEFAULT.REALM`).
# Before this migration, Kerberos identities using the default realm are typically stored
# without the realm part.
def kerberos_default_realm
@kerberos_default_realm ||= begin
require "krb5_auth"
krb5 = ::Krb5Auth::Krb5.new
default_realm = krb5.get_default_realm
krb5.close # release memory allocated by the krb5 library
default_realm || ''
rescue Exception
'' # could not find the system's default realm, maybe there's no Kerberos at all
end
end
def change
reversible do |dir|
return unless kerberos_default_realm.present?
dir.up do
# add the default realm to any kerberos identity not having a realm already
execute("UPDATE identities SET extern_uid = CONCAT(extern_uid, '@#{quote_string(kerberos_default_realm)}')
WHERE provider = 'kerberos' AND extern_uid NOT LIKE '%@%'")
end
dir.down do
# remove the realm from kerberos identities using the default realm
execute("UPDATE identities SET extern_uid = REPLACE(extern_uid, '@#{quote_string(kerberos_default_realm)}', '')
WHERE provider = 'kerberos' AND extern_uid LIKE '%@#{quote_string(kerberos_default_realm)}'")
end
end
end
end
...@@ -5,7 +5,7 @@ module Grack ...@@ -5,7 +5,7 @@ module Grack
def self.call(env) def self.call(env)
# Avoid issues with instance variables in Grack::Auth persisting across # Avoid issues with instance variables in Grack::Auth persisting across
# requests by creating a new instance for each request. # requests by creating a new instance for each request.
Auth.new({}).call(env) Auth.new({}).call_with_kerberos_support(env)
end end
end end
...@@ -13,6 +13,11 @@ module Grack ...@@ -13,6 +13,11 @@ module Grack
attr_accessor :user, :project, :env attr_accessor :user, :project, :env
def call_with_kerberos_support(env)
# Make sure the final leg of Kerberos authentication is applied as per RFC4559
apply_negotiate_final_leg(call(env))
end
def call(env) def call(env)
@env = env @env = env
@request = Rack::Request.new(env) @request = Rack::Request.new(env)
...@@ -42,7 +47,7 @@ module Grack ...@@ -42,7 +47,7 @@ module Grack
elsif @user.nil? && !@ci elsif @user.nil? && !@ci
unauthorized unauthorized
else else
apply_negotiate_final_leg(render_not_found) render_not_found
end end
end end
...@@ -99,8 +104,7 @@ module Grack ...@@ -99,8 +104,7 @@ module Grack
return unless krb_principal return unless krb_principal
# Set @user if authentication succeeded # Set @user if authentication succeeded
identity = ::Identity.find_by(provider: 'kerberos', extern_uid: krb_principal) identity = ::Identity.find_by(provider: :kerberos, extern_uid: krb_principal)
identity ||= ::Identity.find_by(provider: 'kerberos', extern_uid: krb_principal.split("@")[0])
@user = identity.user if identity @user = identity.user if identity
else else
# Authentication with username and password # Authentication with username and password
......
...@@ -4,6 +4,13 @@ require "krb5_auth" ...@@ -4,6 +4,13 @@ require "krb5_auth"
module Gitlab module Gitlab
module Kerberos module Kerberos
class Authentication class Authentication
def self.kerberos_default_realm
krb5 = ::Krb5Auth::Krb5.new
default_realm = krb5.get_default_realm
krb5.close # release memory allocated by the krb5 library
default_realm
end
def self.login(login, password) def self.login(login, password)
return unless Devise.omniauth_providers.include?(:kerberos) return unless Devise.omniauth_providers.include?(:kerberos)
return unless login.present? && password.present? return unless login.present? && password.present?
...@@ -25,15 +32,14 @@ module Gitlab ...@@ -25,15 +32,14 @@ module Gitlab
end end
def login def login
valid? && find_by_login(@login) # get_default_principal consistently returns the canonical Kerberos principal name, with realm
valid? && find_by_login(@krb5.get_default_principal)
end end
private private
def find_by_login(login) def find_by_login(login)
identity = ::Identity. identity = ::Identity.find_by(provider: :kerberos, extern_uid: login)
where(provider: :kerberos).
where('lower(extern_uid) = ?', login).last
identity && identity.user identity && identity.user
end end
end end
......
...@@ -8,8 +8,21 @@ module Gitlab ...@@ -8,8 +8,21 @@ module Gitlab
@auth_hash = auth_hash @auth_hash = auth_hash
end end
def kerberos_default_realm
Gitlab::Kerberos::Authentication.kerberos_default_realm
end
def normalized_uid
return auth_hash.uid.to_s unless provider == 'kerberos'
# For Kerberos, usernames `principal` and `principal@DEFAULT.REALM` are equivalent and
# may be used indifferently, but omniauth_kerberos does not normalize them as of version 0.3.0.
# Normalize here the uid to always have the canonical Kerberos principal name with realm.
return auth_hash.uid if auth_hash.uid.include?("@")
auth_hash.uid + "@" + kerberos_default_realm
end
def uid def uid
@uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s) @uid ||= Gitlab::Utils.force_utf8(normalized_uid)
end end
def provider def provider
......
...@@ -5,7 +5,6 @@ describe Grack::Auth do ...@@ -5,7 +5,6 @@ describe Grack::Auth do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:app) { lambda { |env| [200, {}, "Success!"] } } let(:app) { lambda { |env| [200, {}, "Success!"] } }
let!(:auth) { Grack::Auth.new(app) }
let(:env) do let(:env) do
{ {
'rack.input' => '', 'rack.input' => '',
...@@ -13,7 +12,7 @@ describe Grack::Auth do ...@@ -13,7 +12,7 @@ describe Grack::Auth do
'QUERY_STRING' => 'service=git-upload-pack' 'QUERY_STRING' => 'service=git-upload-pack'
} }
end end
let(:status) { auth.call(env).first } let(:status) { Grack::AuthSpawner::call(env).first }
describe "#call" do describe "#call" do
context "when the project doesn't exist" do context "when the project doesn't exist" do
...@@ -58,7 +57,7 @@ describe Grack::Auth do ...@@ -58,7 +57,7 @@ describe Grack::Auth do
end end
it "responds with the right project" do it "responds with the right project" do
response = auth.call(env) response = Grack::AuthSpawner::call(env)
json_body = ActiveSupport::JSON.decode(response[2][0]) json_body = ActiveSupport::JSON.decode(response[2][0])
expect(response.first).to eq(200) expect(response.first).to eq(200)
...@@ -92,6 +91,83 @@ describe Grack::Auth do ...@@ -92,6 +91,83 @@ describe Grack::Auth do
end end
end end
context "when Kerberos token is provided" do
before do
allow_any_instance_of(Grack::Auth).to receive(:allow_kerberos_auth?).and_return(true)
env["HTTP_AUTHORIZATION"] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
end
shared_examples "RFC4559 compliance" do
it "complies with RFC4559" do
allow_any_instance_of(Grack::Auth::Request).to receive(:spnego_response_token).and_return("opaque_response_token")
headers = Grack::AuthSpawner::call(env)[1]
expect(headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}")
end
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)
end
it "responds with status 401" do
expect(status).to eq(401)
end
end
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")
end
it "responds with status 401" do
expect(status).to eq(401)
end
end
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
end
context "when the user has access to the project" do
before do
project.team << [user, :master]
end
context "when the user is blocked" do
before do
user.block
project.team << [user, :master]
end
it "responds with status 404" do
expect(status).to eq(404)
end
end
context "when the user isn't blocked" do
it "responds with status 200" do
expect(status).to eq(200)
end
end
include_examples "RFC4559 compliance"
end
context "when the user doesn't have access to the project" do
it "responds with status 404" do
expect(status).to eq(404)
end
include_examples "RFC4559 compliance"
end
end
end
context "when username and password are provided" do context "when username and password are provided" do
context "when authentication fails" do context "when authentication fails" do
before do before do
...@@ -162,8 +238,7 @@ describe Grack::Auth do ...@@ -162,8 +238,7 @@ describe Grack::Auth do
def attempt_login(include_password) def attempt_login(include_password)
password = include_password ? user.password : "" password = include_password ? user.password : ""
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password) env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password)
Grack::Auth.new(app) Grack::AuthSpawner::call(env).first
auth.call(env).first
end end
it "repeated attempts followed by successful attempt" do it "repeated attempts followed by successful attempt" do
......
...@@ -2,26 +2,39 @@ require 'spec_helper' ...@@ -2,26 +2,39 @@ require 'spec_helper'
describe Gitlab::Kerberos::Authentication do describe Gitlab::Kerberos::Authentication do
let(:klass) { Gitlab::Kerberos::Authentication } let(:klass) { Gitlab::Kerberos::Authentication }
let(:user) { create(:omniauth_user, provider: :kerberos, extern_uid: 'gitlab') } let(:user) { create(:omniauth_user, provider: :kerberos, extern_uid: 'gitlab@FOO.COM') }
let(:login) { 'john' } let(:login) { 'john' }
let(:password) { 'password' } let(:password) { 'password' }
describe :kerberos_default_realm do
it "returns the default realm exposed by the Kerberos library" do
allow_any_instance_of(::Krb5Auth::Krb5).to receive_messages(get_default_realm: "FOO.COM")
expect(klass.kerberos_default_realm).to eq("FOO.COM")
end
end
describe :login do describe :login do
before do before do
allow(Devise).to receive_messages(omniauth_providers: [:kerberos]) allow(Devise).to receive_messages(omniauth_providers: [:kerberos])
user # make sure user is instanciated
end end
it "finds the user if authentication is successful" do it "finds the user if authentication is successful (login without kerberos realm)" do
kerberos_realm = user.email.sub(/.*@/, '') allow_any_instance_of(::Krb5Auth::Krb5).to receive_messages(get_init_creds_password: true, get_default_principal: 'gitlab@FOO.COM')
allow_any_instance_of(::Krb5Auth::Krb5).to receive_messages(get_init_creds_password: true, get_default_realm: kerberos_realm)
expect(klass.login('gitlab', password)).to be_truthy expect(klass.login('gitlab', password)).to be_truthy
end end
it "finds the user if authentication is successful (login with a kerberos realm)" do
allow_any_instance_of(::Krb5Auth::Krb5).to receive_messages(get_init_creds_password: true, get_default_principal: 'gitlab@FOO.COM')
expect(klass.login('gitlab@FOO.COM', password)).to be_truthy
end
it "returns false if there is no such user in kerberos" do it "returns false if there is no such user in kerberos" do
kerberos_login = "some-login" kerberos_login = "some-login"
kerberos_realm = user.email.sub(/.*@/, '') allow_any_instance_of(::Krb5Auth::Krb5).to receive_messages(get_init_creds_password: true, get_default_principal: 'some-login@FOO.COM')
allow_any_instance_of(::Krb5Auth::Krb5).to receive_messages(get_init_creds_password: true, get_default_realm: kerberos_realm)
expect(klass.login(kerberos_login, password)).to be_falsy expect(klass.login(kerberos_login, password)).to be_falsy
end end
......
...@@ -54,6 +54,29 @@ describe Gitlab::OAuth::AuthHash do ...@@ -54,6 +54,29 @@ describe Gitlab::OAuth::AuthHash do
it { expect(auth_hash.password).not_to be_empty } it { expect(auth_hash.password).not_to be_empty }
end end
context 'with kerberos provider' do
let(:provider_ascii) { 'kerberos'.force_encoding(Encoding::ASCII_8BIT) }
context "and uid contains a kerberos realm" do
let(:uid_ascii) { 'mylogin@BAR.COM'.force_encoding(Encoding::ASCII_8BIT) }
it "preserves the canonical uid" do
expect(auth_hash.uid).to eq('mylogin@BAR.COM')
end
end
context "and uid does not contain a kerberos realm" do
let(:uid_ascii) { 'mylogin'.force_encoding(Encoding::ASCII_8BIT) }
before do
allow(Gitlab::Kerberos::Authentication).to receive(:kerberos_default_realm).and_return("FOO.COM")
end
it "canonicalizes uid with kerberos realm" do
expect(auth_hash.uid).to eq('mylogin@FOO.COM')
end
end
end
context 'email not provided' do context 'email not provided' do
before { info_hash.delete(:email) } before { info_hash.delete(:email) }
......
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