Commit d062a5cc authored by Nick Thomas's avatar Nick Thomas

Automatically link kerberos users to LDAP people

parent d2a55fc0
---
title: Automatically link kerberos users to LDAP people
merge_request: 2405
author:
...@@ -105,6 +105,24 @@ user associated with the email, which is built from the Kerberos username and ...@@ -105,6 +105,24 @@ user associated with the email, which is built from the Kerberos username and
realm. User accounts will be created automatically when authentication was realm. User accounts will be created automatically when authentication was
successful. successful.
## Linking Kerberos and LDAP accounts together
If your users log in with Kerberos, but you also have [LDAP integration](../administration/auth/ldap.md)
enabled, then your users will be automatically linked to their LDAP accounts on
first login. For this to work, some prerequisites must be met:
The Kerberos username must match the LDAP user's UID. You can choose which LDAP
attribute is used as the UID in GitLab's [LDAP configuration](../administration/auth/ldap.md#configuration)
but for Active Directory, this should be `sAMAccountName`.
The Kerberos realm must match the domain part of the LDAP user's Distinguished
Name. For instance, if the Kerberos realm is `AD.EXAMPLE.COM`, then the LDAP
user's Distinguished Name should end in `dc=ad,dc=example,dc=com`.
Taken together, these rules mean that linking will only work if your users'
Kerberos usernames are of the form `foo@AD.EXAMPLE.COM` and their
LDAP Distinguished Names look like `sAMAccountName=foo,dc=ad,dc=example,dc=com`.
## HTTP Git access ## HTTP Git access
A linked Kerberos account enables you to `git pull` and `git push` using your A linked Kerberos account enables you to `git pull` and `git push` using your
......
require 'net/ldap/dn'
module EE module EE
module Gitlab module Gitlab
module LDAP module LDAP
...@@ -15,6 +17,31 @@ module EE ...@@ -15,6 +17,31 @@ module EE
nil nil
end end
def find_by_kerberos_principal(principal, adapter)
uid, domain = principal.split('@', 2)
return nil unless uid && domain
# In multi-forest setups, there may be several users with matching
# uids but differing DNs, so skip adapters configured to connect to
# non-matching domains
return unless domain.casecmp(domain_from_dn(adapter.config.base)) == 0
find_by_uid(uid, adapter)
end
# Extracts the rightmost unbroken set of domain components from an
# LDAP DN and constructs a domain name from them
def domain_from_dn(dn)
dn_components = []
Net::LDAP::DN.new(dn).each_pair { |name, value| dn_components << { name: name, value: value } }
dn_components
.reverse
.take_while { |rdn| rdn[:name].casecmp('DC').zero? } # Domain Component
.map { |rdn| rdn[:value] }
.reverse
.join('.')
end
end end
def ssh_keys def ssh_keys
...@@ -27,23 +54,12 @@ module EE ...@@ -27,23 +54,12 @@ module EE
end end
end end
# We assume that the Kerberos username matches the configured uid
# attribute in LDAP. For Active Directory, this is `sAMAccountName`
def kerberos_principal def kerberos_principal
# The following is only meaningful for Active Directory return nil unless uid
return unless entry.respond_to?(:sAMAccountName)
entry[:sAMAccountName].first + '@' + windows_domain_name.upcase
end
def windows_domain_name uid + '@' + self.class.domain_from_dn(dn).upcase
# The following is only meaningful for Active Directory
require 'net/ldap/dn'
dn_components = []
Net::LDAP::DN.new(dn).each_pair { |name, value| dn_components << { name: name, value: value } }
dn_components
.reverse
.take_while { |rdn| rdn[:name].casecmp('DC').zero? } # Domain Component
.map { |rdn| rdn[:value] }
.reverse
.join('.')
end end
def memberof def memberof
......
...@@ -6,24 +6,20 @@ module EE ...@@ -6,24 +6,20 @@ module EE
::Gitlab::Kerberos::Authentication.kerberos_default_realm ::Gitlab::Kerberos::Authentication.kerberos_default_realm
end end
# 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.
def kerberos_normalized_uid
@kerberos_normalized_uid ||=
begin
uid = ::Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
uid += '@' + kerberos_default_realm unless uid.include?('@')
uid
end
end
def uid def uid
if provider == 'kerberos' return @ee_uid if defined?(@ee_uid)
kerberos_normalized_uid
else ee_uid = super
super
# 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, so add the default
# realm ourselves if appropriate
if provider == 'kerberos' && ee_uid.present?
ee_uid += "@#{kerberos_default_realm}" unless ee_uid.include?('@')
end end
@ee_uid = ee_uid
end end
end end
end end
......
module EE
module Gitlab
module OAuth
module User
protected
def find_ldap_person(auth_hash, adapter)
if auth_hash.provider == 'kerberos'
::Gitlab::LDAP::Person.find_by_kerberos_principal(auth_hash.uid, adapter)
else
super
end
end
end
end
end
end
...@@ -8,6 +8,8 @@ module Gitlab ...@@ -8,6 +8,8 @@ module Gitlab
SignupDisabledError = Class.new(StandardError) SignupDisabledError = Class.new(StandardError)
class User class User
prepend ::EE::Gitlab::OAuth::User
attr_accessor :auth_hash, :gl_user attr_accessor :auth_hash, :gl_user
def initialize(auth_hash) def initialize(auth_hash)
...@@ -101,14 +103,18 @@ module Gitlab ...@@ -101,14 +103,18 @@ module Gitlab
# Look for a corresponding person with same uid in any of the configured LDAP providers # Look for a corresponding person with same uid in any of the configured LDAP providers
Gitlab::LDAP::Config.providers.each do |provider| Gitlab::LDAP::Config.providers.each do |provider|
adapter = Gitlab::LDAP::Adapter.new(provider) adapter = Gitlab::LDAP::Adapter.new(provider)
@ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) @ldap_person = find_ldap_person(auth_hash, adapter)
# The `uid` might actually be a DN. Try it next.
@ldap_person ||= Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
break if @ldap_person break if @ldap_person
end end
@ldap_person @ldap_person
end end
def find_ldap_person(auth_hash, adapter)
by_uid = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter)
# The `uid` might actually be a DN. Try it next.
by_uid || Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
end
def ldap_config def ldap_config
Gitlab::LDAP::Config.new(ldap_person.provider) if ldap_person Gitlab::LDAP::Config.new(ldap_person.provider) if ldap_person
end end
......
...@@ -29,32 +29,56 @@ describe Gitlab::LDAP::Person do ...@@ -29,32 +29,56 @@ describe Gitlab::LDAP::Person do
end end
end end
describe '#kerberos_principal' do describe '.find_by_kerberos_principal' do
let(:entry) do let(:adapter) { ldap_adapter }
ldif = "dn: cn=foo, dc=bar, dc=com\n" let(:username) { 'foo' }
ldif += "sAMAccountName: #{sam_account_name}\n" if sam_account_name let(:principal) { username + '@' + kerberos_realm }
Net::LDAP::Entry.from_single_ldif_string(ldif) let(:ldap_server) { 'ad.example.com' }
subject { described_class.find_by_kerberos_principal(principal, adapter) }
before do
stub_ldap_config(uid: 'sAMAccountName', base: 'ou=foo,dc=' + ldap_server.gsub('.', ',dc='))
end end
subject { described_class.new(entry, 'ldapmain') } context 'LDAP server is not for kerberos realm' do
let(:kerberos_realm) { 'kerberos.example.com' }
context 'when sAMAccountName is not defined (non-AD LDAP server)' do it 'returns nil without searching' do
let(:sam_account_name) { nil } expect(adapter).not_to receive(:user)
it 'returns nil' do is_expected.to be_nil
expect(subject.kerberos_principal).to be_nil
end end
end end
context 'when sAMAccountName is defined (AD server)' do context 'LDAP server is for kerberos realm' do
let(:sam_account_name) { 'mylogin' } let(:kerberos_realm) { ldap_server }
it 'searches by configured uid attribute' do
expect(adapter).to receive(:user).with('sAMAccountName', username).and_return(:fake_user)
it 'returns the principal combining sAMAccountName and DC components of the distinguishedName' do is_expected.to eq(:fake_user)
expect(subject.kerberos_principal).to eq('mylogin@BAR.COM')
end end
end end
end end
describe '#kerberos_principal' do
let(:entry) do
ldif = "dn: cn=foo, dc=bar, dc=com\nsAMAccountName: myName\n"
Net::LDAP::Entry.from_single_ldif_string(ldif)
end
subject { described_class.new(entry, 'ldapmain') }
before do
stub_ldap_config(uid: 'sAMAccountName')
end
it 'returns the principal combining the configured UID and DC components of the distinguishedName' do
expect(subject.kerberos_principal).to eq('myName@BAR.COM')
end
end
describe '#ssh_keys' do describe '#ssh_keys' do
let(:ssh_key) { 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrSQHff6a1rMqBdHFt+FwIbytMZ+hJKN3KLkTtOWtSvNIriGhnTdn4rs+tjD/w+z+revytyWnMDM9dS7J8vQi006B16+hc9Xf82crqRoPRDnBytgAFFQY1G/55ql2zdfsC5yvpDOFzuwIJq5dNGsojS82t6HNmmKPq130fzsenFnj5v1pl3OJvk513oduUyKiZBGTroWTn7H/eOPtu7s9MD7pAdEjqYKFLeaKmyidiLmLqQlCRj3Tl2U9oyFg4PYNc0bL5FZJ/Z6t0Ds3i/a2RanQiKxrvgu3GSnUKMx7WIX373baL4jeM7cprRGiOY/1NcS+1cAjfJ8oaxQF/1dYj' } let(:ssh_key) { 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrSQHff6a1rMqBdHFt+FwIbytMZ+hJKN3KLkTtOWtSvNIriGhnTdn4rs+tjD/w+z+revytyWnMDM9dS7J8vQi006B16+hc9Xf82crqRoPRDnBytgAFFQY1G/55ql2zdfsC5yvpDOFzuwIJq5dNGsojS82t6HNmmKPq130fzsenFnj5v1pl3OJvk513oduUyKiZBGTroWTn7H/eOPtu7s9MD7pAdEjqYKFLeaKmyidiLmLqQlCRj3Tl2U9oyFg4PYNc0bL5FZJ/Z6t0Ds3i/a2RanQiKxrvgu3GSnUKMx7WIX373baL4jeM7cprRGiOY/1NcS+1cAjfJ8oaxQF/1dYj' }
let(:ssh_key_attribute_name) { 'altSecurityIdentities' } let(:ssh_key_attribute_name) { 'altSecurityIdentities' }
......
require 'spec_helper'
describe Gitlab::OAuth::User, lib: true do
include LdapHelpers
describe 'login through kerberos with linkable LDAP user' do
let(:uid) { 'foo' }
let(:provider) { 'kerberos' }
let(:realm) { 'ad.example.com' }
let(:base_dn) { 'ou=users,dc=ad,dc=example,dc=com' }
let(:info_hash) { { email: uid + '@' + realm, username: uid } }
let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:oauth_user) { Gitlab::OAuth::User.new(auth_hash) }
let(:real_email) { 'myname@example.com' }
before do
allow(::Gitlab::Kerberos::Authentication).to receive(:kerberos_default_realm).and_return(realm)
allow(Gitlab.config.omniauth).to receive_messages(auto_link_ldap_user: true, allow_single_sign_on: ['kerberos'])
stub_ldap_config(base: base_dn)
ldap_entry = Net::LDAP::Entry.new("uid=#{uid}," + base_dn).tap do |entry|
entry['uid'] = uid
entry['email'] = real_email
end
stub_ldap_person_find_by_uid(uid, ldap_entry)
oauth_user.save
end
it 'links the LDAP person to the GitLab user' do
gl_user = oauth_user.gl_user
identities = gl_user.identities.map do |identity|
{ provider: identity.provider, extern_uid: identity.extern_uid }
end
expect(identities).to contain_exactly(
{ provider: 'ldapmain', extern_uid: "uid=#{uid},#{base_dn}" },
{ provider: 'kerberos', extern_uid: uid + '@' + realm }
)
expect(gl_user.email).to eq(real_email)
end
end
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