Support nested groups in Active Directory

Active Directory has a very specific mechanism to
handle nested groups.  LDAP queries need to look like this:

"(&(objectClass=group)
   (member=member:1.2.840.113556.1.4.1941:=CN=nwalnut,OU=Users,DC=EXAMPLE,DC=COM))"

If a deployment is using nested groups, three queries need to be
modified to support it:

  - list users in a group
  - list groups for a user
  - check if a user is in a group

Since all three are necessary, a single configuration value ensures
that the change is synchronized across all three calls.

Closed-Bug: #1638603
Change-Id: Ia66f81f86d7c43fbc5ba7f18ada91c77d047f7a2
This commit is contained in:
Adam Young 2016-10-20 14:51:27 -04:00 committed by Steve Martinelli
parent d45d82f6ed
commit e8e56dc7c1
4 changed files with 110 additions and 6 deletions

View File

@ -443,6 +443,14 @@ object and `group_attr` is the attribute which should appear in the identity
API.
"""))
group_ad_nesting = cfg.BoolOpt(
'group_ad_nesting',
default=False,
help=utils.fmt("""
If enabled, group queries will use Active Directory specific filters for
nested groups.
"""))
tls_cacertfile = cfg.StrOpt(
'tls_cacertfile',
help=utils.fmt("""
@ -614,6 +622,7 @@ ALL_OPTS = [
group_allow_update,
group_allow_delete,
group_additional_attribute_mapping,
group_ad_nesting,
tls_cacertfile,
tls_cacertdir,
use_tls,

View File

@ -35,6 +35,8 @@ _DEPRECATION_MSG = _('%s for the LDAP identity backend has been deprecated in '
'the Mitaka release in favor of read-only identity LDAP '
'access. It will be removed in the "O" release.')
LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
class Identity(base.IdentityDriverBase):
def __init__(self, conf=None):
@ -337,6 +339,7 @@ class GroupApi(common_ldap.BaseLdap):
def __init__(self, conf):
super(GroupApi, self).__init__(conf)
self.group_ad_nesting = conf.ldap.group_ad_nesting
self.member_attribute = (conf.ldap.group_member_attribute
or self.DEFAULT_MEMBER_ATTRIBUTE)
@ -386,15 +389,29 @@ class GroupApi(common_ldap.BaseLdap):
def list_user_groups(self, user_dn):
"""Return a list of groups for which the user is a member."""
user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
query = '(%s=%s)' % (self.member_attribute,
user_dn_esc)
if self.group_ad_nesting:
query = '(%s:%s:=%s)' % (
self.member_attribute,
LDAP_MATCHING_RULE_IN_CHAIN,
user_dn_esc)
else:
query = '(%s=%s)' % (self.member_attribute,
user_dn_esc)
return self.get_all(query)
def list_user_groups_filtered(self, user_dn, hints):
"""Return a filtered list of groups for which the user is a member."""
user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
query = '(%s=%s)' % (self.member_attribute,
user_dn_esc)
if self.group_ad_nesting:
# Hardcoded to member as that is how the Matching Rule in Chain
# Mechanisms expects it. The member_attribute might actually be
# member_of elsewhere, so they are not the same.
query = '(member:%s:=%s)' % (
LDAP_MATCHING_RULE_IN_CHAIN,
user_dn_esc)
else:
query = '(%s=%s)' % (self.member_attribute,
user_dn_esc)
return self.get_all_filtered(hints, query)
def list_group_users(self, group_id):
@ -403,8 +420,20 @@ class GroupApi(common_ldap.BaseLdap):
group_dn = group_ref['dn']
try:
attrs = self._ldap_get_list(group_dn, ldap.SCOPE_BASE,
attrlist=[self.member_attribute])
if self.group_ad_nesting:
# NOTE(ayoung): LDAP_SCOPE is used here instead of hard-
# coding to SCOPE_SUBTREE to get through the unit tests.
# However, it is also probably more correct.
attrs = self._ldap_get_list(
self.tree_dn, self.LDAP_SCOPE,
query_params={
"member:%s:" % LDAP_MATCHING_RULE_IN_CHAIN:
group_dn},
attrlist=[self.member_attribute])
else:
attrs = self._ldap_get_list(group_dn, ldap.SCOPE_BASE,
attrlist=[self.member_attribute])
except ldap.NO_SUCH_OBJECT:
raise self.NotFound(group_id=group_id)

View File

@ -3279,3 +3279,60 @@ class LdapFilterTests(identity_tests.FilterTests, LDAPTestSetup):
# The LDAP identity driver currently does not support filtering on the
# listing users for a given group, so will fail this test.
super(LdapFilterTests, self).test_list_users_in_group_exact_filtered()
class LDAPMatchingRuleInChainTests(LDAPTestSetup):
def setUp(self):
self.useFixture(database.Database())
super(LDAPMatchingRuleInChainTests, self).setUp()
_assert_backends(self, identity='ldap')
group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id)
self.group = self.identity_api.create_group(group)
user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id)
self.user = self.identity_api.create_user(user)
self.identity_api.add_user_to_group(self.user['id'],
self.group['id'])
def config_overrides(self):
super(LDAPMatchingRuleInChainTests, self).config_overrides()
self.config_fixture.config(group='identity', driver='ldap')
self.config_fixture.config(
group='ldap',
group_ad_nesting=True,
url='fake://memory',
chase_referrals=False,
group_tree_dn='cn=UserGroups,cn=example,cn=com',
query_scope='one')
def config_files(self):
config_files = super(LDAPMatchingRuleInChainTests, self).config_files()
config_files.append(unit.dirs.tests_conf('backend_ldap.conf'))
return config_files
def test_get_group(self):
group_ref = self.identity_api.get_group(self.group['id'])
self.assertDictEqual(self.group, group_ref)
def test_list_user_groups(self):
# tests indirectly by calling delete user
self.identity_api.delete_user(self.user['id'])
def test_list_groups_for_user(self):
groups_ref = self.identity_api.list_groups_for_user(self.user['id'])
self.assertEqual(0, len(groups_ref))
def test_list_groups(self):
groups_refs = self.identity_api.list_groups()
self.assertEqual(1, len(groups_refs))
self.assertEqual(self.group['id'], groups_refs[0]['id'])
self.identity_api.delete_group(self.group['id'])
self.assertRaises(exception.GroupNotFound,
self.identity_api.get_group,
self.group['id'])
groups_refs = self.identity_api.list_groups()
self.assertEqual([], groups_refs)

View File

@ -0,0 +1,9 @@
---
features:
- >
[`bug 1638603 <https://bugs.launchpad.net/keystone/+bug/1638603>`_]
Support nested groups in Active Directory. A new boolean option
``[ldap] group_ad_nesting`` has been added, it defaults to ``False``.
Enable the option is using Active Directory with nested groups. This
option will impact the ``list_users_in_group``, ``list_groups_for_user``,
and ``check_user_in_group`` operations.