4 This module is intended to be a thin wrapper around LDAP operations.
5 Methods on the connection object correspond in a straightforward way
6 to LDAP queries and updates.
8 A LDAP entry is the most important component of a CSC UNIX account.
9 The entry contains the username, user id number, real name, shell,
10 and other important information. All non-local UNIX accounts must
11 have an LDAP entry, even if the account does not log in directly.
13 This module makes use of python-ldap, a Python module with bindings
14 to libldap, OpenLDAP's native C client library.
19 class LDAPException(Exception):
20 """Exception class for LDAP-related errors."""
23 class LDAPConnection(object):
25 Connection to the LDAP directory. All directory
26 queries and updates are made via this class.
28 Exceptions: (all methods)
29 LDAPException - on directory query failure
32 connection = LDAPConnection()
33 connection.connect(...)
35 # make queries and updates, e.g.
36 connection.user_delete('mspang')
38 connection.disconnect()
45 def connect(self, server, bind_dn, bind_pw, user_base, group_base):
47 Establish a connection to the LDAP Server.
50 server - connection string (e.g. ldap://foo.com, ldaps://bar.com)
51 bind_dn - distinguished name to bind to
52 bind_pw - password of bind_dn
53 user_base - base of the users subtree
54 group_base - baes of the group subtree
56 Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
57 'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
58 'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
62 if bind_pw is None: bind_pw = ''
67 self.ldap = ldap.initialize(server)
70 self.ldap.simple_bind_s(bind_dn, bind_pw)
72 except ldap.LDAPError, e:
73 raise LDAPException("unable to connect: %s" % e)
75 self.user_base = user_base
76 self.group_base = group_base
80 """Close the connection to the LDAP server."""
88 except ldap.LDAPError, e:
89 raise LDAPException("unable to disconnect: %s" % e)
93 """Determine whether the connection has been established."""
95 return self.ldap is not None
99 ### Helper Methods ###
101 def lookup(self, dn):
103 Helper method to retrieve the attributes of an entry.
106 dn - the distinguished name of the directory entry
108 Returns: a dictionary of attributes of the matched dn, or
109 None of the dn does not exist in the directory
112 # search for the specified dn
114 matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
115 except ldap.NO_SUCH_OBJECT:
117 except ldap.LDAPError, e:
118 raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
120 # this should never happen due to the nature of DNs
122 raise LDAPException("duplicate dn in ldap: " + dn)
124 # return the attributes of the single successful match
127 match_dn, match_attributes = match
128 return match_attributes
132 ### User-related Methods ###
134 def user_lookup(self, uid):
136 Retrieve the attributes of a user.
139 uid - the UNIX username to look up
141 Returns: attributes of user with uid
143 Example: connection.user_lookup('mspang') ->
144 { 'uid': 'mspang', 'uidNumber': 21292 ...}
147 if not self.connected(): raise LDAPException("Not connected!")
149 dn = 'uid=' + uid + ',' + self.user_base
150 return self.lookup(dn)
153 def user_search(self, search_filter):
155 Helper for user searches.
158 search_filter - LDAP filter string to match users against
160 Returns: the list of uids matched (usernames)
163 # search for entries that match the filter
165 matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
166 except ldap.LDAPError, e:
167 raise LDAPException("user search failed: %s" % e)
169 # list for uids found
172 for match in matches:
173 dn, attributes = match
175 # uid is a required attribute of posixAccount
176 if not attributes.has_key('uid'):
177 raise LDAPException(dn + ' (posixAccount) has no uid')
179 # do not handle the case of multiple usernames in one entry (yet)
180 elif len(attributes['uid']) > 1:
181 raise LDAPException(dn + ' (posixAccount) has multiple uids')
183 # append the sole uid of this match to the list
184 uids.append( attributes['uid'][0] )
189 def user_search_id(self, uidNumber):
191 Retrieves a list of users with a certain UNIX uid number.
193 LDAP (or passwd for that matter) does not enforce any
194 restriction on the number of accounts that can have
195 a certain UID. Therefore this method returns a list of matches.
198 uidNumber - the user id of the accounts desired
200 Returns: the list of uids matched (usernames)
202 Example: connection.user_search_id(21292) -> ['mspang']
205 # search for posixAccount entries with the specified uidNumber
206 search_filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
207 return self.user_search(search_filter)
210 def user_search_gid(self, gidNumber):
212 Retrieves a list of users with a certain UNIX gid
213 number (search by default group).
215 Returns: the list of uids matched (usernames)
218 # search for posixAccount entries with the specified gidNumber
219 search_filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
220 return self.user_search(search_filter)
223 def user_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None):
225 Adds a user to the directory.
228 uid - the UNIX username for the account
229 cn - the real name of the member
230 uidNumber - the UNIX user id number
231 gidNumber - the UNIX group id number (default group)
232 homeDirectory - home directory for the user
233 loginShell - login shell for the user
234 gecos - comment field (usually stores name etc)
235 description - description field (optional and unimportant)
237 Example: connection.user_add('mspang', 'Michael Spang',
238 21292, 100, '/users/mspang', '/bin/bash',
242 dn = 'uid=' + uid + ',' + self.user_base
244 'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
247 'loginShell': [ loginShell ],
248 'uidNumber': [ str(uidNumber) ],
249 'gidNumber': [ str(gidNumber) ],
250 'homeDirectory': [ homeDirectory ],
255 attrs['loginShell'] = loginShell
257 attrs['description'] = [ description ]
260 modlist = ldap.modlist.addModlist(attrs)
261 self.ldap.add_s(dn, modlist)
262 except ldap.LDAPError, e:
263 raise LDAPException("unable to add: %s" % e)
266 def user_modify(self, uid, attrs):
268 Update user attributes in the directory.
271 uid - username of the user to modify
272 attrs - dictionary as returned by user_lookup() with changes to make.
273 omitted attributes are DELETED.
275 Example: user = user_lookup('mspang')
276 user['uidNumber'] = [ '0' ]
277 connection.user_modify('mspang', user)
280 # distinguished name of the entry to modify
281 dn = 'uid=' + uid + ',' + self.user_base
283 # retrieve current state of user
284 old_user = self.user_lookup(uid)
288 # build list of modifications to make
289 changes = ldap.modlist.modifyModlist(old_user, attrs)
292 self.ldap.modify_s(dn, changes)
294 except ldap.LDAPError, e:
295 raise LDAPException("unable to modify: %s" % e)
298 def user_delete(self, uid):
300 Removes a user from the directory.
302 Example: connection.user_delete('mspang')
306 dn = 'uid=' + uid + ',' + self.user_base
307 self.ldap.delete_s(dn)
308 except ldap.LDAPError, e:
309 raise LDAPException("unable to delete: %s" % e)
313 ### Group-related Methods ###
315 def group_lookup(self, cn):
317 Retrieves the attributes of a group.
320 cn - the UNIX group name to lookup
322 Returns: attributes of the group's LDAP entry
324 Example: connection.group_lookup('office') -> {
331 dn = 'cn=' + cn + ',' + self.group_base
332 return self.lookup(dn)
335 def group_search_id(self, gidNumber):
337 Retrieves a list of groups with the specified UNIX group number.
339 Returns: a list of groups with gid gidNumber
341 Example: connection.group_search_id(1001) -> ['office']
344 # search for posixAccount entries with the specified uidNumber
346 search_filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
347 matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, search_filter)
348 except ldap.LDAPError,e :
349 raise LDAPException("group search failed: %s" % e)
351 # list for groups found
354 for match in matches:
355 dn, attributes = match
357 # cn is a required attribute of posixGroup
358 if not attributes.has_key('cn'):
359 raise LDAPException(dn + ' (posixGroup) has no cn')
361 # do not handle the case of multiple cns for one group (yet)
362 elif len(attributes['cn']) > 1:
363 raise LDAPException(dn + ' (posixGroup) has multiple cns')
365 # append the sole uid of this match to the list
366 group_cns.append( attributes['cn'][0] )
371 def group_add(self, cn, gidNumber, description=None):
373 Adds a group to the directory.
375 Example: connection.group_add('office', 1001, 'Office Staff')
378 dn = 'cn=' + cn + ',' + self.group_base
380 'objectClass': [ 'top', 'posixGroup' ],
382 'gidNumber': [ str(gidNumber) ],
385 attrs['description'] = description
388 modlist = ldap.modlist.addModlist(attrs)
389 self.ldap.add_s(dn, modlist)
390 except ldap.LDAPError, e:
391 raise LDAPException("unable to add group: %s" % e)
394 def group_modify(self, cn, attrs):
396 Update group attributes in the directory.
398 The only available updates are fairly destructive (rename or renumber)
399 but this method is provided for completeness.
402 cn - name of the group to modify
403 entry - dictionary as returned by group_lookup() with changes to make.
404 omitted attributes are DELETED.
406 Example: group = group_lookup('office')
407 group['gidNumber'] = [ str(connection.first_id(20000, 40000)) ]
408 del group['memberUid']
409 connection.group_modify('office', group)
412 # distinguished name of the entry to modify
413 dn = 'cn=' + cn + ',' + self.group_base
415 # retrieve current state of group
416 old_group = self.group_lookup(cn)
420 # build list of modifications to make
421 changes = ldap.modlist.modifyModlist(old_group, attrs)
424 self.ldap.modify_s(dn, changes)
426 except ldap.LDAPError, e:
427 raise LDAPException("unable to modify: %s" % e)
430 def group_delete(self, cn):
432 Removes a group from the directory."
434 Example: connection.group_delete('office')
438 dn = 'cn=' + cn + ',' + self.group_base
439 self.ldap.delete_s(dn)
440 except ldap.LDAPError, e:
441 raise LDAPException("unable to delete group: %s" % e)
444 ### Miscellaneous Methods ###
446 def used_uids(self, minimum=None, maximum=None):
448 Compiles a list of used UIDs in a range.
451 minimum - smallest uid to return in the list
452 maximum - largest uid to return in the list
454 Returns: list of integer uids
456 Example: connection.used_uids(20000, 40000) -> [20000, 20001, ...]
460 users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['uidNumber'])
461 except ldap.LDAPError, e:
462 raise LDAPException("search for uids failed: %s" % e)
467 uid = int(attrs['uidNumber'][0])
468 if (not minimum or uid >= minimum) and (not maximum or uid <= maximum):
474 def used_gids(self, minimum=None, maximum=None):
476 Compiles a list of used GIDs in a range.
479 minimum - smallest gid to return in the list
480 maximum - largest gid to return in the list
482 Returns: list of integer gids
484 Example: connection.used_gids(20000, 40000) -> [20000, 20001, ...]
488 users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['gidNumber'])
489 except ldap.LDAPError, e:
490 raise LDAPException("search for gids failed: %s" % e)
495 gid = int(attrs['gidNumber'][0])
496 if (not minimum or gid >= minimum) and (not maximum or gid <= maximum):
505 if __name__ == '__main__':
507 from csc.common.test import *
509 conffile = '/etc/csc/ldap.cf'
510 cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
511 srvurl = cfg['server_url'][1:-1]
512 binddn = cfg['admin_bind_dn'][1:-1]
513 bindpw = cfg['admin_bind_pw'][1:-1]
514 ubase = cfg['users_base'][1:-1]
515 gbase = cfg['groups_base'][1:-1]
519 # t=test u=user g=group c=changed r=real e=expected
521 turname = 'Test User'
522 tuhome = '/home/testuser'
523 tushell = '/bin/false'
524 tugecos = 'Test User,,,'
526 cushell = '/bin/true'
527 cuhome = '/home/changed'
528 curname = 'Test Modified User'
530 test("LDAPConnection()")
531 connection = LDAPConnection()
535 connection.disconnect()
539 connection.connect(srvurl, binddn, bindpw, ubase, gbase)
540 if not connection.connected():
541 fail("not connected")
545 connection.user_delete(tuname)
546 connection.group_delete(tgname)
547 except LDAPException:
551 uids = connection.used_uids(minid, maxid)
552 if type(uids) is not list:
553 fail("list not returned")
557 gids = connection.used_gids(minid, maxid)
558 if type(gids) is not list:
559 fail("list not returned")
563 for idnum in xrange(minid, maxid):
564 if not idnum in uids and not idnum in gids:
565 unusedids.append(idnum)
567 tuuid = unusedids.pop()
568 tugid = unusedids.pop()
571 'loginShell': [ tushell ],
572 'uidNumber': [ str(tuuid) ],
573 'gidNumber': [ str(tugid) ],
574 'gecos': [ tugecos ],
575 'homeDirectory': [ tuhome ],
580 connection.user_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
583 tggid = unusedids.pop()
586 'gidNumber': [ str(tggid) ]
590 connection.group_add(tgname, tggid)
593 test("user_lookup()")
594 udata = connection.user_lookup(tuname)
595 del udata['objectClass']
596 assert_equal(eudata, udata)
599 test("group_lookup()")
600 gdata = connection.group_lookup(tgname)
601 del gdata['objectClass']
602 assert_equal(egdata, gdata)
605 test("user_search_id()")
607 ulist = connection.user_search_id(tuuid)
608 assert_equal(eulist, ulist)
611 test("user_search_gid()")
612 ulist = connection.user_search_gid(tugid)
613 if tuname not in ulist:
614 fail("(%s) not in (%s)" % (tuname, ulist))
617 ecudata = connection.user_lookup(tuname)
618 ecudata['loginShell'] = [ cushell ]
619 ecudata['homeDirectory'] = [ cuhome ]
620 ecudata['cn'] = [ curname ]
623 connection.user_modify(tuname, ecudata)
624 cudata = connection.user_lookup(tuname)
625 assert_equal(ecudata, cudata)
628 ecgdata = connection.group_lookup(tgname)
629 ecgdata['memberUid'] = [ tuname ]
631 test("group_modify()")
632 connection.group_modify(tgname, ecgdata)
633 cgdata = connection.group_lookup(tgname)
634 assert_equal(ecgdata, cgdata)
637 test("user_delete()")
638 connection.group_delete(tgname)
642 connection.disconnect()