From 0f696f22472c0d513a7ceb950956717285a88b62 Mon Sep 17 00:00:00 2001 From: M Kraai Date: Tue, 2 Apr 2019 13:32:19 -0700 Subject: [PATCH] Enable Active Directory primary groups Include primary group relationships when searching groups by users, or users by group. Active Directory does not include this relationship in the member and memberOf collections in LDAP queries. --- lib/ldap_fluff/active_directory.rb | 7 +++++ lib/ldap_fluff/ad_member_service.rb | 40 ++++++++++++++++++++++++++--- test/ad_member_services_test.rb | 25 ++++++++++++++++-- test/ad_test.rb | 21 +++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/lib/ldap_fluff/active_directory.rb b/lib/ldap_fluff/active_directory.rb index 999b8f3..7aa05bc 100644 --- a/lib/ldap_fluff/active_directory.rb +++ b/lib/ldap_fluff/active_directory.rb @@ -44,6 +44,13 @@ def users_from_search_results(search, method) end end + # In AD, the relationship between a user account and the "Primary Group" for that account + # is not included in the member and memberof attributes. + if search.respond_to? 'primarygrouptoken' + primary_users = @ldap.search(:base => @ldap.base, :filter => Net::LDAP::Filter.eq('primarygroupid',search['primarygrouptoken'].first)) + users += primary_users.map { |user| @member_service.get_login_from_entry(user) } + end + users.flatten.uniq end diff --git a/lib/ldap_fluff/ad_member_service.rb b/lib/ldap_fluff/ad_member_service.rb index ae1f42c..aff8a49 100644 --- a/lib/ldap_fluff/ad_member_service.rb +++ b/lib/ldap_fluff/ad_member_service.rb @@ -8,6 +8,12 @@ def initialize(ldap, config) super end + def find_group(gid) + group = @ldap.search(:filter => group_filter(gid), :base => @group_base, :attributes => ['*','primaryGroupToken']) + raise self.class::GIDNotFoundException if (group.nil? || group.empty?) + group + end + # get a list [] of ldap groups for a given user # in active directory, this means a recursive lookup def find_user_groups(uid) @@ -19,9 +25,20 @@ def find_user_groups(uid) def _groups_from_ldap_data(payload) data = [] if !payload.nil? - first_level = payload[:memberof] - total_groups, _ = _walk_group_ancestry(first_level, first_level) - data = (get_groups(first_level + total_groups)).uniq + first_level = payload[:memberof] + normal_groups, _ = _walk_group_ancestry(first_level, first_level) + # In AD, a user's primary group is not included in the memberOf list, and must be handled separately. + # By default, a new user's primary group is 'Domain Users' + primary_groups = [] + primary_first_level = [] + if !payload[:primarygroupid].nil? + domain_sid = _get_sid_string(payload[:objectsid].first).split('-')[0..-2].join('-') + primary_sid = domain_sid + '-' + payload[:primarygroupid].first + primary_group = @ldap.search(:filter => Net::LDAP::Filter.eq('objectsid', primary_sid), :base => @group_base, :attributes => ['memberof']).first + primary_first_level = primary_group[:dn] + primary_groups, _ = _walk_group_ancestry(primary_first_level, primary_first_level) + end + data = (get_groups(first_level + normal_groups + primary_first_level + primary_groups)).uniq end data end @@ -43,6 +60,23 @@ def _walk_group_ancestry(group_dns = [], known_groups = []) [set, known_groups] end + def _get_sid_string(sid_bin) + sid = [] + + # Byte 1: SID structure revision number (always 1 so far...) + sid << sid_bin[0].unpack("H2").first.to_i + + # Skip byte 2 + # Bytes 3-8: Identifier Authority + sid << sid_bin[2,6].unpack("H*").first.to_i + + # Remaining bytes: list of unsigned, 32-bit, little-endian ints + sid += sid_bin.unpack("@8V*") + + # Put it all together. + "S-" + sid.join('-') + end + def class_filter Net::LDAP::Filter.eq("objectclass", "group") end diff --git a/test/ad_member_services_test.rb b/test/ad_member_services_test.rb index c58afc8..58cd4c1 100644 --- a/test/ad_member_services_test.rb +++ b/test/ad_member_services_test.rb @@ -15,7 +15,7 @@ def basic_user end def basic_group - @ldap.expect(:search, ad_group_payload, [:filter => ad_group_filter("broze"), :base => @config.group_base]) + @ldap.expect(:search, ad_group_payload, [:filter => ad_group_filter("broze"), :base => @config.group_base, :attributes=>["*", "primaryGroupToken"]]) end def nest_deep(n) @@ -116,7 +116,7 @@ def test_find_good_group end def test_find_missing_group - @ldap.expect(:search, nil, [:filter => ad_group_filter("broze"), :base => @config.group_base]) + @ldap.expect(:search, nil, [:filter => ad_group_filter("broze"), :base => @config.group_base, :attributes=>["*", "primaryGroupToken"]]) @adms.ldap = @ldap assert_raises(LdapFluff::ActiveDirectory::MemberService::GIDNotFoundException) do @adms.find_group('broze') @@ -161,4 +161,25 @@ def test_get_login_from_entry_missing_attr assert_nil(@adms.get_login_from_entry(entry)) end + def test_find_primary_group + prim_group = Net::LDAP::Entry.new('p_group') + test_user = Net::LDAP::Entry.new('t_user') + + prim_group[:cn] = ['p_group'] + prim_group[:dn] = ['CN=p_group,DC=corp,DC=example,DC=com'] + prim_group[:primarygrouptoken] = ['12345'] + prim_group[:member] = [] + test_user[:objectsid] = ["\x01\x04\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\a\x87\x00\x00\xA0[\x00\x00\xE1\x10\x00\x00"] + test_user[:primarygroupid] = ['12345'] + test_user[:samaccountname] = ['tuser'] + test_user[:memberof] = [] + + @ldap.expect(:search, [test_user], [:filter => Net::LDAP::Filter.eq('samaccountname','tuser')]) + @ldap.expect(:search, [prim_group], [:filter => Net::LDAP::Filter.eq('objectsid', 'S-1-5-21-34567-23456-12345'), :base => @config.group_base, :attributes => ['memberof']]) + @ldap.expect(:search, [], [:base => 'CN=p_group,DC=corp,DC=example,DC=com', :scope => Net::LDAP::SearchScope_BaseObject, :attributes => ['memberof']]) + + @adms.ldap = @ldap + assert_equal(@adms.find_user_groups('tuser'), ['p_group']) + end + end diff --git a/test/ad_test.rb b/test/ad_test.rb index 28ebf1d..1435501 100644 --- a/test/ad_test.rb +++ b/test/ad_test.rb @@ -209,4 +209,25 @@ def test_find_users_with_empty_nested_group md.verify end + def test_find_user_from_primary_group + prim_group = Net::LDAP::Entry.new('p_group') + test_user = Net::LDAP::Entry.new('t_user') + + prim_group[:cn] = ['p_group'] + prim_group[:primarygrouptoken] = ['12345'] + prim_group[:member] = [] + test_user[:primarygroupid] = ['12345'] + test_user[:samaccountname] = ['tuser'] + + @ldap.expect(:auth, nil, %w(service pass)) + @ldap.expect(:bind, true) + 2.times { @ldap.expect(:search, [prim_group], [:filter => ad_group_filter('p_group'), :base => @config.group_base, :attributes=>["*", "primaryGroupToken"]]) } + @ldap.expect(:search, [test_user], [:base => @config.base_dn, :filter => Net::LDAP::Filter.eq('primarygroupid','12345')]) + @ldap.expect(:base, @config.base_dn) + + @ad.ldap = @ldap + @ad.member_service.ldap = @ldap + assert_equal(@ad.users_for_gid('p_group'), ['tuser']) + end + end