Commit 10f1a1f2 authored by Bob Van Landuyt's avatar Bob Van Landuyt Committed by Drew Blessing

Add a user's memberships when logging in

parent 1552a182
...@@ -34,6 +34,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -34,6 +34,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
prompt_for_two_factor(@user) prompt_for_two_factor(@user)
else else
log_audit_event(@user, with: :ldap) log_audit_event(@user, with: :ldap)
flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\
'Refresh the page to see the changes.'
sign_in_and_redirect(@user) sign_in_and_redirect(@user)
end end
else else
......
class LdapAllGroupsSyncWorker
include Sidekiq::Worker
include CronjobQueue
def perform
logger.info 'Started LDAP group sync'
EE::Gitlab::LDAP::Sync::Groups.execute
logger.info 'Finished LDAP group sync'
end
end
class LdapGroupSyncWorker class LdapGroupSyncWorker
include Sidekiq::Worker include Sidekiq::Worker
include CronjobQueue include DedicatedSidekiqQueue
def perform(group_id = nil) def perform(group_ids, provider = nil)
if group_id groups = Group.where(id: Array(group_ids))
group = Group.find_by(id: group_id)
unless group if provider
logger.warn "Could not find group #{group_id} for LDAP group sync" EE::Gitlab::LDAP::Sync::Proxy.open(provider) do |proxy|
return sync_groups(groups, proxy: proxy)
end end
else
sync_groups(groups)
end
end
logger.info "Started LDAP group sync for group #{group.name} (#{group.id})" def sync_groups(groups, proxy: nil)
EE::Gitlab::LDAP::Sync::Group.execute_all_providers(group) groups.each { |group| sync_group(group, proxy: proxy) }
logger.info "Finished LDAP group sync for group #{group.name} (#{group.id})" end
def sync_group(group, proxy: nil)
logger.info "Started LDAP group sync for group #{group.name} (#{group.id})"
if proxy
EE::Gitlab::LDAP::Sync::Group.execute(group, proxy)
else else
logger.info 'Started LDAP group sync' EE::Gitlab::LDAP::Sync::Group.execute_all_providers(group)
EE::Gitlab::LDAP::Sync::Groups.execute
logger.info 'Finished LDAP group sync'
end end
logger.info "Finished LDAP group sync for group #{group.name} (#{group.id})"
end end
end end
---
title: Add a user's memberships when logging in through LDAP
merge_request: 1819
author:
...@@ -392,7 +392,7 @@ Settings.cron_jobs['ldap_sync_worker']['cron'] ||= '30 1 * * *' ...@@ -392,7 +392,7 @@ Settings.cron_jobs['ldap_sync_worker']['cron'] ||= '30 1 * * *'
Settings.cron_jobs['ldap_sync_worker']['job_class'] = 'LdapSyncWorker' Settings.cron_jobs['ldap_sync_worker']['job_class'] = 'LdapSyncWorker'
Settings.cron_jobs['ldap_group_sync_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['ldap_group_sync_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ldap_group_sync_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['ldap_group_sync_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['ldap_group_sync_worker']['job_class'] = 'LdapGroupSyncWorker' Settings.cron_jobs['ldap_group_sync_worker']['job_class'] = 'LdapAllGroupsSyncWorker'
Settings.cron_jobs['geo_bulk_notify_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['geo_bulk_notify_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_bulk_notify_worker']['cron'] ||= '*/10 * * * * *' Settings.cron_jobs['geo_bulk_notify_worker']['cron'] ||= '*/10 * * * * *'
Settings.cron_jobs['geo_bulk_notify_worker']['job_class'] ||= 'GeoBulkNotifyWorker' Settings.cron_jobs['geo_bulk_notify_worker']['job_class'] ||= 'GeoBulkNotifyWorker'
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
- [update_user_activity, 1] - [update_user_activity, 1]
- [propagate_service_template, 1] - [propagate_service_template, 1]
# EE specific queues # EE specific queues
- [ldap_group_sync, 2]
- [geo, 1] - [geo, 1]
- [project_mirror, 1] - [project_mirror, 1]
- [project_update_repository_storage, 1] - [project_update_repository_storage, 1]
......
...@@ -29,6 +29,11 @@ The process will also update the following user information: ...@@ -29,6 +29,11 @@ The process will also update the following user information:
## Group Sync ## Group Sync
If your LDAP supports the `memberof` property, GitLab will add the user to any
new groups they might be added to when the user logs in. That way they don't need
to wait for the hourly sync to be granted access to the groups that they are in
in LDAP.
If `group_base` is set in LDAP configuration, a group sync process will run If `group_base` is set in LDAP configuration, a group sync process will run
every hour, on the hour. This allows GitLab group membership to be automatically every hour, on the hour. This allows GitLab group membership to be automatically
updated based on LDAP group members. updated based on LDAP group members.
......
...@@ -89,6 +89,7 @@ module Gitlab ...@@ -89,6 +89,7 @@ module Gitlab
def update_user def update_user
update_email update_email
update_memberships
update_ssh_keys if sync_ssh_keys? update_ssh_keys if sync_ssh_keys?
update_kerberos_identity if import_kerberos_identities? update_kerberos_identity if import_kerberos_identities?
end end
...@@ -162,6 +163,16 @@ module Gitlab ...@@ -162,6 +163,16 @@ module Gitlab
ldap_config.active_directory && (Gitlab.config.kerberos.enabled || AuthHelper.kerberos_enabled? ) ldap_config.active_directory && (Gitlab.config.kerberos.enabled || AuthHelper.kerberos_enabled? )
end end
def update_memberships
return if ldap_user.nil? || ldap_user.group_cns.empty?
group_ids = LdapGroupLink.where(cn: ldap_user.group_cns, provider: provider)
.distinct(:group_id)
.pluck(:group_id)
LdapGroupSyncWorker.perform_async(group_ids, provider) if group_ids.any?
end
private private
def logger def logger
......
...@@ -107,7 +107,7 @@ module Gitlab ...@@ -107,7 +107,7 @@ module Gitlab
end end
def user_attributes def user_attributes
%W(#{config.uid} cn mail dn) %W(#{config.uid} cn mail dn memberof)
end end
end end
end end
......
...@@ -45,6 +45,22 @@ module Gitlab ...@@ -45,6 +45,22 @@ module Gitlab
attribute_value(:email) attribute_value(:email)
end end
def memberof
return [] unless entry.attribute_names.include?(:memberof)
entry.memberof
end
def group_cns
memberof.map { |memberof_value| cn_from_memberof(memberof_value) }
end
def cn_from_memberof(memberof)
# Only get the first CN value of the string, that's the one that contains
# the group name
memberof.match(/(?:cn=([\w\s]+))/i)&.captures&.first
end
delegate :dn, to: :entry delegate :dn, to: :entry
private private
......
...@@ -43,7 +43,7 @@ describe Gitlab::LDAP::Adapter, lib: true do ...@@ -43,7 +43,7 @@ describe Gitlab::LDAP::Adapter, lib: true do
describe '#user_attributes' do describe '#user_attributes' do
it 'appends EE-specific attributes' do it 'appends EE-specific attributes' do
stub_ldap_config(uid: 'uid', sync_ssh_keys: 'sshPublicKey') stub_ldap_config(uid: 'uid', sync_ssh_keys: 'sshPublicKey')
expect(adapter.user_attributes).to match_array(%w(uid dn cn mail sshPublicKey)) expect(adapter.user_attributes).to match_array(%w(uid dn cn mail sshPublicKey memberof))
end end
end end
end end
...@@ -65,10 +65,11 @@ describe EE::Gitlab::LDAP::Sync::Group, lib: true do ...@@ -65,10 +65,11 @@ describe EE::Gitlab::LDAP::Sync::Group, lib: true do
end end
before do before do
allow(Gitlab::LDAP::Config) stub_ldap_config(providers: %w[main secundary])
.to receive(:providers).and_return(%w(main secondary))
allow(EE::Gitlab::LDAP::Sync::Proxy) adapter = ldap_adapter('main')
.to receive(:open).and_yield(double('proxy').as_null_object) proxy = proxy(adapter, 'main')
allow(EE::Gitlab::LDAP::Sync::Proxy).to receive(:open).and_yield(proxy)
end end
let(:group) do let(:group) do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::LDAP::Access, lib: true do describe Gitlab::LDAP::Access, lib: true do
include LdapHelpers
let(:access) { Gitlab::LDAP::Access.new user } let(:access) { Gitlab::LDAP::Access.new user }
let(:user) { create(:omniauth_user) } let(:user) { create(:omniauth_user) }
...@@ -173,6 +174,12 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -173,6 +174,12 @@ describe Gitlab::LDAP::Access, lib: true do
subject subject
end end
it 'updates the group memberships' do
expect(access).to receive(:update_memberships).once
subject
end
it 'syncs ssh keys if enabled by configuration' do it 'syncs ssh keys if enabled by configuration' do
allow(access).to receive_messages(group_base: '', sync_ssh_keys?: true) allow(access).to receive_messages(group_base: '', sync_ssh_keys?: true)
expect(access).to receive(:update_ssh_keys).once expect(access).to receive(:update_ssh_keys).once
...@@ -305,4 +312,50 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -305,4 +312,50 @@ describe Gitlab::LDAP::Access, lib: true do
expect{ access.update_email }.to change(user, :email) expect{ access.update_email }.to change(user, :email)
end end
end end
describe '#update_memberships' do
let(:provider) { user.ldap_identity.provider }
let(:entry) { ldap_user_entry(user.ldap_identity.extern_uid) }
let(:person_with_memberof) do
entry['memberof'] = ['CN=Group1,CN=Users,DC=The dc,DC=com',
'CN=Group2,CN=Builtin,DC=The dc,DC=com']
Gitlab::LDAP::Person.new(entry, provider)
end
it 'triggers a sync for all groups found in `memberof`' do
group_link_1 = create(:ldap_group_link, cn: 'Group1', provider: provider)
group_link_2 = create(:ldap_group_link, cn: 'Group2', provider: provider)
group_ids = [group_link_1, group_link_2].map(&:group_id)
allow(access).to receive(:ldap_user).and_return(person_with_memberof)
expect(LdapGroupSyncWorker).to receive(:perform_async)
.with(group_ids, provider)
access.update_memberships
end
it "doesn't continue when there is no `memberOf` param" do
allow(access).to receive(:ldap_user)
.and_return(Gitlab::LDAP::Person.new(entry, provider))
expect(LdapGroupLink).not_to receive(:where)
expect(LdapGroupSyncWorker).not_to receive(:perform_async)
access.update_memberships
end
it "doesn't trigger a sync when there are no links for the provider" do
_another_provider = create(:ldap_group_link,
cn: 'Group1',
provider: 'not-this-ldap')
allow(access).to receive(:ldap_user).and_return(person_with_memberof)
expect(LdapGroupSyncWorker).not_to receive(:perform_async)
access.update_memberships
end
end
end end
...@@ -5,6 +5,11 @@ describe Gitlab::LDAP::Adapter, lib: true do ...@@ -5,6 +5,11 @@ describe Gitlab::LDAP::Adapter, lib: true do
let(:ldap) { double(:ldap) } let(:ldap) { double(:ldap) }
let(:adapter) { ldap_adapter('ldapmain', ldap) } let(:adapter) { ldap_adapter('ldapmain', ldap) }
let(:default_user_search_attributes) { }
def user_search_attributes(id_name)
[id_name, 'cn', 'mail', 'dn', 'memberof']
end
describe '#users' do describe '#users' do
before do before do
...@@ -16,7 +21,7 @@ describe Gitlab::LDAP::Adapter, lib: true do ...@@ -16,7 +21,7 @@ describe Gitlab::LDAP::Adapter, lib: true do
expect(adapter).to receive(:ldap_search) do |arg| expect(adapter).to receive(:ldap_search) do |arg|
expect(arg[:filter].to_s).to eq('(uid=johndoe)') expect(arg[:filter].to_s).to eq('(uid=johndoe)')
expect(arg[:base]).to eq('dc=example,dc=com') expect(arg[:base]).to eq('dc=example,dc=com')
expect(arg[:attributes]).to match(%w{uid cn mail dn}) expect(arg[:attributes]).to match(user_search_attributes('uid'))
end.and_return({}) end.and_return({})
adapter.users('uid', 'johndoe') adapter.users('uid', 'johndoe')
...@@ -26,7 +31,7 @@ describe Gitlab::LDAP::Adapter, lib: true do ...@@ -26,7 +31,7 @@ describe Gitlab::LDAP::Adapter, lib: true do
expect(adapter).to receive(:ldap_search).with( expect(adapter).to receive(:ldap_search).with(
base: 'uid=johndoe,ou=users,dc=example,dc=com', base: 'uid=johndoe,ou=users,dc=example,dc=com',
scope: Net::LDAP::SearchScope_BaseObject, scope: Net::LDAP::SearchScope_BaseObject,
attributes: %w{uid cn mail dn}, attributes: user_search_attributes('uid'),
filter: nil filter: nil
).and_return({}) ).and_return({})
...@@ -63,7 +68,7 @@ describe Gitlab::LDAP::Adapter, lib: true do ...@@ -63,7 +68,7 @@ describe Gitlab::LDAP::Adapter, lib: true do
it 'uses the right uid attribute when non-default' do it 'uses the right uid attribute when non-default' do
stub_ldap_config(uid: 'sAMAccountName') stub_ldap_config(uid: 'sAMAccountName')
expect(adapter).to receive(:ldap_search).with( expect(adapter).to receive(:ldap_search).with(
hash_including(attributes: %w{sAMAccountName cn mail dn}) hash_including(attributes: user_search_attributes('sAMAccountName'))
).and_return({}) ).and_return({})
adapter.users('sAMAccountName', 'johndoe') adapter.users('sAMAccountName', 'johndoe')
......
...@@ -43,4 +43,51 @@ describe Gitlab::LDAP::Person do ...@@ -43,4 +43,51 @@ describe Gitlab::LDAP::Person do
expect(person.email).to eq([user_principal_name]) expect(person.email).to eq([user_principal_name])
end end
end end
describe '#memberof' do
it 'returns an empty array if the field was not present' do
person = described_class.new(entry, 'ldapmain')
expect(person.memberof).to eq([])
end
it 'returns the values of `memberof` if the field was present' do
example_memberof = ['CN=Group Policy Creator Owners,CN=Users,DC=Vosmaer,DC=com',
'CN=Domain Admins,CN=Users,DC=Vosmaer,DC=com',
'CN=Enterprise Admins,CN=Users,DC=Vosmaer,DC=com',
'CN=Schema Admins,CN=Users,DC=Vosmaer,DC=com',
'CN=Administrators,CN=Builtin,DC=Vosmaer,DC=com']
entry['memberof'] = example_memberof
person = described_class.new(entry, 'ldapmain')
expect(person.memberof).to eq(example_memberof)
end
end
describe '#cn_from_memberof' do
it 'gets the group cn from the memberof value' do
person = described_class.new(entry, 'ldapmain')
expect(person.cn_from_memberof('cN=Group Policy Creator Owners,CN=Users,DC=Vosmaer,DC=com'))
.to eq('Group Policy Creator Owners')
end
it "doesn't break when there is no CN property" do
person = described_class.new(entry, 'ldapmain')
expect(person.cn_from_memberof('DC=Vosmaer,DC=com'))
.to be_nil
end
end
describe '#group_cns' do
it 'returns only CNs from the memberof values' do
example_memberof = ['CN=Group Policy Creator Owners,CN=Users,DC=Vosmaer,DC=com',
'CN=Administrators,CN=Builtin,DC=Vosmaer,DC=com']
entry['memberof'] = example_memberof
person = described_class.new(entry, 'ldapmain')
expect(person.group_cns).to eq(['Group Policy Creator Owners', 'Administrators'])
end
end
end end
require 'spec_helper'
describe LdapAllGroupsSyncWorker do
let(:subject) { described_class.new }
before do
allow(Sidekiq.logger).to receive(:info)
end
describe '#perform' do
it 'syncs all groups when group_id is nil' do
expect(EE::Gitlab::LDAP::Sync::Groups).to receive(:execute)
subject.perform
end
end
end
require 'spec_helper' require 'spec_helper'
describe LdapGroupSyncWorker do describe LdapGroupSyncWorker do
include LdapHelpers
let(:subject) { described_class.new }
let(:group) { create(:group) }
def expect_fake_proxy(provider)
fake = double
expect(EE::Gitlab::LDAP::Sync::Proxy)
.to receive(:open).with(provider).and_yield(fake)
fake
end
before do
allow(Sidekiq.logger).to receive(:info)
end
describe '#perform' do describe '#perform' do
it 'syncs all groups when group_id is nil' do it 'syncs a single group when group_id is present' do
expect(EE::Gitlab::LDAP::Sync::Groups).to receive(:execute) expect(subject).to receive(:sync_groups).with([group])
described_class.new.perform subject.perform(group.id)
end end
it 'syncs a single group when group_id is present' do it 'creates a proxy for syncing a single provider' do
group = create(:group) fake_proxy = expect_fake_proxy('the-provider')
expect(subject).to receive(:sync_groups).with([group], proxy: fake_proxy)
subject.perform(group.id, 'the-provider')
end
end
describe '#sync_groups' do
it 'syncs a group when it was found without a proxy' do
expect(subject).to receive(:sync_group).with(group, proxy: nil)
subject.sync_groups([group])
end
it 'syncs with an existing proxy when one was given' do
fake_proxy = double('proxy')
expect(subject).to receive(:sync_group).with(group, proxy: fake_proxy)
subject.sync_groups([group], proxy: fake_proxy)
end
end
describe '#sync_group' do
it 'syncs a single provider when a provider was given' do
proxy = EE::Gitlab::LDAP::Sync::Proxy.new('ldapmain', ldap_adapter)
expect(EE::Gitlab::LDAP::Sync::Group) expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute)
.to receive(:execute_all_providers).with(group) .with(group, proxy)
described_class.new.perform(group.id) subject.sync_group(group, proxy: proxy)
end end
it 'logs an error when group cannot be found' do it 'syncs all providers when no proxy was given' do
expect(EE::Gitlab::LDAP::Sync::Group).not_to receive(:execute_all_providers) expect(EE::Gitlab::LDAP::Sync::Group).to receive(:execute_all_providers)
expect(Sidekiq.logger) .with(group)
.to receive(:warn).with('Could not find group 9999 for LDAP group sync')
described_class.new.perform(9999) subject.sync_group(group)
end end
end 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