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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.20"
|
||||
__version__ = "2.0.0"
|
||||
|
||||
4
setup.py
4
setup.py
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user