implemented discovery of a user's groups by recursive memberOf search

in the course of this I also decided to change some of the general
structure, especially concerning the handling of the MSsfu option.
This required a change in the API, regrettably, and to a major version
bump.
Also I adopted semantiv versioning.
This commit is contained in:
2013-04-30 17:21:12 +02:00
parent 94c20aedd6
commit d722c32000
5 changed files with 135 additions and 64 deletions

View File

@@ -186,11 +186,11 @@ parser.add_option('--group-ou',
help = 'default OU for groups (%s)' % config['group_ou'],
default = None
)
parser.add_option('--no-msSFU',
action = 'store_true',
dest = 'flag_nosfu',
parser.add_option('-n', '--allow-no-mssfu',
action = 'store_false',
dest = 'flag_mssfu',
help = 'do not restrict to entries with unix (msSFU) mappings',
default = False
default = True
)
parser.add_option('-V',
action = 'store_true',
@@ -210,6 +210,8 @@ if len(args) > 0:
if options.flag_debug:
ch.setLevel(logging.DEBUG)
flag_mssfu = options.flag_mssfu
if options.flag_version:
sys.stdout.write('Library version: ' + libversion + '\n')
sys.exit(0)
@@ -243,7 +245,6 @@ if options.user_ou:
config['user_ou'] = options.user_ou
flag_verbose = options.flag_verbose
flag_sfu = not options.flag_nosfu
userfilter = options.userfilter
if options.flag_del:
@@ -283,7 +284,7 @@ to LDAP DN for binding (you may want to use the explicit -D user_dn option)
''' % os.getlogin() )
sys.exit(1)
else:
sys.stderr.write('Uncaught Error: %s' % str(err))
sys.stderr.write('Uncaught Error: %s' % str(err))
except ldap.LDAPError, e:
sys.stderr.write('LDAP error: %s\n' % str(e))
@@ -305,32 +306,33 @@ if user_pw == '':
sys.exit(1)
try:
l = LdapUserDir(config['serverurl'],
user_dn,
user_pw,
config['group_ou'],
config['user_ou'],
sfu = flag_sfu,
logger=mylogger)
ldapdir = LdapUserDir(config['serverurl'],
user_dn,
user_pw,
group_ou = config['group_ou'],
user_ou = config['user_ou'],
logger=mylogger)
if mode == 'list':
sfilter = config['default_group_filter']
if group:
sfilter = group
l.list_groups(sfilter)
ldapdir.list_groups(sfilter, mssfu=flag_mssfu)
elif mode == 'userlist':
l.list_users_etcpwd(userfilter, verbose = flag_verbose)
ldapdir.list_users_etcpwd(userfilter, verbose = flag_verbose)
elif mode == 'user_to_group':
try:
sys.stdout.write("\n".join(l.get_groups_for_user(user_to_group))
+ "\n")
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))
# + "\n")
except LdapUserDirError, err:
sys.stderr.write('Error: ' + str(err) +'\n')
sys.exit(1)
elif mode == 'add':
l.add_groupmembers(group, usernames)
ldapdir.add_groupmembers(group, usernames)
elif mode == 'del':
l.del_groupmembers(group, usernames)
ldapdir.del_groupmembers(group, usernames)
except ldap.INVALID_CREDENTIALS, e:
sys.exit(1)
except ldap.LDAPError, e:

View File

@@ -2,6 +2,9 @@
# URL for contacting the LDAP server
serverurl = ldaps://d.psi.ch:636
# base ldap path for global searches: not yet used
top_ou = OU=PSI,DC=d,DC=psi,DC=ch
# base ldap path under which all users are found
user_ou = OU=Users,OU=PSI,DC=d,DC=psi,DC=ch

View File

@@ -15,26 +15,10 @@ import ldap
import os
import sys
import re
from glob import fnmatch
import logging
import time
##############################################################
# definitions of the search strings
# restrict to entries with msSFU mappings
searches_mssfu= dict({
'get_users' : '(&(objectClass=user)(!(objectClass=computer))(msSFU30UidNumber=*)(msSFU30HomeDirectory=*)(cn=%s))',
'systemuser2dn' : '(&(objectClass=user)(!(objectClass=computer))(msSFU30UidNumber=*)(msSFU30HomeDirectory=*)(cn=%s))',
'get_groups_struct' : '(&(objectClass=Group)(msSFU30GidNumber=*)(cn=%s))',
'get_groups_for_user' : '(&(objectClass=Group)(msSFU30GidNumber=*)(cn=%s)(member=%s))'})
# allow entries without msSFU mappings
searches_nomssfu= dict({
'get_users' : '(&(objectClass=user)(!(objectClass=computer))(cn=%s))',
'systemuser2dn' : '(&(objectClass=user)(!(objectClass=computer))(msSFU30UidNumber=*)(msSFU30HomeDirectory=*)(cn=%s))',
'get_groups_struct' : '(&(objectClass=Group)(cn=%s))',
'get_groups_for_user' : '(&(objectClass=Group)(cn=%s)(member=%s))'})
##############################################################
class LdapUserDirError(Exception):
def __init__(self, errmsg):
@@ -50,22 +34,14 @@ class LdapUserDir(object):
user_pw,
group_ou = 'ou=example.com',
user_ou = 'ou=example.com',
sfu = True,
logger = None):
logger = None,
):
self.serverurl = serverurl
self.group_ou = group_ou
self.user_ou = user_ou
self.user_dn = user_dn
self.user_pw = user_pw
# whether to only search for entries with msSFU mappings
# i.e. with existing unix attributes
if sfu:
self.searches = searches_mssfu
else:
self.searches = searches_nomssfu
if logger == None:
self.logger = logging.getLogger('LdapUserDir')
self.logger.setLevel(logging.WARNING)
@@ -100,6 +76,15 @@ class LdapUserDir(object):
except ldap.LDAPError, e:
raise
@staticmethod
def has_dn_format(name):
"""returns true if name has the format of a distinguished name
"""
# currently we are satisfied with a very primitive check
if ',' in name:
return True
return False
def search_s_reconn(self, base, scope, filterstr='(objectClass=*)',
attrlist=None, attrsonly=0, recon_attempts = 2):
"""wrapper of standard ldap.search_s synchronous search that
@@ -166,17 +151,24 @@ class LdapUserDir(object):
#def search_s(self,base,scope,filterstr='(objectClass=*)',attrlist=None,attrsonly=0):
def get_users(self, filter='*', ou=None):
def get_users(self, filter='*', ou=None, mssfu=False):
"""get the names of all users from the directory service
@param filter A filter expression used for the cn part of the ldap dn
@param ou The organisational unit to be used in the ldap search
@param mssfu Whether to only show users with mssfu mappings
@returns A dictionary of the matching users { dn1:list1, ... }
"""
if ou == None:
user_ou = self.user_ou
if mssfu:
srch = '(&(objectClass=user)(!(objectClass=computer))(msSFU30UidNumber=*)(msSFU30HomeDirectory=*)(cn=%s))'
else:
srch = '(&(objectClass=user)(!(objectClass=computer))(cn=%s))'
#try:
r = self.search_s_reconn(user_ou, ldap.SCOPE_SUBTREE,
self.searches['get_users'] % filter)
srch % filter)
#except ldap.LDAPError, e:
# print e
# return
@@ -187,7 +179,7 @@ class LdapUserDir(object):
@param filter A filter expression used for the cn part of the ldap dn
@param ou The organisational unit to be used in the ldap search
"""
r = self.get_users(filter, ou)
r = self.get_users(filter, ou, mssfu=True)
for dn, entry in r:
# MUST fields
try:
@@ -220,7 +212,8 @@ class LdapUserDir(object):
@exception may throw an ldap.LDAPError or LdapUserDir("No such user")
"""
#try:
srch = self.searches['systemuser2dn'] % uname
srch = '(&(objectClass=user)(!(objectClass=computer))(msSFU30UidNumber=*)(msSFU30HomeDirectory=*)(cn=%s))' % uname
self.logger.debug('systemuser2dn: %s' % srch)
r = self.search_s_reconn(self.user_ou, ldap.SCOPE_SUBTREE, srch)
#except ldap.LDAPError, e:
@@ -232,25 +225,95 @@ class LdapUserDir(object):
self.logger.debug('systemuser2dn: dn = %s' % r[0][0])
return r[0][0]
def get_groups_struct(self, gfilter='*', ou = None):
"""searches for groups
def get_groups_struct(self, gfilter='*', ou = None, mssfu=False):
"""searches for groups that match filter
returns the full ldap search result structure for the search
with the optional filter applied to the cn field
@param filter A filter expression used for the cn part of the ldap dn
@param ou The organisational unit to be used in the ldap search
@param mssfu Whether to only show users with mssfu mappings
@returns A dictionary of the matching groups { dn1:list1, ... }
"""
if ou == None:
group_ou = self.group_ou
#try:
srch = self.searches['get_groups_struct'] % gfilter
if mssfu:
srch = '(&(objectClass=Group)(msSFU30GidNumber=*)(cn=%s))' % gfilter
else:
srch = '(&(objectClass=Group)(cn=%s))' % gfilter
self.logger.debug('get_groups_struct: %s' % srch)
#try:
r = self.search_s_reconn(group_ou, ldap.SCOPE_SUBTREE, srch)
#except ldap.LDAPError, e:
# print e
return r
def get_groups_for_user(self, user, gfilter='*', ou=None, returndn = False):
def get_memberof(self, dn, gfilter=None, returndn = False,
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
Parameters
----------
dn : distinguished name
gfilter : filter expression used for the cn part of the results, optional
returndn : If True then return DNs, optional
recursive : recurse into hierarchical groups, optional
mssfu : whether to only return entries with MSsfu attributes, optional
Returns
-------
List of groups
"""
self.logger.debug('get_memberof for %s' % dn)
srch = '(&)'
if mssfu:
srch = '(|(msSFU30GidNumber=*)(msSFU30UidNumber=*))'
r = self.search_s_reconn(dn, ldap.SCOPE_BASE, srch,
attrlist=['memberOf'])
if r == []:
return []
grplist = []
if 'memberOf' in r[0][1]:
grplist = r[0][1]['memberOf']
if mssfu:
srch = '(msSFU30GidNumber=*)'
tmplist = []
for g in grplist:
self.logger.debug('testing msSFU for %s' % g)
r2 = self.search_s_reconn(g, ldap.SCOPE_BASE, srch)
if len(r2):
tmplist.append(g)
grplist = tmplist
if recursive:
tmplist = []
for g in grplist:
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
# reslist = []defu
# for dn, entry in r:
# reslist.append(dn)
# if returndn:
# return reslist
def get_groups_for_user(self, user, gfilter='*', ou=None, returndn = False,
mssfu=False):
"""Get groups for a particular user from LDAP.
The function will try to determine whether it receives a DN or
a system username that needs to be converted to a DN first.
@@ -258,16 +321,19 @@ class LdapUserDir(object):
@param gfilter A filter expression used for the cn part of the ldap dn
@param ou The organisational unit to be used in the ldap search
@param returndn If set True the function will return DN, otherwise CN
@param mssfu Whether to only show users with mssfu mappings
@returns list of group names
"""
if ou == None:
group_ou = self.group_ou
if not ',' in user:
if not self.has_dn_format(user):
dnname = self.systemuser2dn(user)
else:
dnname = user
srch = self.searches['get_groups_for_user'] % (gfilter, dnname)
if mssfu:
srch = '(&(objectClass=Group)(msSFU30GidNumber=*)(cn=%s)(member=%s))' % (gfilter, dnname)
srch = '(&(objectClass=Group)(cn=%s)(member=%s))' % (gfilter, dnname)
self.logger.debug('get_groups_for_user: %s' % srch)
r = self.search_s_reconn(group_ou, ldap.SCOPE_SUBTREE, srch)
@@ -290,12 +356,12 @@ class LdapUserDir(object):
return cnlist
def list_groups(self, filter = '*', ou = None):
def list_groups(self, filter = '*', ou = None, mssfu=False):
"""Prints a list of groups from the LDAP directory
@param filter A filter expression used for the cn part of the ldap dn
@param ou The organisational unit to be used in the ldap search
"""
r = self.get_groups_struct(filter, ou)
r = self.get_groups_struct(filter, ou, mssfu)
if len(r) == 0:
sys.stderr.write("Error: no groups found (filter: %s)\n" % filter)
return 0
@@ -312,11 +378,11 @@ class LdapUserDir(object):
@param dngroup DN of the group
@param usernames List of usernames (system names or DNs)
"""
if not ',' in dngroup:
if not self.has_dn_format(dngroup):
dngroup = ''.join(['cn=', dngroup, ',', self.group_ou])
dnlist = []
for name in usernames:
if not ',' in name:
if not self.has_dn_format(name):
dnname = self.systemuser2dn(name)
if dnname == '':
raise RuntimeError('Error: No such ldap user: %s' % name)

View File

@@ -1 +1 @@
__version__ = "1.20"
__version__ = "2.0.0"

View File

@@ -7,8 +7,8 @@ from setuptools import setup, find_packages
# we get the package version from inside our package, since we then
# can use it also from within the package
from ldapuserdir.version import __version__
#execfile('ldapuserdir/version.py')
#from ldapuserdir.version import __version__
execfile('ldapuserdir/version.py')
setup(
name = "ldapuserdir",