diff --git a/docs/administration.rst b/docs/administration.rst index eec01cc5..9d6c8d12 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -324,6 +324,7 @@ Here is an example LDAP configuration stanza you can add to your config. Adjust LDAP_CACERT_FILE = '/opt/lemur/trusted.pem' LDAP_REQUIRED_GROUP = 'certificate-management-access' LDAP_GROUPS_TO_ROLES = {'certificate-management-admin': 'admin', 'certificate-management-read-only': 'read-only'} + LDAP_IS_ACTIVE_DIRECTORY = True The lemur ldap module uses the `user principal name` (upn) of the authenticating user to bind. This is done once for each user at login time. The UPN is effectively the email address in AD/LDAP of the user. If the user doesn't provide the email address, it constructs one based on the username supplied (which should normally match the samAccountName) and the value provided by the config LDAP_EMAIL_DOMAIN. @@ -406,6 +407,17 @@ The following LDAP options are not required, however TLS is always recommended. LDAP_GROUPS_TO_ROLES = {'lemur_admins': 'admin', 'Lemur Team DL Group': 'team@example.com'} +.. data:: LDAP_IS_ACTIVE_DIRECTORY + :noindex: + + When set to True, nested group memberships are supported, by searching for groups with the member:1.2.840.113556.1.4.1941 attribute set to the user DN. + When set to False, the list of groups will be determined by the 'memberof' attribute of the LDAP user logging in. + + :: + + LDAP_IS_ACTIVE_DIRECTORY = False + + Authentication Providers ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lemur/auth/ldap.py b/lemur/auth/ldap.py index 398a5830..7eded060 100644 --- a/lemur/auth/ldap.py +++ b/lemur/auth/ldap.py @@ -41,6 +41,7 @@ class LdapPrincipal(): self.ldap_default_role = current_app.config.get("LEMUR_DEFAULT_ROLE", None) self.ldap_required_group = current_app.config.get("LDAP_REQUIRED_GROUP", None) self.ldap_groups_to_roles = current_app.config.get("LDAP_GROUPS_TO_ROLES", None) + self.ldap_is_active_directory = current_app.config.get("LDAP_IS_ACTIVE_DIRECTORY", False) self.ldap_attrs = ['memberOf'] self.ldap_client = None self.ldap_groups = None @@ -168,11 +169,28 @@ class LdapPrincipal(): except ldap.LDAPError as e: raise Exception("ldap error: {0}".format(e)) - lgroups = self.ldap_client.search_s(self.ldap_base_dn, - ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs)[0][1]['memberOf'] - # lgroups is a list of utf-8 encoded strings - # convert to a single string of groups to allow matching - self.ldap_groups = b''.join(lgroups).decode('ascii') + if self.ldap_is_active_directory: + # Lookup user DN, needed to search for group membership + userdn = self.ldap_client.search_s(self.ldap_base_dn, + ldap.SCOPE_SUBTREE, ldap_filter, + ['distinguishedName'])[0][1]['distinguishedName'][0] + userdn = userdn.decode('utf-8') + # Search all groups that have the userDN as a member + groupfilter = '(&(objectclass=group)(member:1.2.840.113556.1.4.1941:={0}))'.format(userdn) + lgroups = self.ldap_client.search_s(self.ldap_base_dn, ldap.SCOPE_SUBTREE, groupfilter, ['cn']) + + # Create a list of group CN's from the result + self.ldap_groups = [] + for group in lgroups: + (dn, values) = group + self.ldap_groups.append(values['cn'][0].decode('ascii')) + else: + lgroups = self.ldap_client.search_s(self.ldap_base_dn, + ldap.SCOPE_SUBTREE, ldap_filter, self.ldap_attrs)[0][1]['memberOf'] + # lgroups is a list of utf-8 encoded strings + # convert to a single string of groups to allow matching + self.ldap_groups = b''.join(lgroups).decode('ascii') + self.ldap_client.unbind() def _ldap_validate_conf(self):