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

@@ -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)