Commit 836c0f4c authored by Drew Blessing's avatar Drew Blessing

Ensure LDAP Group Sync by Filter normalizes DNs

Fixes #8241. The newer LDAP Group Sync feature to sync by
LDAP filter did not normalize (ultimately, downcase) the DNs
so membership comparison was not always happening correctly. This
resulted in members be re-added during each resync even if they
were already members. This caused unnecessary writes to the DB
and also reset the joined at/given access at date.
parent 6f9038ea
---
title: Ensure LDAP Group Sync by Filter normalizes DNs
merge_request:
author:
type: fixed
...@@ -56,6 +56,13 @@ module EE ...@@ -56,6 +56,13 @@ module EE
end end
end end
def filter_search(filter)
ldap_search(
base: config.base,
filter: Net::LDAP::Filter.construct(filter)
)
end
def user_by_certificate_assertion(certificate_assertion) def user_by_certificate_assertion(certificate_assertion)
options = user_options_for_cert(certificate_assertion) options = user_options_for_cert(certificate_assertion)
users_search(options).first users_search(options).first
......
...@@ -123,7 +123,7 @@ module EE ...@@ -123,7 +123,7 @@ module EE
end end
def get_member_dns(group_link) def get_member_dns(group_link)
group_link.cn ? dns_for_group_cn(group_link.cn) : UserFilter.filter(@proxy, group_link.filter) group_link.cn ? dns_for_group_cn(group_link.cn) : proxy.dns_for_filter(group_link.filter)
end end
def dns_for_group_cn(group_cn) def dns_for_group_cn(group_cn)
...@@ -136,10 +136,6 @@ module EE ...@@ -136,10 +136,6 @@ module EE
proxy.dns_for_group_cn(group_cn) proxy.dns_for_group_cn(group_cn)
end end
def dn_for_uid(uid)
proxy.dn_for_uid(uid)
end
def update_existing_group_membership(group, access_levels) def update_existing_group_membership(group, access_levels)
logger.debug { "Updating existing membership for '#{group.name}' group" } logger.debug { "Updating existing membership for '#{group.name}' group" }
......
...@@ -35,6 +35,11 @@ module EE ...@@ -35,6 +35,11 @@ module EE
@dn_for_uid[uid] @dn_for_uid[uid]
end end
def dns_for_filter(filter)
@dns_for_filter ||= Hash.new { |h, k| h[k] = dn_filter_search(k) }
@dns_for_filter[filter.downcase]
end
private private
def ldap_group_member_dns(ldap_group_cn) def ldap_group_member_dns(ldap_group_cn)
...@@ -72,7 +77,8 @@ module EE ...@@ -72,7 +77,8 @@ module EE
def ensure_full_dns!(dns) def ensure_full_dns!(dns)
dns.map! do |dn| dns.map! do |dn|
begin begin
parsed_dn = ::Gitlab::Auth::LDAP::DN.new(dn).to_a dn_obj = ::Gitlab::Auth::LDAP::DN.new(dn)
parsed_dn = dn_obj.to_a
rescue ::Gitlab::Auth::LDAP::DN::FormatError => e rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
logger.error { "Found malformed DN: '#{dn}'. Skipping. Error: \"#{e.message}\"" } logger.error { "Found malformed DN: '#{dn}'. Skipping. Error: \"#{e.message}\"" }
next next
...@@ -82,7 +88,7 @@ module EE ...@@ -82,7 +88,7 @@ module EE
# If there is more than one key/value set we must have a full DN, # If there is more than one key/value set we must have a full DN,
# or at least the probability is higher. # or at least the probability is higher.
if parsed_dn.count > 2 if parsed_dn.count > 2
dn dn_obj.to_normalized_s
elsif parsed_dn.count == 0 elsif parsed_dn.count == 0
logger.warn { "Found null DN. Skipping." } logger.warn { "Found null DN. Skipping." }
nil nil
...@@ -143,6 +149,18 @@ module EE ...@@ -143,6 +149,18 @@ module EE
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def dn_filter_search(filter)
logger.debug { "Running filter \"#{filter}\" against #{provider}" }
dns = adapter.filter_search(filter).map(&:dn)
ensure_full_dns!(dns)
logger.debug { "Found #{dns.count} matching users for filter #{filter}" }
dns
end
def logger def logger
Rails.logger # rubocop:disable Gitlab/RailsLogger Rails.logger # rubocop:disable Gitlab/RailsLogger
end end
......
# frozen_string_literal: true
module EE
module Gitlab
module Auth
module LDAP
class UserFilter
def self.filter(*args)
new(*args).filter
end
def initialize(proxy, filter)
@proxy = proxy
@filter = filter
end
def filter
logger.debug "Running filter #{@filter} against #{@proxy.provider}"
@proxy.adapter.ldap_search(options).map(&:dn).tap do |dns|
logger.debug "Found #{dns.count} mathing users for filter #{@filter}"
end
end
private
def options
{ base: config.base, filter: construct_filter }
end
def construct_filter
Net::LDAP::Filter.construct(@filter)
end
def config
@proxy.adapter.config
end
def logger
Rails.logger # rubocop:disable Gitlab/RailsLogger
end
end
end
end
end
end
...@@ -428,11 +428,18 @@ describe EE::Gitlab::Auth::LDAP::Sync::Group do ...@@ -428,11 +428,18 @@ describe EE::Gitlab::Auth::LDAP::Sync::Group do
end end
describe '#update_permissions' do describe '#update_permissions' do
let(:group) do
create(:group_with_ldap_group_filter_link,
:access_requestable,
group_access: ::Gitlab::Access::DEVELOPER)
end
let(:sync_group) { described_class.new(group, proxy(adapter)) }
before do before do
# Safe-check because some permissions are removed when `Group#ldap_synced?` # Safe-check because some permissions are removed when `Group#ldap_synced?`
# is true (e.g. in `GroupPolicy`). # is true (e.g. in `GroupPolicy`).
expect(group).to be_ldap_synced expect(group).to be_ldap_synced
allow(EE::Gitlab::Auth::LDAP::UserFilter).to receive(:filter).and_return([user_dn(user.username)]) allow(sync_group.proxy).to receive(:dns_for_filter).and_return([user_dn(user.username)])
group.start_ldap_sync group.start_ldap_sync
end end
...@@ -441,14 +448,6 @@ describe EE::Gitlab::Auth::LDAP::Sync::Group do ...@@ -441,14 +448,6 @@ describe EE::Gitlab::Auth::LDAP::Sync::Group do
group.finish_ldap_sync group.finish_ldap_sync
end end
let(:group) do
create(:group_with_ldap_group_filter_link,
:access_requestable,
group_access: ::Gitlab::Access::DEVELOPER)
end
let(:sync_group) { described_class.new(group, proxy(adapter)) }
context 'with all functionality against one LDAP group type' do context 'with all functionality against one LDAP group type' do
context 'with basic add/update actions' do context 'with basic add/update actions' do
let(:ldap_group1) { ldap_group_entry(user_dn(user.username)) } let(:ldap_group1) { ldap_group_entry(user_dn(user.username)) }
......
...@@ -49,6 +49,14 @@ describe EE::Gitlab::Auth::LDAP::Sync::Proxy do ...@@ -49,6 +49,14 @@ describe EE::Gitlab::Auth::LDAP::Sync::Proxy do
expect(sync_proxy.dns_for_group_cn('ldap_group1')).to match_array(dns) expect(sync_proxy.dns_for_group_cn('ldap_group1')).to match_array(dns)
end end
it 'returns normalized DNs' do
ldap_group = ldap_group_entry(['UID=JaneDoe,OU=Users,DC=Example,DC=com'])
stub_ldap_group_find_by_cn('ldap_group1', ldap_group, adapter)
expect(sync_proxy.dns_for_group_cn('ldap_group1'))
.to match_array(['uid=janedoe,ou=users,dc=example,dc=com'])
end
it 'returns cached results after the first lookup' do it 'returns cached results after the first lookup' do
ldap_group = ldap_group_entry(dns) ldap_group = ldap_group_entry(dns)
stub_ldap_group_find_by_cn('ldap_group1', ldap_group, adapter) stub_ldap_group_find_by_cn('ldap_group1', ldap_group, adapter)
...@@ -123,13 +131,13 @@ describe EE::Gitlab::Auth::LDAP::Sync::Proxy do ...@@ -123,13 +131,13 @@ describe EE::Gitlab::Auth::LDAP::Sync::Proxy do
context 'when secondary_extern_uid is not stored in the database' do context 'when secondary_extern_uid is not stored in the database' do
before do before do
ldap_user_entry = ldap_user_entry('jane_doe') ldap_user_entry = Net::LDAP::Entry.new
ldap_user_entry['dn'] = 'UID=Jane_Doe,OU=Users,DC=Example,DC=com'
stub_ldap_person_find_by_uid('jane_doe', ldap_user_entry) stub_ldap_person_find_by_uid('jane_doe', ldap_user_entry)
end end
it 'returns the user DN' do it 'returns the normalized user DN' do
expect(sync_proxy.dn_for_uid('jane_doe')) expect(sync_proxy.dn_for_uid('jane_doe')).to eq('uid=jane_doe,ou=users,dc=example,dc=com')
.to eq('uid=jane_doe,ou=users,dc=example,dc=com')
end end
it 'retrieves the user from LDAP' do it 'retrieves the user from LDAP' do
...@@ -208,4 +216,34 @@ describe EE::Gitlab::Auth::LDAP::Sync::Proxy do ...@@ -208,4 +216,34 @@ describe EE::Gitlab::Auth::LDAP::Sync::Proxy do
end end
end end
end end
describe '#dns_for_filter' do
let(:filter) { '(ou=people)' }
it 'returns DNs from an LDAP search' do
entry = ldap_user_entry('johndoe')
allow(adapter).to receive(:ldap_search).and_return([entry])
expect(sync_proxy.dns_for_filter(filter)).to eq(['uid=johndoe,ou=users,dc=example,dc=com'])
end
it 'normalizes DNs' do
entry = Net::LDAP::Entry.new
entry['dn'] = 'UID=JaneDoe,OU=Users,DC=Example,DC=com'
allow(adapter).to receive(:ldap_search).and_return([entry])
expect(sync_proxy.dns_for_filter(filter)).to eq(['uid=janedoe,ou=users,dc=example,dc=com'])
end
it 'returns cached results after the first lookup' do
allow(adapter).to receive(:ldap_search).and_return([]).once
sync_proxy.dns_for_filter(filter)
expect(sync_proxy).not_to receive(:dn_filter_search)
sync_proxy.dns_for_filter(filter)
end
end
end end
require 'spec_helper'
describe EE::Gitlab::Auth::LDAP::UserFilter do
include LdapHelpers
let(:auth_hash) do
OmniAuth::AuthHash.new(uid: 'uid=john,ou=people,dc=example,dc=com', provider: 'ldapmain')
end
let!(:fake_proxy) { fake_ldap_sync_proxy(auth_hash.provider) }
let(:fake_entry) { ldap_user_entry('user1') }
before do
stub_ldap_config(
base: 'dc=example,dc=com',
active_directory: false
)
allow(fake_proxy).to receive(:provider)
end
describe '#filter' do
it 'returns dns from an LDAP search' do
filter = '(ou=people)'
allow(fake_proxy.adapter).to receive(:ldap_search).and_return([fake_entry])
expect(described_class.filter(fake_proxy, filter)).to eq(['uid=user1,ou=users,dc=example,dc=com'])
end
it 'errors out with an invalid filter' do
filter = ')('
expect { described_class.filter(fake_proxy, filter) }.to raise_error(Net::LDAP::FilterSyntaxInvalidError, 'Invalid filter syntax.')
end
end
end
...@@ -40,6 +40,28 @@ describe Gitlab::Auth::LDAP::Adapter do ...@@ -40,6 +40,28 @@ describe Gitlab::Auth::LDAP::Adapter do
end end
end end
describe '#filter_search' do
before do
stub_ldap_config(
base: 'ou=my_groups,dc=example,dc=com'
)
end
it 'searches with the proper options' do
expect(adapter).to receive(:ldap_search) do |arg|
expect(arg[:filter].to_s).to eq('(ou=people)')
expect(arg[:base]).to eq('ou=my_groups,dc=example,dc=com')
end.and_return({})
adapter.filter_search('(ou=people)')
end
it 'errors out with an invalid filter' do
expect { adapter.filter_search(')(') }
.to raise_error(Net::LDAP::FilterSyntaxInvalidError, 'Invalid filter syntax.')
end
end
describe '#user_by_certificate_assertion' do describe '#user_by_certificate_assertion' do
let(:certificate_assertion) { 'certificate_assertion' } let(:certificate_assertion) { 'certificate_assertion' }
......
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