refactored ldapuserdir code after splitting from sigateway project

This commit is contained in:
2012-09-28 12:12:03 +02:00
commit d5311440f2
7 changed files with 632 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
*~
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
sdist
# Installer logs
pip-log.txt

298
bin/ldapuserdir-ctl Executable file
View File

@@ -0,0 +1,298 @@
#!/usr/bin/python
import logging
from ldapuserdir import LdapUserDir
import ldap
import sys
import os
from optparse import OptionParser
import ConfigParser
import getpass
def read_cfg(filename):
cfg = ConfigParser.ConfigParser()
try:
mylogger.debug('reading config from %s' % filename)
cfg.read(filename)
config = {
'serverurl' : cfg.get('Ldap','serverurl'),
'user_ou' : cfg.get('Ldap','user_ou'),
'group_ou' : cfg.get('Ldap','group_ou'),
'default_user_dn' : cfg.get('Ldap','default_user_dn'),
'default_user_pw' : cfg.get('Ldap','default_user_pw'),
}
except Exception, err:
sys.stderr.write("Error in reading configuration from %s\n" % filename)
sys.stderr.write(str(err)+'\n')
sys.exit(1)
try:
config['default_group_filter'] = cfg.get('Ldap','default_group_filter')
except:
config['default_group_filter'] = '*'
return config
#Defaults
cfgfile_loc = [os.path.expanduser('~/.ldapuserdir-ctl.cfg'),
'/etc/ldapuserdir-ctl.cfg']
config = {
'serverurl' : 'ldaps://xyzdir.example.com:636',
'user_ou' : 'OU=Users,DC=example.com,DC=ch',
'group_ou' : 'OU=Groups,DC=example.com,DC=ch',
'default_user_dn' : 'CN=minpriv_user,OU=Services,DC=example.com,DC=ch',
'default_user_pw' : 'dummypwd',
'default_group_filter' : 'svc-ra*'
}
flag_needprivileges = False
userfilter = '-'
user_pw = ''
mode = 'list'
mylogger = logging.getLogger(os.path.basename(sys.argv[0]))
mylogger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(name)s %(levelname)s: %(message)s')
ch = logging.StreamHandler()
ch.setLevel(logging.WARNING)
ch.setFormatter(formatter)
mylogger.addHandler(ch)
################################################
# OPTION PARSING
usage = """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)
Examples:
List group members
%prog svc_ra_x06sa
%prog 'svc_ra_*'
Get group memberships for user mueller
%prog -g mueller
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
List users matching a pattern
%prog -u 'mueller*'
The configuration is read from a configuration file (default
locations: """
usage += ", ".join(cfgfile_loc) + ')\n'
usage += """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 = TBVsK5zOfqMyxVmXco7y
# Optional:
# default filter to be used for group searches
default_group_filter = svc-ra*
"""
parser = OptionParser(usage=usage)
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('-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('-v',
action = 'store_true',
dest = 'flag_verbose',
help = 'use more verbose output',
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('--no-msSFU',
action = 'store_true',
dest = 'flag_nosfu',
help = 'do not restrict to entries with unix (msSFU) mappings',
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)
cfgfile = None
if(options.cfgfile != ''):
cfgfile = options.cfgfile
else:
for tmp in cfgfile_loc:
if os.path.exists(tmp):
cfgfile = tmp
if cfgfile == None:
sys.stderr.write('Error: You must provide a config file (default locations '
+ ', '.join(cfgfile_loc) + ' or explicitely)\n')
sys.exit(1)
config = read_cfg(cfgfile)
user_dn = config['default_user_dn']
if options.user_dn:
config['user_dn'] = options.user_dn
if options.group_ou:
config['group_ou'] = options.group_ou
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:
mode = 'del'
flag_needprivileges = True
if options.flag_add:
mode = 'add'
flag_needprivileges = True
if userfilter:
mode = 'userlist'
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)
if ',' not in user_dn:
user_dn = 'CN=' + user_dn + ',' + user_ou
if flag_needprivileges and user_dn == config['default_user_dn']:
try:
l_unpriv = LdapUserDir(config['serverurl'],
config['default_user_dn'],
config['default_user_pw'],
user_ou = config['user_ou'],
logger = mylogger)
user_dn = l_unpriv.systemuser2dn(os.getlogin())
if user_dn == '':
sys.stderr.write('Error: Need priviledged user and cannot map your system user "%s" to LDAP DN for binding (you may want to use explicit -D user_dn option?)' % os.getlogin() )
sys.exit(1)
except ldap.LDAPError, e:
sys.stderr.write('LDAP error: %s\n' % str(e))
sys.exit(1)
if user_dn == config['default_user_dn']:
user_pw = config['default_user_pw']
if options.pwfile != '':
pwf = open(options.pwfile)
user_pw = pwf.readline().rstrip('\n')
if user_pw == '':
user_pw = getpass.getpass()
if user_pw == '':
sys.stderr.write('Error: No empty passwd allowed\n')
# Note: AD accepts empty passwds, but will give anonymous access
# So, we want to catch that case
sys.exit(1)
try:
l = LdapUserDir(config['serverurl'],
user_dn,
user_pw,
config['group_ou'],
config['user_ou'],
sfu = flag_sfu,
logger=mylogger)
if mode == 'list':
sfilter = config['default_group_filter']
if group:
sfilter = group
l.list_groups(sfilter)
elif mode == 'userlist':
l.list_users_etcpwd(userfilter, verbose = flag_verbose)
elif mode == 'user_to_group':
sys.stdout.write("\n".join(l.get_groups_for_user(user_to_group))
+ "\n")
elif mode == 'add':
l.add_groupmembers(group, usernames)
elif mode == 'del':
l.del_groupmembers(group, usernames)
except ldap.INVALID_CREDENTIALS, e:
sys.exit(1)
except ldap.LDAPError, e:
sys.stderr.write('Unhandled LDAP error: %s\n' % str(e))
sys.exit(1)
except:
sys.stderr.write('Unhandled Exception!!!!!!!!\n')
raise
sys.exit(0)

18
etc/ldapuserdir-ctl.cfg Normal file
View File

@@ -0,0 +1,18 @@
[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 = secret_pwd
# Optional
# default filter to be used for group searches
default_group_filter = svc-ra*

1
ldapuserdir/__init__.py Normal file
View File

@@ -0,0 +1 @@
from ldapuserdir import LdapUserDir

267
ldapuserdir/ldapuserdir.py Normal file
View File

@@ -0,0 +1,267 @@
#!/usr/bin/env python
######################################################################
# Tool for modifying group memberships in AD
#
# Author: Derek Feichtinger <derek.feichtinger@psi.ch>
#
# Version info: $Id: ldapgroupmng.py 65 2012-09-25 15:35:38Z feichtinger $
######################################################################
""" This module provides the LdapUserDir class. It is used to interact
with an LDAP based user directory service
"""
import ldap
import os
import sys
import re
import logging
##############################################################
# 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 LdapUserDir(object):
""" A class to interact with a LDAP based user and group directory
"""
def __init__(self,
serverurl,
user_dn,
user_pw,
group_ou = 'ou=example.com',
user_ou = 'ou=example.com',
sfu = True,
logger = None):
self.serverurl = serverurl
self.group_ou = group_ou
self.user_ou = user_ou
# 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)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setFormatter(formatter)
ch.setLevel(logging.DEBUG)
self.logger.addHandler(ch)
else:
self.logger = logger
self._ldap = ldap.initialize(self.serverurl, trace_level=0,
trace_file=sys.stderr)
self.logger.debug('binding to: %s\n' % serverurl)
self.logger.debug('binding as user: %s\n' % user_dn)
try:
self._ldap.bind_s(user_dn, user_pw)
except ldap.INVALID_CREDENTIALS, e:
self.logger.error('Authentication failure for dn:"%s"\n' % user_dn)
raise
# need to clean that later
except ldap.LDAPError, e:
raise
def get_users(self, filter='*', ou=None):
"""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
@returns A dictionary of the matching users { dn1:list1, ... }
"""
if ou == None:
user_ou = self.user_ou
#try:
r = self._ldap.search_s(user_ou, ldap.SCOPE_SUBTREE,
self.searches['get_users'] % filter)
#except ldap.LDAPError, e:
# print e
# return
return r
def list_users_etcpwd(self, filter='*', ou=None, verbose = False):
"""Print '/etc/pwd' format like information about matching users
@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)
for dn, entry in r:
# MUST fields
try:
print ':'.join([entry['cn'][0], entry['msSFU30UidNumber'][0],
'IGNORE',
entry['msSFU30GidNumber'][0],
entry['displayName'][0],
entry['msSFU30HomeDirectory'][0],
entry['msSFU30LoginShell'][0],
]),
except KeyError:
print ':'.join([entry['cn'][0],"","",entry['displayName'][0],
"",""])
if verbose:
for k in ['description', 'mail', 'mobile']:
print '[%s:]' % (k,) ,
if k in entry:
print entry[k][0],
else:
print 'N.A.',
print
def systemuser2dn(self, uname):
"""Converts a user's system username to the dn of the ldap directory
by performing a search on ldap
@param uname The system username
@returns The DN of the user or '' if no matching record was found
@exception may throw an ldap.LDAPError exception
"""
#try:
srch = self.searches['systemuser2dn'] % uname
self.logger.debug('systemuser2dn: %s' % srch)
r = self._ldap.search_s(self.user_ou, ldap.SCOPE_SUBTREE, srch)
#except ldap.LDAPError, e:
# print e
if len(r) == 0:
return ''
self.logger.debug('systemuser2dn: dn = %s' % r[0][0])
return r[0][0]
def get_groups_struct(self, gfilter='*', ou = None):
"""searches for groups
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
@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
self.logger.debug('get_groups_struct: %s' % srch)
r = self._ldap.search_s(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):
"""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.
@param user The user's DN or system name
@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
@returns list of group names
"""
if ou == None:
group_ou = self.group_ou
if not ',' in user:
dnname = self.systemuser2dn(user)
else:
dnname = user
srch = self.searches['get_groups_for_user'] % (gfilter, dnname)
self.logger.debug('get_groups_for_user: %s' % srch)
r = self._ldap.search_s(group_ou, ldap.SCOPE_SUBTREE, srch)
reslist = []
for dn, entry in r:
reslist.append(dn)
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):
"""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)
if len(r) == 0:
sys.stderr.write("Error: no groups found (filter: %s)\n" % filter)
return 0
for dn, entry in r:
print entry['cn'][0]
if 'member' in entry:
for cn in entry['member']:
print ' member: ', cn
def _mod_groupmembers(self, ldapmode, dngroup, usernames):
"""modifies members of an LDAP group entry
@param ldapmode Either ldap.MOD_ADD, or ldap.MOD_DELETE
@param dngroup DN of the group
@param usernames List of usernames (system names or DNs)
"""
if not ',' in dngroup:
dngroup = ''.join(['cn=', dngroup, ',', self.group_ou])
dnlist = []
for name in usernames:
if not ',' in name:
dnname = self.systemuser2dn(name)
if dnname == '':
raise RuntimeError('Error: No such ldap user: %s' % name)
dnlist.append(dnname)
else:
dnlist.append(name)
mod_attrs = [(ldapmode, 'member', dnlist)]
try:
self._ldap.modify_s(dngroup, mod_attrs)
except ldap.ALREADY_EXISTS, e:
self.logger.error('Entry already exists in group %s' % dngroup)
raise
except ldap.LDAPError, e:
self.logger.error('ERROR modifying LDAP: group: %s; user list: %s\n'
% (dngroup, dnlist))
raise
def add_groupmembers(self, group, usernames):
"""Adds users to an LDAP group
@param dngroup DN of the group
@param usernames List of usernames (system names or DNs)
"""
self._mod_groupmembers(ldap.MOD_ADD, group, usernames)
def del_groupmembers(self, group, usernames):
"""Deletes users from an LDAP group
@param dngroup DN of the group
@param usernames List of usernames (system names or DNs)
"""
self._mod_groupmembers(ldap.MOD_DELETE, group, usernames)
def __del__(self):
self._ldap.unbind()

1
ldapuserdir/version.py Normal file
View File

@@ -0,0 +1 @@
__version__ = "0.1"

31
setup.py Normal file
View File

@@ -0,0 +1,31 @@
#from setuptools import setup, find_packages
#from distutils.core import setup
# we use the distribute framework that has a backward compatible invocation
# to the setuptools
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
execfile('ldapuserdir/version.py')
setup(
name = "ldapuserdir",
version = __version__,
description = "Client for interacting with a LDAP user/group directory service",
long_description = "Client for listing user and group information and"
+ " for managing group memberships",
author = "Derek Feichtinger",
author_email = "derek.feichtinger@psi.ch",
license = "GPL",
# url =
#packages = find_packages('sigateway'),
packages = ['ldapuserdir'],
scripts = ['bin/ldapuserdir-ctl'],
# following format is (targetdir,[list of files])
data_files = [('etc',['etc/ldapuserdir-ctl.cfg'])],
install_requires = ['python-ldap']
)