Fix CEO group add for rfc2307bis
[public/pyceo-broken.git] / pylib / csc / backends / ldapi.py
index cf32e8f..002cfbe 100644 (file)
@@ -1,4 +1,3 @@
-# $Id: ldapi.py 41 2006-12-29 04:22:31Z mspang $
 """
 LDAP Backend Interface
 
@@ -60,14 +59,14 @@ class LDAPConnection(object):
         
         """
 
-        if bind_pw == None: bind_pw = ''
+        if bind_pw is None: bind_pw = ''
 
         try:
 
             # open the connection
             self.ldap = ldap.initialize(server)
 
-            # authenticate as ceo
+            # authenticate
             self.ldap.simple_bind_s(bind_dn, bind_pw)
 
         except ldap.LDAPError, e:
@@ -93,13 +92,13 @@ class LDAPConnection(object):
     def connected(self):
         """Determine whether the connection has been established."""
 
-        return self.ldap != None
+        return self.ldap is not None
 
 
 
     ### Helper Methods ###
 
-    def lookup(self, dn):
+    def lookup(self, dn, objectClass=None):
         """
         Helper method to retrieve the attributes of an entry.
 
@@ -110,9 +109,15 @@ class LDAPConnection(object):
                  None of the dn does not exist in the directory
         """
 
+        if not self.connected(): raise LDAPException("Not connected!")
+
         # search for the specified dn
         try:
-            matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
+            if objectClass:
+                search_filter = '(objectClass=%s)' % self.escape(objectClass)
+                matches = self.ldap.search_s(dn, ldap.SCOPE_BASE, search_filter)
+            else:
+                matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
         except ldap.NO_SUCH_OBJECT:
             return None
         except ldap.LDAPError, e:
@@ -121,125 +126,179 @@ class LDAPConnection(object):
         # this should never happen due to the nature of DNs
         if len(matches) > 1:
             raise LDAPException("duplicate dn in ldap: " + dn)
-        
+
+        # dn was found, but didn't match the objectClass filter
+        elif len(matches) < 1:
+            return None
+
         # return the attributes of the single successful match
-        else:
-            match = matches[0]
-            match_dn, match_attributes = match
-            return match_attributes
+        match = matches[0]
+        match_dn, match_attributes = match
+        return match_attributes
+
 
 
-    
     ### User-related Methods ###
 
-    def user_lookup(self, uid):
+    def user_lookup(self, uid, objectClass=None):
         """
         Retrieve the attributes of a user.
 
         Parameters:
-            uid - the UNIX user accound name of the user
+            uid - the uid to look up
 
         Returns: attributes of user with uid
-
-        Example: connection.user_lookup('mspang') ->
-                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
         """
-        
+
         dn = 'uid=' + uid + ',' + self.user_base
-        return self.lookup(dn)
-        
+        return self.lookup(dn, objectClass)
 
-    def user_search(self, filter):
+
+    def user_search(self, search_filter, params):
         """
-        Helper for user searches.
+        Search for users with a filter.
 
         Parameters:
-            filter - LDAP filter string to match users against
+            search_filter - LDAP filter string to match users against
 
-        Returns: the list of uids matched
+        Returns: a dictionary mapping uids to attributes
         """
 
+        if not self.connected(): raise LDAPException("Not connected!")
+
+        search_filter = search_filter % tuple(self.escape(x) for x in params)
+
         # search for entries that match the filter
         try:
-            matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, filter)
+            matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
         except ldap.LDAPError, e:
             raise LDAPException("user search failed: %s" % e)
-        
-        # list for uids found
-        uids = []
-        
+
+        results = {}
         for match in matches:
-            dn, attributes = match
-            
-            # uid is a required attribute of posixAccount
-            if not attributes.has_key('uid'):
-                raise LDAPException(dn + ' (posixAccount) has no uid')
-            
-            # do not handle the case of multiple usernames in one entry (yet)
-            elif len(attributes['uid']) > 1:
-                raise LDAPException(dn + ' (posixAccount) has multiple uids')
-            
-            # append the sole uid of this match to the list
-            uids.append( attributes['uid'][0] )
+            dn, attrs = match
+            uid = attrs['uid'][0]
+            results[uid] = attrs
 
-        return uids
+        return results
 
 
-    def user_search_id(self, uidNumber):
+    def user_modify(self, uid, attrs):
         """
-        Retrieves a list of users with a certain UNIX uid number.
-
-        LDAP (or passwd for that matter) does not enforce any
-        restriction on the number of accounts that can have
-        a certain UID. Therefore this method returns a list of matches.
+        Update user attributes in the directory.
 
         Parameters:
-            uidNumber - the user id of the accounts desired
+            uid   - username of the user to modify
+            attrs - dictionary as returned by user_lookup() with changes to make.
+                    omitted attributes are DELETED.
 
-        Returns: the list of uids matched
+        Example: user = user_lookup('mspang')
+                 user['uidNumber'] = [ '0' ]
+                 connection.user_modify('mspang', user)
+        """
+
+        # distinguished name of the entry to modify
+        dn = 'uid=' + uid + ',' + self.user_base
+
+        # retrieve current state of user
+        old_user = self.user_lookup(uid)
+
+        try:
+
+            # build list of modifications to make
+            changes = ldap.modlist.modifyModlist(old_user, attrs)
 
-        Example: connection.user_search_id(21292) -> ['mspang']
+            # apply changes
+            self.ldap.modify_s(dn, changes)
+
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to modify: %s" % e)
+
+
+    def user_delete(self, uid):
         """
+        Removes a user from the directory.
 
-        # search for posixAccount entries with the specified uidNumber
-        filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
-        return self.user_search(filter)
+        Example: connection.user_delete('mspang')
+        """
+
+        try:
+            dn = 'uid=' + uid + ',' + self.user_base
+            self.ldap.delete_s(dn)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to delete: %s" % e)
 
 
-    def user_search_gid(self, gidNumber):
+
+    ### Account-related Methods ###
+
+    def account_lookup(self, uid):
         """
-        Retrieves a list of users with a certain UNIX gid number.
+        Retrieve the attributes of an account.
 
         Parameters:
-            gidNumber - the group id of the accounts desired
+            uid - the uid to look up
 
-        Returns: the list of uids matched
+        Returns: attributes of user with uid
         """
 
-        # search for posixAccount entries with the specified gidNumber
-        filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
-        return self.user_search(filter)
+        return self.user_lookup(uid, 'posixAccount')
 
 
-    def user_add(self, uid, cn, loginShell, uidNumber, gidNumber, homeDirectory, gecos):
+    def account_search_id(self, uidNumber):
         """
-        Adds a user to the directory.
+        Retrieves a list of accounts with a certain UNIX uid number.
+
+        LDAP (or passwd for that matter) does not enforce any restriction on
+        the number of accounts that can have a certain UID number. Therefore
+        this method returns a list of matches.
+
+        Parameters:
+            uidNumber - the user id of the accounts desired
+
+        Returns: a dictionary mapping uids to attributes
+
+        Example: connection.account_search_id(21292) -> {'mspang': { ... }}
+        """
+
+        search_filter = '(&(objectClass=posixAccount)(uidNumber=%s))'
+        return self.user_search(search_filter, [ uidNumber ])
+
+
+    def account_search_gid(self, gidNumber):
+        """
+        Retrieves a list of accounts with a certain UNIX gid
+        number (search by default group).
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=posixAccount)(gidNumber=%s))'
+        return self.user_search(search_filter, [ gidNumber ])
+
+
+    def account_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None, update=False):
+        """
+        Adds a user account to the directory.
 
         Parameters:
             uid           - the UNIX username for the account
-            cn            - the full name of the member
-            userPassword  - password of the account (our setup does not use this)
-            loginShell    - login shell for the user
+            cn            - the real name of the member
             uidNumber     - the UNIX user id number
-            gidNumber     - the UNIX group id number
+            gidNumber     - the UNIX group id number (default group)
             homeDirectory - home directory for the user
-            gecos         - comment field (usually stores miscellania)
+            loginShell    - login shell for the user
+            gecos         - comment field (usually stores name etc)
+            description   - description field (optional and unimportant)
+            update        - if True, will update existing entries
 
         Example: connection.user_add('mspang', 'Michael Spang',
-                     '/bin/bash', 21292, 100, '/users/mspang',
+                     21292, 100, '/users/mspang', '/bin/bash',
                      'Michael Spang,,,')
         """
-        
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
         dn = 'uid=' + uid + ',' + self.user_base
         attrs = {
             'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
@@ -252,60 +311,32 @@ class LDAPConnection(object):
             'gecos': [ gecos ],
         }
 
-        try:
-            modlist = ldap.modlist.addModlist(attrs)
-            self.ldap.add_s(dn, modlist)
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to add: %s" % e)
-
-
-    def user_modify(self, uid, attrs):
-        """
-        Update user attributes in the directory.
-
-        Parameters:
-            uid   - username of the user to modify
-            entry - dictionary as returned by user_lookup() with changes to make.
-                    omitted attributes are DELETED.
-
-        Example: user = user_lookup('mspang')
-                 user['uidNumber'] = [ '0' ]
-                 connection.user_modify('mspang', user)
-        """
-
-        # distinguished name of the entry to modify
-        dn = 'uid=' + uid + ',' + self.user_base
-
-        # retrieve current state of user
-        old_user = self.user_lookup(uid)
+        if loginShell:
+            attrs['loginShell'] = [ loginShell ]
+        if description:
+            attrs['description'] = [ description ]
 
         try:
-            
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_user, attrs)
 
-            # apply changes
-            self.ldap.modify_s(dn, changes)
+            old_entry = self.user_lookup(uid)
+            if old_entry and 'posixAccount' not in old_entry['objectClass'] and update:
 
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to modify: %s" % e)
+                attrs.update(old_entry)
+                attrs['objectClass'] = list(attrs['objectClass'])
+                attrs['objectClass'].append('posixAccount')
+                if not 'shadowAccount' in attrs['objectClass']:
+                    attrs['objectClass'].append('shadowAccount')
 
+                modlist = ldap.modlist.modifyModlist(old_entry, attrs)
+                self.ldap.modify_s(dn, modlist)
 
-    def user_delete(self, uid):
-        """
-        Removes a user from the directory.
+            else:
+
+                modlist = ldap.modlist.addModlist(attrs)
+                self.ldap.add_s(dn, modlist)
 
-        Parameters:
-            uid - the UNIX username of the account
-        
-        Example: connection.user_delete('mspang')
-        """
-        
-        try:
-            dn = 'uid=' + uid + ',' + self.user_base
-            self.ldap.delete_s(dn)
         except ldap.LDAPError, e:
-            raise LDAPException("unable to delete: %s" % e)
+            raise LDAPException("unable to add: %s" % e)
 
 
 
@@ -318,7 +349,7 @@ class LDAPConnection(object):
         Parameters:
             cn - the UNIX group name to lookup
 
-        Returns: attributes of group with cn
+        Returns: attributes of the group's LDAP entry
 
         Example: connection.group_lookup('office') -> {
                      'cn': 'office',
@@ -326,67 +357,58 @@ class LDAPConnection(object):
                      ...
                  }
         """
-        
+
         dn = 'cn=' + cn + ',' + self.group_base
-        return self.lookup(dn)
-                                                                                    
+        return self.lookup(dn, 'posixGroup')
+
 
     def group_search_id(self, gidNumber):
         """
         Retrieves a list of groups with the specified UNIX group number.
         
-        Parameters:
-            gidNumber - the group id of the groups desired
-
         Returns: a list of groups with gid gidNumber
 
         Example: connection.group_search_id(1001) -> ['office']
         """
 
+        if not self.connected(): raise LDAPException("Not connected!")
+
         # search for posixAccount entries with the specified uidNumber
         try:
-            filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
-            matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, filter)
-        except ldap.LDAPError,:
+            search_filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
+            matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, search_filter)
+        except ldap.LDAPError, e:
             raise LDAPException("group search failed: %s" % e)
 
         # list for groups found
         group_cns = []
 
+        results = {}
         for match in matches:
-            dn, attributes = match
-
-            # cn is a required attribute of posixGroup
-            if not attributes.has_key('cn'):
-                raise LDAPException(dn + ' (posixGroup) has no cn')
+            dn, attrs = match
+            uid = attrs['cn'][0]
+            results[uid] = attrs
 
-            # do not handle the case of multiple cns for one group (yet)
-            elif len(attributes['cn']) > 1:
-                raise LDAPException(dn + ' (posixGroup) has multiple cns')
+        return results
 
-            # append the sole uid of this match to the list
-            group_cns.append( attributes['cn'][0] )
 
-        return group_cns
-
-
-    def group_add(self, cn, gidNumber):
+    def group_add(self, cn, gidNumber, description=None):
         """
         Adds a group to the directory.
 
-        Parameters:
-            cn        - the name of the group
-            gidNumber - the number of the group
-
-        Example: connection.group_add('office', 1001)
+        Example: connection.group_add('office', 1001, 'Office Staff')
         """
-        
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
         dn = 'cn=' + cn + ',' + self.group_base
         attrs = {
-            'objectClass': [ 'top', 'posixGroup' ],
+            'objectClass': [ 'top', 'posixGroup', 'group' ],
             'cn': [ cn ],
             'gidNumber': [ str(gidNumber) ],
         }
+        if description:
+            attrs['description'] = description
 
         try:
             modlist = ldap.modlist.addModlist(attrs)
@@ -399,9 +421,8 @@ class LDAPConnection(object):
         """
         Update group attributes in the directory.
         
-        The only available updates are fairly destructive
-        (rename or renumber) but this method is provided
-        for completeness.
+        The only available updates are fairly destructive (rename or renumber)
+        but this method is provided for completeness.
 
         Parameters:
             cn    - name of the group to modify
@@ -414,6 +435,8 @@ class LDAPConnection(object):
                  connection.group_modify('office', group)
         """
 
+        if not self.connected(): raise LDAPException("Not connected!")
+
         # distinguished name of the entry to modify
         dn = 'cn=' + cn + ',' + self.group_base
 
@@ -436,12 +459,11 @@ class LDAPConnection(object):
         """
         Removes a group from the directory."
 
-        Parameters:
-            cn - the name of the group
-
         Example: connection.group_delete('office')
         """
-        
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
         try:
             dn = 'cn=' + cn + ',' + self.group_base
             self.ldap.delete_s(dn)
@@ -449,129 +471,382 @@ class LDAPConnection(object):
             raise LDAPException("unable to delete group: %s" % e)
 
 
-    def group_members(self, cn):
+
+    ### Member-related Methods ###
+
+    def member_lookup(self, uid):
         """
-        Retrieves a group's members.
+        Retrieve the attributes of a member. This method will only return
+        results that have the objectClass 'member'.
 
         Parameters:
-            cn - the name of the group
+            uid - the username to look up
 
-        Example: connection.group_members('office') ->
-                 ['sfflaw', 'jeperry', 'cschopf' ...]
+        Returns: attributes of member with uid
+
+        Example: connection.member_lookup('mspang') ->
+                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
         """
 
-        group = self.group_lookup(cn)
-        return group.get('memberUid', None)
+        if not self.connected(): raise LDAPException("Not connected!")
+
+        dn = 'uid=' + uid + ',' + self.user_base
+        return self.lookup(dn, 'member')
+
+
+    def member_search_name(self, name):
+        """
+        Retrieves a list of members with the specified name (fuzzy).
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=member)(cn~=%s))'
+        return self.user_search(search_filter, [ name ] )
+
+
+    def member_search_term(self, term):
+        """
+        Retrieves a list of members who were registered in a certain term.
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=member)(term=%s))'
+        return self.user_search(search_filter, [ term ])
+
+
+    def member_search_program(self, program):
+        """
+        Retrieves a list of members in a certain program (fuzzy).
+
+        Returns: a dictionary mapping uids to attributes
+        """
+
+        search_filter = '(&(objectClass=member)(program~=%s))'
+        return self.user_search(search_filter, [ program ])
+
+
+    def member_add(self, uid, cn, program=None, description=None):
+        """
+        Adds a member to the directory.
+
+        Parameters:
+            uid           - the UNIX username for the member
+            cn            - the real name of the member
+            program       - the member's program of study
+            description   - a description for the entry
+        """
+
+        dn = 'uid=' + uid + ',' + self.user_base
+        attrs = {
+            'objectClass': [ 'top', 'account', 'member' ],
+            'uid': [ uid ],
+            'cn': [ cn ],
+        }
+
+        if program:
+            attrs['program'] = [ program ]
+        if description:
+            attrs['description'] = [ description ]
+
+        try:
+            modlist = ldap.modlist.addModlist(attrs)
+            self.ldap.add_s(dn, modlist)
+        except ldap.LDAPError, e:
+            raise LDAPException("unable to add: %s" % e)
+
+
+    def member_add_account(self, uid, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None):
+        """
+        Adds login privileges to a member.
+        """
+
+        return self.account_add(uid, None, uidNumber, gidNumber, homeDirectory, loginShell, gecos, None, True)
+
 
 
     ### Miscellaneous Methods ###
-    
-    def first_id(self, minimum, maximum):
+
+    def escape(self, value):
+        """
+        Escapes special characters in a value so that it may be safely inserted
+        into an LDAP search filter.
         """
-        Determines the first available id within a range.
 
-        To be "available", there must be neither a user
-        with the id nor a group with the id.
+        value = str(value)
+        value = value.replace('\\', '\\5c').replace('*', '\\2a')
+        value = value.replace('(', '\\28').replace(')', '\\29')
+        value = value.replace('\x00', '\\00')
+        return value
+
+
+    def used_uids(self, minimum=None, maximum=None):
+        """
+        Compiles a list of used UIDs in a range.
 
         Parameters:
-            minimum - smallest uid that may be returned
-            maximum - largest uid that may be returned
+            minimum - smallest uid to return in the list
+            maximum - largest uid to return in the list
 
-        Returns: the id, or None if there are none available
+        Returns: list of integer uids
 
-        Example: connection.first_id(20000, 40000) -> 20018
+        Example: connection.used_uids(20000, 40000) -> [20000, 20001, ...]
         """
 
-        # compile a list of used uids
+        if not self.connected(): raise LDAPException("Not connected!")
+
         try:
             users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['uidNumber'])
         except ldap.LDAPError, e:
             raise LDAPException("search for uids failed: %s" % e)
+        
         uids = []
         for user in users:
             dn, attrs = user
             uid = int(attrs['uidNumber'][0])
-            if minimum <= uid <= maximum:
+            if (not minimum or uid >= minimum) and (not maximum or uid <= maximum):
                 uids.append(uid)
 
-        # compile a list of used gids
+        return uids
+            
+    
+    def used_gids(self, minimum=None, maximum=None):
+        """
+        Compiles a list of used GIDs in a range.
+
+        Parameters:
+            minimum - smallest gid to return in the list
+            maximum - largest gid to return in the list
+
+        Returns: list of integer gids
+
+        Example: connection.used_gids(20000, 40000) -> [20000, 20001, ...]
+        """
+
+        if not self.connected(): raise LDAPException("Not connected!")
+
         try:
-            groups = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)', ['gidNumber'])
+            users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['gidNumber'])
         except ldap.LDAPError, e:
             raise LDAPException("search for gids failed: %s" % e)
+        
         gids = []
-        for group in groups:
-            dn, attrs = group
+        for user in users:
+            dn, attrs = user
             gid = int(attrs['gidNumber'][0])
-            if minimum <= gid <= maximum:
+            if (not minimum or gid >= minimum) and (not maximum or gid <= maximum):
                 gids.append(gid)
 
-        # iterate through ids and return the first available
-        for id in xrange(minimum, maximum+1):
-            if not id in uids and not id in gids:
-                return id
+        return gids
 
-        # no suitable id was found
-        return None
 
 
 ### Tests ###
 
 if __name__ == '__main__':
     
-    password_file = 'ldap.ceo'
-    server   = 'ldaps:///'
-    base_dn  = 'dc=csclub,dc=uwaterloo,dc=ca'
-    bind_dn  = 'cn=ceo,' + base_dn
-    user_dn  = 'ou=People,' + base_dn
-    group_dn = 'ou=Group,' + base_dn
-    bind_pw = open(password_file).readline().strip()
-
+    from csc.common.test import *
+
+    conffile = '/etc/csc/ldap.cf'
+    cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ]) 
+    srvurl = cfg['server_url'][1:-1]
+    binddn = cfg['admin_bind_dn'][1:-1]
+    bindpw = cfg['admin_bind_pw'][1:-1]
+    ubase = cfg['users_base'][1:-1]
+    gbase = cfg['groups_base'][1:-1]
+    minid = 99999000
+    maxid = 100000000
+
+    # t=test u=user g=group c=changed r=real e=expected
+    tuname = 'testuser'
+    turname = 'Test User'
+    tuhome = '/home/testuser'
+    tushell = '/bin/false'
+    tugecos = 'Test User,,,'
+    tgname = 'testgroup'
+    tmname = 'testmember'
+    tmrname = 'Test Member'
+    tmprogram = 'UBW'
+    tmdesc = 'Test Description'
+    cushell = '/bin/true'
+    cuhome = '/home/changed'
+    curname = 'Test Modified User'
+    cmhome = '/home/testmember'
+    cmshell = '/bin/false'
+    cmgecos = 'Test Member,,,'
+
+    test(LDAPConnection)
     connection = LDAPConnection()
-    print "running disconnect()"
+    success()
+
+    test(LDAPConnection.disconnect)
     connection.disconnect()
-    print "running connect('%s', '%s', '%s', '%s', '%s')" % (server, bind_dn, '***', user_dn, group_dn)
-    connection.connect(server, bind_dn, bind_pw, user_dn, group_dn)
-    print "running user_lookup('mspang')", "->", "(%s)" % connection.user_lookup('mspang')['uidNumber'][0]
-    print "running user_search_id(21292)", "->", connection.user_search_id(21292)
-    print "running first_id(20000, 40000)", "->",
-    first_id = connection.first_id(20000, 40000)
-    print first_id
-    print "running group_add('testgroup', %d)" % first_id
-    try:
-        connection.group_add('testgroup', first_id)
-    except Exception, e:
-        print "FAILED: %s (continuing)" % e
-    print "running user_add('testuser', 'Test User', '/bin/false', %d, %d, '/home/null', 'Test User,,,')" % (first_id, first_id)
+    success()
+
+    test(LDAPConnection.connect)
+    connection.connect(srvurl, binddn, bindpw, ubase, gbase)
+    if not connection.connected():
+        fail("not connected")
+    success()
+
     try:
-        connection.user_add('testuser', 'Test User', '/bin/false', first_id, first_id, '/home/null', 'Test User,,,')
-    except Exception, e:
-        print "FAILED: %s (continuing)" % e
-    print "running user_lookup('testuser')", "->",
-    user = connection.user_lookup('testuser')
-    print repr(connection.user_lookup('testuser')['cn'][0])
-    user['homeDirectory'] = ['/home/changed']
-    user['loginShell'] = ['/bin/true']
-    print "running user_modify(...)"
-    connection.user_modify('testuser', user)
-    print "running user_lookup('testuser')", "->",
-    user = connection.user_lookup('testuser')
-    print '(%s, %s)' % (user['homeDirectory'], user['loginShell'])
-    print "running group_lookup('testgroup')", "->",
-    group = connection.group_lookup('testgroup')
-    print group
-    print "running group_modify(...)"
-    group['gidNumber'] = [str(connection.first_id(20000, 40000))]
-    group['memberUid'] = [ str(first_id) ]
-    connection.group_modify('testgroup', group)
-    print "running group_lookup('testgroup')", "->",
-    group = connection.group_lookup('testgroup')
-    print group
-    print "running user_delete('testuser')"
-    connection.user_delete('testuser')
-    print "running group_delete('testgroup')"
-    connection.group_delete('testgroup')
-    print "running user_search_gid(100)", "->", "[" + ", ".join(map(repr,connection.user_search_gid(100)[:10])) + " ...]"
-    print "running group_members('office')", "->", "[" + ", ".join(map(repr,connection.group_members('office')[:10])) + " ...]"
-    print "running disconnect()"
+        connection.user_delete(tuname)
+        connection.user_delete(tmname)
+        connection.group_delete(tgname)
+    except LDAPException:
+        pass
+
+    test(LDAPConnection.used_uids)
+    uids = connection.used_uids(minid, maxid)
+    if type(uids) is not list:
+        fail("list not returned")
+    success()
+
+    test(LDAPConnection.used_gids)
+    gids = connection.used_gids(minid, maxid)
+    if type(gids) is not list:
+        fail("list not returned")
+    success()
+
+    unusedids = []
+    for idnum in xrange(minid, maxid):
+        if not idnum in uids and not idnum in gids:
+            unusedids.append(idnum)
+
+    tuuid = unusedids.pop()
+    tugid = unusedids.pop()
+    eudata = {
+            'uid': [ tuname ],
+            'loginShell': [ tushell ],
+            'uidNumber': [ str(tuuid) ],
+            'gidNumber': [ str(tugid) ],
+            'gecos': [ tugecos ],
+            'homeDirectory': [ tuhome ],
+            'cn': [ turname ]
+            }
+
+    test(LDAPConnection.account_add)
+    connection.account_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
+    success()
+
+    emdata = {
+            'uid': [ tmname ],
+            'cn': [ tmrname ],
+            'program': [ tmprogram ],
+            'description': [ tmdesc ],
+    }
+
+    test(LDAPConnection.member_add)
+    connection.member_add(tmname, tmrname, tmprogram, tmdesc)
+    success()
+
+    tggid = unusedids.pop()
+    egdata = {
+            'cn': [ tgname ],
+            'gidNumber': [ str(tggid) ]
+            }
+
+    test(LDAPConnection.group_add)
+    connection.group_add(tgname, tggid)
+    success()
+
+    test(LDAPConnection.account_lookup)
+    udata = connection.account_lookup(tuname)
+    if udata: del udata['objectClass']
+    assert_equal(eudata, udata)
+    success()
+
+    test(LDAPConnection.member_lookup)
+    mdata = connection.member_lookup(tmname)
+    if mdata: del mdata['objectClass']
+    assert_equal(emdata, mdata)
+    success()
+
+    test(LDAPConnection.user_lookup)
+    udata = connection.user_lookup(tuname)
+    mdata = connection.user_lookup(tmname)
+    if udata: del udata['objectClass']
+    if mdata: del mdata['objectClass']
+    assert_equal(eudata, udata)
+    assert_equal(emdata, mdata)
+    success()
+
+    test(LDAPConnection.group_lookup)
+    gdata = connection.group_lookup(tgname)
+    if gdata: del gdata['objectClass']
+    assert_equal(egdata, gdata)
+    success()
+
+    test(LDAPConnection.account_search_id)
+    eulist = [ tuname ]
+    ulist = connection.account_search_id(tuuid).keys()
+    assert_equal(eulist, ulist)
+    success()
+
+    test(LDAPConnection.account_search_gid)
+    ulist = connection.account_search_gid(tugid)
+    if tuname not in ulist:
+        fail("%s not in %s" % (tuname, ulist))
+    success()
+
+    test(LDAPConnection.member_search_name)
+    mlist = connection.member_search_name(tmrname)
+    if tmname not in mlist:
+        fail("%s not in %s" % (tmname, mlist))
+    success()
+
+    test(LDAPConnection.member_search_program)
+    mlist = connection.member_search_program(tmprogram)
+    if tmname not in mlist:
+        fail("%s not in %s" % (tmname, mlist))
+    success()
+
+    test(LDAPConnection.group_search_id)
+    glist = connection.group_search_id(tggid).keys()
+    eglist = [ tgname ]
+    assert_equal(eglist, glist)
+    success()
+
+    ecudata = connection.account_lookup(tuname)
+    ecudata['loginShell'] = [ cushell ]
+    ecudata['homeDirectory'] = [ cuhome ]
+    ecudata['cn'] = [ curname ]
+
+    test(LDAPConnection.user_modify)
+    connection.user_modify(tuname, ecudata)
+    cudata = connection.account_lookup(tuname)
+    assert_equal(ecudata, cudata)
+    success()
+
+    tmuid = unusedids.pop()
+    tmgid = unusedids.pop()
+    emadata = emdata.copy()
+    emadata.update({
+            'loginShell': [ cmshell ],
+            'uidNumber': [ str(tmuid) ],
+            'gidNumber': [ str(tmgid) ],
+            'gecos': [ cmgecos ],
+            'homeDirectory': [ cmhome ],
+            })
+
+    test(LDAPConnection.member_add_account)
+    connection.member_add_account(tmname, tmuid, tmuid, cmhome, cmshell, cmgecos)
+    success()
+
+    ecgdata = connection.group_lookup(tgname)
+    ecgdata['memberUid'] = [ tuname ]
+
+    test(LDAPConnection.group_modify)
+    connection.group_modify(tgname, ecgdata)
+    cgdata = connection.group_lookup(tgname)
+    assert_equal(ecgdata, cgdata)
+    success()
+
+    test(LDAPConnection.group_delete)
+    connection.group_delete(tgname)
+    success()
+
+    test(LDAPConnection.disconnect)
     connection.disconnect()
+    success()