From f7756cbfa9848d80f348c8aedcaad45aa3130caa Mon Sep 17 00:00:00 2001 From: Derek Feichtinger Date: Thu, 2 May 2013 18:06:33 +0200 Subject: [PATCH] rewrote some functions to have consistent use of DN and mssfu Also improved CLI options usage --- bin/ldapuserdir-ctl | 164 +++++++++++++++++++++---------------- ldapuserdir/ldapuserdir.py | 123 +++++++++++++++++----------- 2 files changed, 170 insertions(+), 117 deletions(-) diff --git a/bin/ldapuserdir-ctl b/bin/ldapuserdir-ctl index 04a19c0..47cf975 100755 --- a/bin/ldapuserdir-ctl +++ b/bin/ldapuserdir-ctl @@ -57,7 +57,7 @@ flag_needprivileges = False userfilter = '-' user_pw = '' -mode = 'list' +mode = 'grouplist' mylogger = logging.getLogger(os.path.basename(sys.argv[0])) mylogger.setLevel(logging.DEBUG) @@ -71,10 +71,13 @@ mylogger.addHandler(ch) # OPTION PARSING usage = """%prog [options] groupname [usernames] - Used to inspect or change members of a group in Active Directory - User names can be given as full distinguished names or just as - the short names (in that case they will be extended by the - standard OU extension) + Shows or changes members of a group in Active Directory. Also + can be used to investigate users and their group memberships. + + User and group names can be given as full distinguished names or + just as the short system names (the tool will try to figure out + the full names based on the standard OU extensions in the config + and a lookup) The configuration is read from a configuration file. Default locations: @@ -87,8 +90,9 @@ usage_epilog = """ %prog svc_ra_x06sa %prog 'svc_ra_*' - Get group memberships for user mueller + 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 @@ -104,14 +108,18 @@ examplecfg = """# Configuration file example: [Ldap] # URL for contacting the LDAP server serverurl = ldaps://d.psi.ch:636 + # base ldap path under which all users are found user_ou = OU=Users,OU=PSI,DC=d,DC=psi,DC=ch + # base ldap path under which groups are found group_ou = ou=Groups,ou=PSI,dc=d,dc=psi,dc=ch + # minimally privileged Ldap user and password for running normal # lookup queries default_user_dn = CN=linux_ldap,OU=Services,OU=IT,DC=d,DC=psi,DC=ch default_user_pw = DEFaultPassword + # Optional: # default filter to be used for group searches default_group_filter = svc-ra* @@ -122,95 +130,98 @@ parser.add_option('-a', action = 'store_true', dest = 'flag_add', help = 'add group members', - ) +) parser.add_option('-d', action = 'store_true', dest = 'flag_del', help = 'delete group members', - ) -parser.add_option('-c', - action = 'store', - dest = 'cfgfile', - help = 'path of a config file', - default = '' - ) -parser.add_option('--configfile', - action = 'store_true', - dest = 'flag_examplecfg', - help = 'show an example configuration file', - default= False - ) -parser.add_option('-u', - action = 'store', - dest = 'userfilter', - help = 'list all matching ldap users that have defined unix mappings', - ) -parser.add_option('--debug', - action = 'store_true', - dest = 'flag_debug', - help = 'debug mode: log messages at debug level', - ) -parser.add_option('-D', - action = 'store', - dest = 'user_dn', - help = 'DN or CN of ldap user for binding to the AD server (%s)' % config['default_user_dn'], - default = None - ) -parser.add_option('-f', - action = 'store', - dest = 'pwfile', - help = 'path to password file (without this pwd will be prompted for)', - default = '' - ) +) parser.add_option('-g', action = 'store', dest = 'user_to_group', help = 'get group memberships for this user', default = '' - ) +) +parser.add_option('-u', + action = 'store', + dest = 'userfilter', + help = 'list all matching ldap users that have defined unix mappings (always implies -n)' +) +parser.add_option('--dn', + action = 'store_true', + dest = 'flag_showdn', + help = 'show full DNs in the output', + default = False +) +parser.add_option('-c', + action = 'store', + dest = 'cfgfile', + help = 'path of a config file (else tries default locations)', + default = '' +) +parser.add_option('-D', + action = 'store', + dest = 'user_dn', + help = 'DN or CN of ldap user for binding to the AD server (%s)' % config['default_user_dn'], + default = None +) +parser.add_option('-f', + action = 'store', + dest = 'pwfile', + help = 'path to password file (without this pwd will be prompted for)', + default = '' +) parser.add_option('-v', action = 'store_true', dest = 'flag_verbose', - help = 'use more verbose output (with user list only)', + help = 'use more verbose output (for group and user lists)', default = False - ) +) parser.add_option('--user-ou', action = 'store', dest = 'user_ou', help = 'default OU for users (%s)' % config['user_ou'], default = None - ) +) parser.add_option('--group-ou', action = 'store', dest = 'group_ou', help = 'default OU for groups (%s)' % config['group_ou'], default = None - ) +) parser.add_option('-n', '--allow-no-mssfu', action = 'store_false', dest = 'flag_mssfu', help = 'do not restrict to entries with unix (msSFU) mappings', default = True - ) +) +parser.add_option('--debug', + action = 'store_true', + dest = 'flag_debug', + help = 'debug mode: log messages at debug level', +) +parser.add_option('--showconf', + action = 'store_true', + dest = 'flag_examplecfg', + help = 'show an example configuration file', + default= False +) parser.add_option('-V', action = 'store_true', dest = 'flag_version', help = 'show version information', default = False - ) +) (options, args) = parser.parse_args() -group = None -usernames = [] -if len(args) > 0: - group = args.pop(0) - usernames = args - if options.flag_debug: ch.setLevel(logging.DEBUG) +flag_verbose = options.flag_verbose +userfilter = options.userfilter flag_mssfu = options.flag_mssfu +flag_showdn = options.flag_showdn if options.flag_version: sys.stdout.write('Library version: ' + libversion + '\n') @@ -244,9 +255,6 @@ if options.group_ou: if options.user_ou: config['user_ou'] = options.user_ou -flag_verbose = options.flag_verbose -userfilter = options.userfilter - if options.flag_del: mode = 'del' flag_needprivileges = True @@ -259,10 +267,6 @@ if options.user_to_group: mode = "user_to_group" user_to_group = options.user_to_group -if (mode == 'add' or mode == 'del') and len(usernames) == 0: - sys.stderr.write("Error: Not enough arguments\n") - sys.exit(1) - # this we should actually do with systemuser2dn if ',' not in user_dn: user_dn = 'CN=' + user_dn + ',' + config['user_ou'] @@ -313,26 +317,46 @@ try: user_ou = config['user_ou'], logger=mylogger) - if mode == 'list': + if mode == 'grouplist': sfilter = config['default_group_filter'] - if group: - sfilter = group - ldapdir.list_groups(sfilter, mssfu=flag_mssfu) + if args: + sfilter = args.pop(0) + ldapdir.list_groups(sfilter, mssfu=flag_mssfu, returndn=flag_showdn, + verbose=flag_verbose) + elif mode == 'userlist': ldapdir.list_users_etcpwd(userfilter, verbose = flag_verbose) + elif mode == 'user_to_group': + sfilter = None + if args: + sfilter = args.pop(0) try: - dn = ldapdir.systemuser2dn(user_to_group) - sys.stdout.write("\n".join(ldapdir.get_memberof(dn, mssfu=flag_mssfu)) + '\n') + sys.stdout.write("\n".join(ldapdir.get_groups_for_user(user_to_group, + gfilter=sfilter, + mssfu=flag_mssfu, + returndn=flag_showdn)) + + '\n') #sys.stdout.write("\n".join(ldapdir.get_groups_for_user(user_to_group)) # + "\n") except LdapUserDirError, err: sys.stderr.write('Error: ' + str(err) +'\n') sys.exit(1) + elif mode == 'add': - ldapdir.add_groupmembers(group, usernames) + if len(args) < 2: + sys.stderr.write("Error: Not enough arguments\n") + sys.exit(1) + group = args.pop(0) + ldapdir.add_groupmembers(group, args) + elif mode == 'del': - ldapdir.del_groupmembers(group, usernames) + if len(args) < 2: + sys.stderr.write("Error: Not enough arguments\n") + sys.exit(1) + group = args.pop(0) + ldapdir.del_groupmembers(group, args) + except ldap.INVALID_CREDENTIALS, e: sys.exit(1) except ldap.LDAPError, e: diff --git a/ldapuserdir/ldapuserdir.py b/ldapuserdir/ldapuserdir.py index 20eddb5..10df10e 100644 --- a/ldapuserdir/ldapuserdir.py +++ b/ldapuserdir/ldapuserdir.py @@ -103,6 +103,31 @@ class LdapUserDir(object): if ',' in name: return True return False + + @staticmethod + def dn_to_cn(dn): + """ + Parameters + ---------- + dn : + + Raises + ------ + RuntimeError + + Returns + ------- + str + """ + """transforms a DN to a CN + """ + reg = re.compile(r'^CN=([^,]+),') + m = reg.match(dn) + if m: + return m.group(1) + else: + raise RuntimeError("failed to convert DN to CN (%s)" % dn) + def search_s_reconn(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, recon_attempts = 2): @@ -146,9 +171,8 @@ class LdapUserDir(object): attrsonly) except Exception, err: ok = False - self.logger.warning("Got ldap error: Reconnecting (try %s). " - % attempts + - "Error was: " + str(err)) + + self.logger.warning("Got ldap error: %s" % str(err)) if attempts >= recon_attempts: raise @@ -159,6 +183,8 @@ class LdapUserDir(object): self.logger.warning("failed to delete LDAP object: %s" % str(err)) + self.logger.warning("Trying reconnecting to ldap (attempt %s)" + % attempts) time.sleep(1) try: @@ -324,21 +350,20 @@ class LdapUserDir(object): # print e return r - def get_memberof(self, dn, gfilter=None, returndn = False, - recursive = True, mssfu = False): + def get_memberof(self, dn, recursive=True, mssfu=False): """Get all memberOf attributes for an dn (optionally recursively) This routine relies on the LDAP directory keeping memberOf attributes - in the individual entries + in the individual entries. + + Note that a recursive search may not show all groups, if mssfu + was selected, and a group in the hierarchy tree has no msSFU + settings. Parameters ---------- dn : str distinguished name - gfilter : str, optional - filter expression used for the cn part of the results - returndn : bool, optional - If True then return DNs, else CNs recursive : bool, optional if True, recurse into hierarchical groups mssfu : bool, optional @@ -347,6 +372,7 @@ class LdapUserDir(object): Returns ------- list + """ self.logger.debug('get_memberof for %s' % dn) @@ -380,13 +406,10 @@ class LdapUserDir(object): tmplist.extend(self.get_memberof(g)) grplist.extend(tmplist) - if gfilter: - grplist = [i for i in grplist if fnmatch.fnmatch(i,'CN=%s' % gfilter)] - return grplist - def get_groups_for_user(self, user, gfilter='*', ou=None, returndn = False, + def get_groups_for_user(self, user, gfilter = None, returndn = False, mssfu=False): """Get groups for a particular user from LDAP. @@ -397,8 +420,6 @@ class LdapUserDir(object): ---------- user : str system username or user DN - gfilter : str, optional - filter expression used for the cn part of the ldap dn ou : str, optional The organisational unit to be used in the ldap search returndn : bool, optional @@ -415,39 +436,26 @@ class LdapUserDir(object): RuntimeError if CN cannot be identified in a resulting group """ - if ou == None: - group_ou = self.group_ou if not self.has_dn_format(user): dnname = self.systemuser2dn(user) else: dnname = user - if mssfu: - srch = '(&(objectClass=Group)(msSFU30GidNumber=*)(cn=%s)(member=%s))' % (gfilter, dnname) - srch = '(&(objectClass=Group)(cn=%s)(member=%s))' % (gfilter, dnname) + reslist = self.get_memberof(dnname, recursive=True, mssfu=mssfu) - self.logger.debug('get_groups_for_user: %s' % srch) - r = self.search_s_reconn(group_ou, ldap.SCOPE_SUBTREE, srch) + if not returndn: + try: + reslist = [self.dn_to_cn(grp) for grp in reslist] + except RuntimeError, e: + self.logger.error(str(e)) - reslist = [] - for dn, entry in r: - reslist.append(dn) + if gfilter: + reslist = fnmatch.filter(reslist, gfilter) + + return reslist - if returndn: - return reslist - - cnlist = [] - reg = re.compile(r'^CN=([^,]+),') - for dn in reslist: - m = reg.match(dn) - if m: - cnlist.append(m.group(1)) - else: - raise RuntimeError("Could not match CN in DN (%s)" % dn) - - return cnlist - - def list_groups(self, filter = '*', ou = None, mssfu=False): + def list_groups(self, filter = '*', ou = None, mssfu=False, + returndn = False, verbose = False): """Prints a list of groups from the LDAP directory to stdout Parameters @@ -458,18 +466,39 @@ class LdapUserDir(object): organisational unit to be used in the ldap search mssfu : bool, optional Whether to only show users with mssfu mappings - + returndn : bool + If true, return full DNs + verbose : bool + If true, print one name per line """ + 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) return - for dn, entry in r: - print entry['cn'][0] - if 'member' in entry: - for cn in entry['member']: - print ' member: ', cn + + if verbose: + for dn, entry in r: + if returndn: + print "group: %s" % dn, + else: + print "group: %s" % entry['cn'][0], + print "(%s)" % entry['msSFU30GidNumber'][0] + if 'member' in entry: + for cn in entry['member']: + if returndn: + print ' member: ', cn + else: + print ' member: ', self.dn_to_cn(cn) + else: + for dn, entry in r: + print "%s:IGNORE:%s:" % (entry['cn'][0], entry['msSFU30GidNumber'][0]), + if 'member' in entry: + print ",".join([self.dn_to_cn(dn) for dn in entry['member']]) + def _mod_groupmembers(self, ldapmode, dngroup, usernames): """modifies (adds/deletes) members of an LDAP group entry