diff --git a/bin/ldapuserdir-ctl b/bin/ldapuserdir-ctl index 14d01ca..bb8d2e8 100755 --- a/bin/ldapuserdir-ctl +++ b/bin/ldapuserdir-ctl @@ -93,16 +93,16 @@ usage += """ usage_epilog = """ Examples: List group members - %prog svc_ra_x06sa - %prog 'svc_ra_*' + %prog svc-ra_x06sa + %prog 'svc-ra_*' Get group memberships for user mueller (optionally with a group filter) %prog -g mueller %prog -g mueller 'svc-ra*' Add/delete users to/from a group (requires access rights!) - %prog -a svc_ra_x06sa user1 user2 user3 - %prog -d svc_ra_x06sa user1 user2 + %prog -a svc-ra_x06sa user1 user2 user3 + %prog -d svc-ra_x06sa user1 user2 List users matching a pattern %prog -u 'mueller*' @@ -227,6 +227,13 @@ parser.add_option('-V', default = False ) +parser.add_option('-r', + action = 'store_true', + dest = 'recursive', + help = 'Recursively resolve groups', + default = False +) + (options, args) = parser.parse_args() if options.flag_debug: @@ -340,7 +347,9 @@ try: if args: sfilter = args.pop(0) ldapdir.list_groups(sfilter, mssfu=flag_mssfu, returndn=flag_showdn, - verbose=flag_verbose) + verbose=flag_verbose, recursive=options.recursive) + + elif mode == 'userlist': records = ldapdir.get_users(userfilter, config['user_ou'], mssfu=flag_mssfu) diff --git a/ldapuserdir/ldapuserdir.py b/ldapuserdir/ldapuserdir.py index 9f4abda..3130c47 100755 --- a/ldapuserdir/ldapuserdir.py +++ b/ldapuserdir/ldapuserdir.py @@ -17,6 +17,7 @@ import re from glob import fnmatch import logging import time +import itertools class LdapUserDirError(Exception): @@ -144,8 +145,10 @@ class LdapUserDir(object): def _search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): - """Helper for search_s_reconn. Wraps ldap.search_ext to use paged results if -desired (see self.page_size).""" + """Helper for search_s_reconn. + + Wraps ldap.search_ext to use paged results if desired (see self.page_size). + """ if self.page_size == 0: # Do not use paged results self.logger.debug('not using paging since page_size is %d\n' % self.page_size) @@ -393,6 +396,7 @@ desired (see self.page_size).""" gfilter : str, optional filter expression used for the cn part of the ldap dn ou : str, optional + group OU mssfu : bool, optional Whether to only show users with mssfu mappings @@ -522,7 +526,7 @@ desired (see self.page_size).""" return reslist def list_groups(self, filter='*', ou=None, mssfu=False, - returndn=False, verbose=False): + returndn=False, verbose=False, recursive=False,indent=0): """Prints a list of groups from the LDAP directory to stdout Parameters @@ -533,48 +537,126 @@ desired (see self.page_size).""" organisational unit to be used in the ldap search mssfu : bool, optional Whether to only show users with mssfu mappings - returndn : bool + returndn : bool, optional If true, return full DNs - verbose : bool + verbose : bool, optional If true, print one name per line + recursive : bool, optional + If true, any groups contained within the output will be resolved recursively to users + indent : int, optional + For internal use only. Indicates indent level for verbose recursive mode. Otherwise ignored. """ if returndn: verbose = True r = self.get_groups_struct(filter, ou, mssfu) if len(r) == 0: - sys.stderr.write("Error: no groups found (filter: %s)\n" % filter) + sys.stderr.write("%sError: no groups found (filter: %s)\n" % (' '*indent, filter)) return if verbose: + indent_increment = 3 # amount to indent members for dn, entry in r: if returndn: - print "group: %s" % dn, + print("%sgroup: %s" % (' '*indent, dn)), else: - print "group: %s" % entry['cn'][0], + print("%sgroup: %s" % (' '*indent, entry['cn'][0])), if not 'msSFU30GidNumber' in entry: gid = '---' else: gid = entry['msSFU30GidNumber'][0] print "(%s)" % gid if 'member' in entry: - for cn in entry['member']: - if returndn: - print ' member: ', cn + for member in entry['member']: + # Check if member is itself a group. This might be PSI-specific + is_group = self._is_group(member) + if recursive and is_group: + self.list_groups( + filter=self.dn_to_cn(member), + ou=ou, + mssfu=mssfu, + returndn=returndn, + verbose=verbose, + recursive=recursive, + indent=indent+indent_increment) else: - print ' member: ', self.dn_to_cn(cn) + if returndn: + print('%smember: %s' % (' '*(indent+indent_increment), member )) + else: + print('%smember: %s' % (' '*(indent+indent_increment), + self.dn_to_cn(member))) else: for dn, entry in r: if not 'msSFU30GidNumber' in entry: gid = '---' else: gid = entry['msSFU30GidNumber'][0] + sys.stdout.write("%s:IGNORE:%s:" % (entry['cn'][0], gid)) if 'member' in entry: - sys.stdout.write(",".join([self.dn_to_cn(dn) for dn in entry['member']]) + "\n") + members = [self.dn_to_cn(dn) for dn in entry['member']] + if recursive: + members = [m for cn in members for m in self._get_all_members(cn)] + sys.stdout.write(",".join(members) + "\n") else: sys.stdout.write("\n") + def _get_all_members(self, filter, include_groups=False, ou=None, mssfu=False): + """Generator to recursively get the CN for all members of a group. + + If filter does not match any groups it is assumed to be a user and is yeilded directly. + + Parameters + ---------- + filter : str + filter expression used for the cn part of the ldap dn. If it matches + multiple entries they will be concatenated. + include_groups : bool + if true, the CN of any member groups will be included in the results + (preordered traversal) + ou : str, optional + organisational unit to be used in the ldap search + mssfu : bool, optional + Whether to only show users with mssfu mappings + + Returns + ------- + Generator of str : CN for all members of groups matching the filter. + """ + r = self.get_groups_struct(filter, ou, mssfu) + if len(r) == 0: + # no group entry, so input must have been a user + yield filter + return + + for dn, entry in r: + # preorder the group + if include_groups: + yield self.dn_to_cn(dn) + if "member" in entry: + # not an empty group + for member in entry["member"]: + # Shortcut recursion for known leaves + #if not LdapUserDir._is_group(member): + # yield member + #else: + cn = self.dn_to_cn(member) + for submember in self._get_all_members(cn, include_groups=include_groups, ou=ou, mssfu=mssfu): + yield submember + + @staticmethod + def _is_group(dn): + """Quick check if a DN is a group. That is, if it contains OU=Groups. + + This may be a PSI-specific check + + Parameters + ---------- + dn : str + DN of the entry + """ + return "OU=Groups" in dn + def _mod_groupmembers(self, ldapmode, dngroup, usernames): """modifies (adds/deletes) members of an LDAP group entry