Simplify sasl code
[mspang/pyceo.git] / pylib / csc / backends / ldapi.py
1 """
2 LDAP Backend Interface
3
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.
7
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.
12
13 This module makes use of python-ldap, a Python module with bindings
14 to libldap, OpenLDAP's native C client library.
15 """
16 import ldap.modlist
17 from subprocess import Popen, PIPE
18
19
20 class LDAPException(Exception):
21     """Exception class for LDAP-related errors."""
22
23
24 class LDAPConnection(object):
25     """
26     Connection to the LDAP directory. All directory
27     queries and updates are made via this class.
28
29     Exceptions: (all methods)
30         LDAPException - on directory query failure
31
32     Example:
33          connection = LDAPConnection()
34          connection.connect(...)
35
36          # make queries and updates, e.g.
37          connection.user_delete('mspang')
38
39          connection.disconnect()
40     """
41
42     def __init__(self):
43         self.ldap = None
44
45
46     def connect_anon(self, uri, user_base, group_base):
47         """
48         Establish a connection to the LDAP Server.
49
50         Parameters:
51             uri        - connection string (e.g. ldap://foo.com, ldaps://bar.com)
52             user_base  - base of the users subtree
53             group_base - baes of the group subtree
54
55         Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
56                      'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
57                      'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
58
59         """
60
61         # open the connection
62         self.ldap = ldap.initialize(uri)
63
64         # authenticate
65         self.ldap.simple_bind_s('', '')
66
67         self.user_base = user_base
68         self.group_base = group_base
69
70     def connect_sasl(self, uri, mech, realm, userid, password, user_base, group_base):
71
72         # open the connection
73         self.ldap = ldap.initialize(uri)
74
75         # authenticate
76         sasl = Sasl(mech, realm, userid, password)
77         self.ldap.sasl_interactive_bind_s('', sasl)
78
79         self.user_base = user_base
80         self.group_base = group_base
81
82
83     def disconnect(self):
84         """Close the connection to the LDAP server."""
85         
86         if self.ldap:
87
88             # close connection
89             try:
90                 self.ldap.unbind_s()
91                 self.ldap = None
92             except ldap.LDAPError, e:
93                 raise LDAPException("unable to disconnect: %s" % e)
94
95
96     def connected(self):
97         """Determine whether the connection has been established."""
98
99         return self.ldap is not None
100
101
102
103     ### Helper Methods ###
104
105     def lookup(self, dn, objectClass=None):
106         """
107         Helper method to retrieve the attributes of an entry.
108
109         Parameters:
110             dn - the distinguished name of the directory entry
111
112         Returns: a dictionary of attributes of the matched dn, or
113                  None of the dn does not exist in the directory
114         """
115
116         if not self.connected(): raise LDAPException("Not connected!")
117
118         # search for the specified dn
119         try:
120             if objectClass:
121                 search_filter = '(objectClass=%s)' % self.escape(objectClass)
122                 matches = self.ldap.search_s(dn, ldap.SCOPE_BASE, search_filter)
123             else:
124                 matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
125         except ldap.NO_SUCH_OBJECT:
126             return None
127         except ldap.LDAPError, e:
128             raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
129             
130         # this should never happen due to the nature of DNs
131         if len(matches) > 1:
132             raise LDAPException("duplicate dn in ldap: " + dn)
133
134         # dn was found, but didn't match the objectClass filter
135         elif len(matches) < 1:
136             return None
137
138         # return the attributes of the single successful match
139         match = matches[0]
140         match_dn, match_attributes = match
141         return match_attributes
142
143
144
145     ### User-related Methods ###
146
147     def user_lookup(self, uid, objectClass=None):
148         """
149         Retrieve the attributes of a user.
150
151         Parameters:
152             uid - the uid to look up
153
154         Returns: attributes of user with uid
155         """
156
157         dn = 'uid=' + uid + ',' + self.user_base
158         return self.lookup(dn, objectClass)
159
160
161     def user_search(self, search_filter, params):
162         """
163         Search for users with a filter.
164
165         Parameters:
166             search_filter - LDAP filter string to match users against
167
168         Returns: a dictionary mapping uids to attributes
169         """
170
171         if not self.connected(): raise LDAPException("Not connected!")
172
173         search_filter = search_filter % tuple(self.escape(x) for x in params)
174
175         # search for entries that match the filter
176         try:
177             matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
178         except ldap.LDAPError, e:
179             raise LDAPException("user search failed: %s" % e)
180
181         results = {}
182         for match in matches:
183             dn, attrs = match
184             uid = attrs['uid'][0]
185             results[uid] = attrs
186
187         return results
188
189
190     def user_modify(self, uid, attrs):
191         """
192         Update user attributes in the directory.
193
194         Parameters:
195             uid   - username of the user to modify
196             attrs - dictionary as returned by user_lookup() with changes to make.
197                     omitted attributes are DELETED.
198
199         Example: user = user_lookup('mspang')
200                  user['uidNumber'] = [ '0' ]
201                  connection.user_modify('mspang', user)
202         """
203
204         # distinguished name of the entry to modify
205         dn = 'uid=' + uid + ',' + self.user_base
206
207         # retrieve current state of user
208         old_user = self.user_lookup(uid)
209
210         try:
211
212             # build list of modifications to make
213             changes = ldap.modlist.modifyModlist(old_user, attrs)
214
215             # apply changes
216             self.ldap.modify_s(dn, changes)
217
218         except ldap.LDAPError, e:
219             raise LDAPException("unable to modify: %s" % e)
220
221
222     def user_delete(self, uid):
223         """
224         Removes a user from the directory.
225
226         Example: connection.user_delete('mspang')
227         """
228
229         try:
230             dn = 'uid=' + uid + ',' + self.user_base
231             self.ldap.delete_s(dn)
232         except ldap.LDAPError, e:
233             raise LDAPException("unable to delete: %s" % e)
234
235
236
237     ### Account-related Methods ###
238
239     def account_lookup(self, uid):
240         """
241         Retrieve the attributes of an account.
242
243         Parameters:
244             uid - the uid to look up
245
246         Returns: attributes of user with uid
247         """
248
249         return self.user_lookup(uid, 'posixAccount')
250
251
252     def account_search_id(self, uidNumber):
253         """
254         Retrieves a list of accounts with a certain UNIX uid number.
255
256         LDAP (or passwd for that matter) does not enforce any restriction on
257         the number of accounts that can have a certain UID number. Therefore
258         this method returns a list of matches.
259
260         Parameters:
261             uidNumber - the user id of the accounts desired
262
263         Returns: a dictionary mapping uids to attributes
264
265         Example: connection.account_search_id(21292) -> {'mspang': { ... }}
266         """
267
268         search_filter = '(&(objectClass=posixAccount)(uidNumber=%s))'
269         return self.user_search(search_filter, [ uidNumber ])
270
271
272     def account_search_gid(self, gidNumber):
273         """
274         Retrieves a list of accounts with a certain UNIX gid
275         number (search by default group).
276
277         Returns: a dictionary mapping uids to attributes
278         """
279
280         search_filter = '(&(objectClass=posixAccount)(gidNumber=%s))'
281         return self.user_search(search_filter, [ gidNumber ])
282
283
284     def account_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None, update=False):
285         """
286         Adds a user account to the directory.
287
288         Parameters:
289             uid           - the UNIX username for the account
290             cn            - the real name of the member
291             uidNumber     - the UNIX user id number
292             gidNumber     - the UNIX group id number (default group)
293             homeDirectory - home directory for the user
294             loginShell    - login shell for the user
295             gecos         - comment field (usually stores name etc)
296             description   - description field (optional and unimportant)
297             update        - if True, will update existing entries
298
299         Example: connection.user_add('mspang', 'Michael Spang',
300                      21292, 100, '/users/mspang', '/bin/bash',
301                      'Michael Spang,,,')
302         """
303
304         if not self.connected(): raise LDAPException("Not connected!")
305
306         dn = 'uid=' + uid + ',' + self.user_base
307         attrs = {
308             'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
309             'uid': [ uid ],
310             'cn': [ cn ],
311             'loginShell': [ loginShell ],
312             'uidNumber': [ str(uidNumber) ],
313             'gidNumber': [ str(gidNumber) ],
314             'homeDirectory': [ homeDirectory ],
315             'gecos': [ gecos ],
316         }
317
318         if loginShell:
319             attrs['loginShell'] = [ loginShell ]
320         if description:
321             attrs['description'] = [ description ]
322
323         try:
324
325             old_entry = self.user_lookup(uid)
326             if old_entry and 'posixAccount' not in old_entry['objectClass'] and update:
327
328                 attrs.update(old_entry)
329                 attrs['objectClass'] = list(attrs['objectClass'])
330                 attrs['objectClass'].append('posixAccount')
331                 if not 'shadowAccount' in attrs['objectClass']:
332                     attrs['objectClass'].append('shadowAccount')
333
334                 modlist = ldap.modlist.modifyModlist(old_entry, attrs)
335                 self.ldap.modify_s(dn, modlist)
336
337             else:
338
339                 modlist = ldap.modlist.addModlist(attrs)
340                 self.ldap.add_s(dn, modlist)
341
342         except ldap.LDAPError, e:
343             raise LDAPException("unable to add: %s" % e)
344
345
346
347     ### Group-related Methods ###
348
349     def group_lookup(self, cn):
350         """
351         Retrieves the attributes of a group.
352
353         Parameters:
354             cn - the UNIX group name to lookup
355
356         Returns: attributes of the group's LDAP entry
357
358         Example: connection.group_lookup('office') -> {
359                      'cn': 'office',
360                      'gidNumber', '1001',
361                      ...
362                  }
363         """
364
365         dn = 'cn=' + cn + ',' + self.group_base
366         return self.lookup(dn, 'posixGroup')
367
368
369     def group_search_id(self, gidNumber):
370         """
371         Retrieves a list of groups with the specified UNIX group number.
372         
373         Returns: a list of groups with gid gidNumber
374
375         Example: connection.group_search_id(1001) -> ['office']
376         """
377
378         if not self.connected(): raise LDAPException("Not connected!")
379
380         # search for posixAccount entries with the specified uidNumber
381         try:
382             search_filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
383             matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, search_filter)
384         except ldap.LDAPError, e:
385             raise LDAPException("group search failed: %s" % e)
386
387         # list for groups found
388         group_cns = []
389
390         results = {}
391         for match in matches:
392             dn, attrs = match
393             uid = attrs['cn'][0]
394             results[uid] = attrs
395
396         return results
397
398
399     def group_add(self, cn, gidNumber, description=None):
400         """
401         Adds a group to the directory.
402
403         Example: connection.group_add('office', 1001, 'Office Staff')
404         """
405
406         if not self.connected(): raise LDAPException("Not connected!")
407
408         dn = 'cn=' + cn + ',' + self.group_base
409         attrs = {
410             'objectClass': [ 'top', 'posixGroup', 'group' ],
411             'cn': [ cn ],
412             'gidNumber': [ str(gidNumber) ],
413         }
414         if description:
415             attrs['description'] = description
416
417         try:
418             modlist = ldap.modlist.addModlist(attrs)
419             self.ldap.add_s(dn, modlist)
420         except ldap.LDAPError, e:
421             raise LDAPException("unable to add group: %s" % e)
422
423
424     def group_modify(self, cn, attrs):
425         """
426         Update group attributes in the directory.
427         
428         The only available updates are fairly destructive (rename or renumber)
429         but this method is provided for completeness.
430
431         Parameters:
432             cn    - name of the group to modify
433             entry - dictionary as returned by group_lookup() with changes to make.
434                     omitted attributes are DELETED.
435
436         Example: group = group_lookup('office')
437                  group['gidNumber'] = [ str(connection.first_id(20000, 40000)) ]
438                  del group['memberUid']
439                  connection.group_modify('office', group)
440         """
441
442         if not self.connected(): raise LDAPException("Not connected!")
443
444         # distinguished name of the entry to modify
445         dn = 'cn=' + cn + ',' + self.group_base
446
447         # retrieve current state of group
448         old_group = self.group_lookup(cn)
449
450         try:
451             
452             # build list of modifications to make
453             changes = ldap.modlist.modifyModlist(old_group, attrs)
454
455             # apply changes
456             self.ldap.modify_s(dn, changes)
457
458         except ldap.LDAPError, e:
459             raise LDAPException("unable to modify: %s" % e)
460
461
462     def group_delete(self, cn):
463         """
464         Removes a group from the directory."
465
466         Example: connection.group_delete('office')
467         """
468
469         if not self.connected(): raise LDAPException("Not connected!")
470
471         try:
472             dn = 'cn=' + cn + ',' + self.group_base
473             self.ldap.delete_s(dn)
474         except ldap.LDAPError, e:
475             raise LDAPException("unable to delete group: %s" % e)
476
477
478
479     ### Member-related Methods ###
480
481     def member_lookup(self, uid):
482         """
483         Retrieve the attributes of a member. This method will only return
484         results that have the objectClass 'member'.
485
486         Parameters:
487             uid - the username to look up
488
489         Returns: attributes of member with uid
490
491         Example: connection.member_lookup('mspang') ->
492                      { 'uid': 'mspang', 'uidNumber': 21292 ...}
493         """
494
495         if not self.connected(): raise LDAPException("Not connected!")
496
497         dn = 'uid=' + uid + ',' + self.user_base
498         return self.lookup(dn, 'member')
499
500
501     def member_search_name(self, name):
502         """
503         Retrieves a list of members with the specified name (fuzzy).
504
505         Returns: a dictionary mapping uids to attributes
506         """
507
508         search_filter = '(&(objectClass=member)(cn~=%s))'
509         return self.user_search(search_filter, [ name ] )
510
511
512     def member_search_term(self, term):
513         """
514         Retrieves a list of members who were registered in a certain term.
515
516         Returns: a dictionary mapping uids to attributes
517         """
518
519         search_filter = '(&(objectClass=member)(term=%s))'
520         return self.user_search(search_filter, [ term ])
521
522
523     def member_search_program(self, program):
524         """
525         Retrieves a list of members in a certain program (fuzzy).
526
527         Returns: a dictionary mapping uids to attributes
528         """
529
530         search_filter = '(&(objectClass=member)(program~=%s))'
531         return self.user_search(search_filter, [ program ])
532
533
534     def member_add(self, uid, cn, program=None, description=None):
535         """
536         Adds a member to the directory.
537
538         Parameters:
539             uid           - the UNIX username for the member
540             cn            - the real name of the member
541             program       - the member's program of study
542             description   - a description for the entry
543         """
544
545         dn = 'uid=' + uid + ',' + self.user_base
546         attrs = {
547             'objectClass': [ 'top', 'account', 'member' ],
548             'uid': [ uid ],
549             'cn': [ cn ],
550         }
551
552         if program:
553             attrs['program'] = [ program ]
554         if description:
555             attrs['description'] = [ description ]
556
557         try:
558             modlist = ldap.modlist.addModlist(attrs)
559             self.ldap.add_s(dn, modlist)
560         except ldap.LDAPError, e:
561             raise LDAPException("unable to add: %s" % e)
562
563
564     def member_add_account(self, uid, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None):
565         """
566         Adds login privileges to a member.
567         """
568
569         return self.account_add(uid, None, uidNumber, gidNumber, homeDirectory, loginShell, gecos, None, True)
570
571
572
573     ### Miscellaneous Methods ###
574
575     def escape(self, value):
576         """
577         Escapes special characters in a value so that it may be safely inserted
578         into an LDAP search filter.
579         """
580
581         value = str(value)
582         value = value.replace('\\', '\\5c').replace('*', '\\2a')
583         value = value.replace('(', '\\28').replace(')', '\\29')
584         value = value.replace('\x00', '\\00')
585         return value
586
587
588     def used_uids(self, minimum=None, maximum=None):
589         """
590         Compiles a list of used UIDs in a range.
591
592         Parameters:
593             minimum - smallest uid to return in the list
594             maximum - largest uid to return in the list
595
596         Returns: list of integer uids
597
598         Example: connection.used_uids(20000, 40000) -> [20000, 20001, ...]
599         """
600
601         if not self.connected(): raise LDAPException("Not connected!")
602
603         try:
604             users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['uidNumber'])
605         except ldap.LDAPError, e:
606             raise LDAPException("search for uids failed: %s" % e)
607         
608         uids = []
609         for user in users:
610             dn, attrs = user
611             uid = int(attrs['uidNumber'][0])
612             if (not minimum or uid >= minimum) and (not maximum or uid <= maximum):
613                 uids.append(uid)
614
615         return uids
616             
617     
618     def used_gids(self, minimum=None, maximum=None):
619         """
620         Compiles a list of used GIDs in a range.
621
622         Parameters:
623             minimum - smallest gid to return in the list
624             maximum - largest gid to return in the list
625
626         Returns: list of integer gids
627
628         Example: connection.used_gids(20000, 40000) -> [20000, 20001, ...]
629         """
630
631         if not self.connected(): raise LDAPException("Not connected!")
632
633         try:
634             users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['gidNumber'])
635         except ldap.LDAPError, e:
636             raise LDAPException("search for gids failed: %s" % e)
637         
638         gids = []
639         for user in users:
640             dn, attrs = user
641             gid = int(attrs['gidNumber'][0])
642             if (not minimum or gid >= minimum) and (not maximum or gid <= maximum):
643                 gids.append(gid)
644
645         return gids
646
647
648     def make_modlist(self, old, new):
649         keys = set(old.keys()).union(set(new))
650         mlist = []
651         for key in keys:
652             if key in old and not key in new:
653                 mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
654             elif key in new and not key in old:
655                 mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
656             else:
657                 to_add = list(set(new[key]) - set(old[key]))
658                 if len(to_add) > 0:
659                     mlist.append((ldap.MOD_ADD, key, to_add))
660                 to_del = list(set(old[key]) - set(new[key]))
661                 if len(to_del) > 0:
662                     mlist.append((ldap.MOD_DELETE, key, to_del))
663         return mlist
664
665
666 class Sasl:
667
668     def __init__(self, mech, realm, userid, password):
669         self.mech = mech
670         if mech == 'GSSAPI':
671             type, arg = password
672             kinit_args = [ '/usr/bin/kinit', '%s@%s' % (userid, realm) ]
673             if type == 'keytab':
674                 kinit_args += [ '-kt', arg ]
675
676             kinit = Popen(kinit_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
677             kinit.wait()
678
679     def callback(self, id, challenge, prompt, defresult):
680         return ''
681
682
683 ### Tests ###
684
685 if __name__ == '__main__':
686     
687     from csc.common.test import *
688
689     conffile = '/etc/csc/ldap.cf'
690     cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ]) 
691     srvurl = cfg['server_url'][1:-1]
692     binddn = cfg['admin_bind_dn'][1:-1]
693     bindpw = cfg['admin_bind_pw'][1:-1]
694     ubase = cfg['users_base'][1:-1]
695     gbase = cfg['groups_base'][1:-1]
696     minid = 99999000
697     maxid = 100000000
698
699     # t=test u=user g=group c=changed r=real e=expected
700     tuname = 'testuser'
701     turname = 'Test User'
702     tuhome = '/home/testuser'
703     tushell = '/bin/false'
704     tugecos = 'Test User,,,'
705     tgname = 'testgroup'
706     tmname = 'testmember'
707     tmrname = 'Test Member'
708     tmprogram = 'UBW'
709     tmdesc = 'Test Description'
710     cushell = '/bin/true'
711     cuhome = '/home/changed'
712     curname = 'Test Modified User'
713     cmhome = '/home/testmember'
714     cmshell = '/bin/false'
715     cmgecos = 'Test Member,,,'
716
717     test(LDAPConnection)
718     connection = LDAPConnection()
719     success()
720
721     test(LDAPConnection.disconnect)
722     connection.disconnect()
723     success()
724
725     test(LDAPConnection.connect)
726     connection.connect(srvurl, binddn, bindpw, ubase, gbase)
727     if not connection.connected():
728         fail("not connected")
729     success()
730
731     try:
732         connection.user_delete(tuname)
733         connection.user_delete(tmname)
734         connection.group_delete(tgname)
735     except LDAPException:
736         pass
737
738     test(LDAPConnection.used_uids)
739     uids = connection.used_uids(minid, maxid)
740     if type(uids) is not list:
741         fail("list not returned")
742     success()
743
744     test(LDAPConnection.used_gids)
745     gids = connection.used_gids(minid, maxid)
746     if type(gids) is not list:
747         fail("list not returned")
748     success()
749
750     unusedids = []
751     for idnum in xrange(minid, maxid):
752         if not idnum in uids and not idnum in gids:
753             unusedids.append(idnum)
754
755     tuuid = unusedids.pop()
756     tugid = unusedids.pop()
757     eudata = {
758             'uid': [ tuname ],
759             'loginShell': [ tushell ],
760             'uidNumber': [ str(tuuid) ],
761             'gidNumber': [ str(tugid) ],
762             'gecos': [ tugecos ],
763             'homeDirectory': [ tuhome ],
764             'cn': [ turname ]
765             }
766
767     test(LDAPConnection.account_add)
768     connection.account_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
769     success()
770
771     emdata = {
772             'uid': [ tmname ],
773             'cn': [ tmrname ],
774             'program': [ tmprogram ],
775             'description': [ tmdesc ],
776     }
777
778     test(LDAPConnection.member_add)
779     connection.member_add(tmname, tmrname, tmprogram, tmdesc)
780     success()
781
782     tggid = unusedids.pop()
783     egdata = {
784             'cn': [ tgname ],
785             'gidNumber': [ str(tggid) ]
786             }
787
788     test(LDAPConnection.group_add)
789     connection.group_add(tgname, tggid)
790     success()
791
792     test(LDAPConnection.account_lookup)
793     udata = connection.account_lookup(tuname)
794     if udata: del udata['objectClass']
795     assert_equal(eudata, udata)
796     success()
797
798     test(LDAPConnection.member_lookup)
799     mdata = connection.member_lookup(tmname)
800     if mdata: del mdata['objectClass']
801     assert_equal(emdata, mdata)
802     success()
803
804     test(LDAPConnection.user_lookup)
805     udata = connection.user_lookup(tuname)
806     mdata = connection.user_lookup(tmname)
807     if udata: del udata['objectClass']
808     if mdata: del mdata['objectClass']
809     assert_equal(eudata, udata)
810     assert_equal(emdata, mdata)
811     success()
812
813     test(LDAPConnection.group_lookup)
814     gdata = connection.group_lookup(tgname)
815     if gdata: del gdata['objectClass']
816     assert_equal(egdata, gdata)
817     success()
818
819     test(LDAPConnection.account_search_id)
820     eulist = [ tuname ]
821     ulist = connection.account_search_id(tuuid).keys()
822     assert_equal(eulist, ulist)
823     success()
824
825     test(LDAPConnection.account_search_gid)
826     ulist = connection.account_search_gid(tugid)
827     if tuname not in ulist:
828         fail("%s not in %s" % (tuname, ulist))
829     success()
830
831     test(LDAPConnection.member_search_name)
832     mlist = connection.member_search_name(tmrname)
833     if tmname not in mlist:
834         fail("%s not in %s" % (tmname, mlist))
835     success()
836
837     test(LDAPConnection.member_search_program)
838     mlist = connection.member_search_program(tmprogram)
839     if tmname not in mlist:
840         fail("%s not in %s" % (tmname, mlist))
841     success()
842
843     test(LDAPConnection.group_search_id)
844     glist = connection.group_search_id(tggid).keys()
845     eglist = [ tgname ]
846     assert_equal(eglist, glist)
847     success()
848
849     ecudata = connection.account_lookup(tuname)
850     ecudata['loginShell'] = [ cushell ]
851     ecudata['homeDirectory'] = [ cuhome ]
852     ecudata['cn'] = [ curname ]
853
854     test(LDAPConnection.user_modify)
855     connection.user_modify(tuname, ecudata)
856     cudata = connection.account_lookup(tuname)
857     assert_equal(ecudata, cudata)
858     success()
859
860     tmuid = unusedids.pop()
861     tmgid = unusedids.pop()
862     emadata = emdata.copy()
863     emadata.update({
864             'loginShell': [ cmshell ],
865             'uidNumber': [ str(tmuid) ],
866             'gidNumber': [ str(tmgid) ],
867             'gecos': [ cmgecos ],
868             'homeDirectory': [ cmhome ],
869             })
870
871     test(LDAPConnection.member_add_account)
872     connection.member_add_account(tmname, tmuid, tmuid, cmhome, cmshell, cmgecos)
873     success()
874
875     ecgdata = connection.group_lookup(tgname)
876     ecgdata['memberUid'] = [ tuname ]
877
878     test(LDAPConnection.group_modify)
879     connection.group_modify(tgname, ecgdata)
880     cgdata = connection.group_lookup(tgname)
881     assert_equal(ecgdata, cgdata)
882     success()
883
884     test(LDAPConnection.group_delete)
885     connection.group_delete(tgname)
886     success()
887
888     test(LDAPConnection.disconnect)
889     connection.disconnect()
890     success()