Fix CEO group add for rfc2307bis
[public/pyceo-broken.git] / pylib / csc / backends / ldapi.py
index 7f31256..002cfbe 100644 (file)
@@ -66,7 +66,7 @@ class LDAPConnection(object):
             # 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:
@@ -98,7 +98,7 @@ class LDAPConnection(object):
 
     ### Helper Methods ###
 
-    def lookup(self, dn):
+    def lookup(self, dn, objectClass=None):
         """
         Helper method to retrieve the attributes of an entry.
 
@@ -109,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:
@@ -120,109 +126,160 @@ 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 username to look up
+            uid - the uid to look up
 
         Returns: attributes of user with uid
-
-        Example: connection.user_lookup('mspang') ->
-                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
         """
 
-        if not self.connected(): raise LDAPException("Not connected!")
-        
         dn = 'uid=' + uid + ',' + self.user_base
-        return self.lookup(dn)
-        
+        return self.lookup(dn, objectClass)
+
 
-    def user_search(self, search_filter):
+    def user_search(self, search_filter, params):
         """
-        Helper for user searches.
+        Search for users with a filter.
 
         Parameters:
             search_filter - LDAP filter string to match users against
 
-        Returns: the list of uids matched (usernames)
+        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, 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_modify(self, uid, attrs):
+        """
+        Update user attributes in the directory.
 
+        Parameters:
+            uid   - username of the user to modify
+            attrs - dictionary as returned by user_lookup() with changes to make.
+                    omitted attributes are DELETED.
 
-    def user_search_id(self, uidNumber):
+        Example: user = user_lookup('mspang')
+                 user['uidNumber'] = [ '0' ]
+                 connection.user_modify('mspang', user)
         """
-        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.
+        # 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)
+
+            # 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.
+
+        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)
+
+
+
+    ### Account-related Methods ###
+
+    def account_lookup(self, uid):
+        """
+        Retrieve the attributes of an account.
+
+        Parameters:
+            uid - the uid to look up
+
+        Returns: attributes of user with uid
+        """
+
+        return self.user_lookup(uid, 'posixAccount')
+
+
+    def account_search_id(self, uidNumber):
+        """
+        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: the list of uids matched (usernames)
+        Returns: a dictionary mapping uids to attributes
 
-        Example: connection.user_search_id(21292) -> ['mspang']
+        Example: connection.account_search_id(21292) -> {'mspang': { ... }}
         """
 
-        # search for posixAccount entries with the specified uidNumber
-        search_filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
-        return self.user_search(search_filter)
+        search_filter = '(&(objectClass=posixAccount)(uidNumber=%s))'
+        return self.user_search(search_filter, [ uidNumber ])
 
 
-    def user_search_gid(self, gidNumber):
+    def account_search_gid(self, gidNumber):
         """
-        Retrieves a list of users with a certain UNIX gid
+        Retrieves a list of accounts with a certain UNIX gid
         number (search by default group).
 
-        Returns: the list of uids matched (usernames)
+        Returns: a dictionary mapping uids to attributes
         """
 
-        # search for posixAccount entries with the specified gidNumber
-        search_filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
-        return self.user_search(search_filter)
+        search_filter = '(&(objectClass=posixAccount)(gidNumber=%s))'
+        return self.user_search(search_filter, [ gidNumber ])
 
 
-    def user_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None):
+    def account_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None, update=False):
         """
-        Adds a user to the directory.
+        Adds a user account to the directory.
 
         Parameters:
             uid           - the UNIX username for the account
@@ -233,12 +290,15 @@ class LDAPConnection(object):
             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',
-                     21292, 100, '/users/mspang', '/bin/bash', 
+                     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' ],
@@ -250,63 +310,33 @@ class LDAPConnection(object):
             'homeDirectory': [ homeDirectory ],
             'gecos': [ gecos ],
         }
-        
+
         if loginShell:
-            attrs['loginShell'] = loginShell
+            attrs['loginShell'] = [ loginShell ]
         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 user_modify(self, uid, attrs):
-        """
-        Update user attributes in the directory.
+            old_entry = self.user_lookup(uid)
+            if old_entry and 'posixAccount' not in old_entry['objectClass'] and update:
 
-        Parameters:
-            uid   - username of the user to modify
-            attrs - 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)
-
-        try:
-            
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_user, attrs)
-
-            # apply changes
-            self.ldap.modify_s(dn, changes)
+                attrs.update(old_entry)
+                attrs['objectClass'] = list(attrs['objectClass'])
+                attrs['objectClass'].append('posixAccount')
+                if not 'shadowAccount' in attrs['objectClass']:
+                    attrs['objectClass'].append('shadowAccount')
 
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to modify: %s" % e)
+                modlist = ldap.modlist.modifyModlist(old_entry, attrs)
+                self.ldap.modify_s(dn, modlist)
 
+            else:
 
-    def user_delete(self, uid):
-        """
-        Removes a user from the directory.
+                modlist = ldap.modlist.addModlist(attrs)
+                self.ldap.add_s(dn, modlist)
 
-        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)
 
 
 
@@ -327,10 +357,10 @@ class LDAPConnection(object):
                      ...
                  }
         """
-        
+
         dn = 'cn=' + cn + ',' + self.group_base
-        return self.lookup(dn)
-                                                                                    
+        return self.lookup(dn, 'posixGroup')
+
 
     def group_search_id(self, gidNumber):
         """
@@ -341,31 +371,25 @@ class LDAPConnection(object):
         Example: connection.group_search_id(1001) -> ['office']
         """
 
+        if not self.connected(): raise LDAPException("Not connected!")
+
         # search for posixAccount entries with the specified uidNumber
         try:
             search_filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
             matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, search_filter)
-        except ldap.LDAPError,:
+        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')
-
-            # do not handle the case of multiple cns for one group (yet)
-            elif len(attributes['cn']) > 1:
-                raise LDAPException(dn + ' (posixGroup) has multiple cns')
-
-            # append the sole uid of this match to the list
-            group_cns.append( attributes['cn'][0] )
+            dn, attrs = match
+            uid = attrs['cn'][0]
+            results[uid] = attrs
 
-        return group_cns
+        return results
 
 
     def group_add(self, cn, gidNumber, description=None):
@@ -374,10 +398,12 @@ class LDAPConnection(object):
 
         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) ],
         }
@@ -409,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
 
@@ -433,7 +461,9 @@ class LDAPConnection(object):
 
         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)
@@ -441,8 +471,116 @@ class LDAPConnection(object):
             raise LDAPException("unable to delete group: %s" % e)
 
 
+
+    ### Member-related Methods ###
+
+    def member_lookup(self, uid):
+        """
+        Retrieve the attributes of a member. This method will only return
+        results that have the objectClass 'member'.
+
+        Parameters:
+            uid - the username to look up
+
+        Returns: attributes of member with uid
+
+        Example: connection.member_lookup('mspang') ->
+                     { 'uid': 'mspang', 'uidNumber': 21292 ...}
+        """
+
+        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 escape(self, value):
+        """
+        Escapes special characters in a value so that it may be safely inserted
+        into an LDAP search filter.
+        """
+
+        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.
@@ -456,6 +594,8 @@ class LDAPConnection(object):
         Example: connection.used_uids(20000, 40000) -> [20000, 20001, ...]
         """
 
+        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:
@@ -484,6 +624,8 @@ class LDAPConnection(object):
         Example: connection.used_gids(20000, 40000) -> [20000, 20001, ...]
         """
 
+        if not self.connected(): raise LDAPException("Not connected!")
+
         try:
             users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['gidNumber'])
         except ldap.LDAPError, e:
@@ -523,19 +665,26 @@ if __name__ == '__main__':
     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()")
+    test(LDAPConnection)
     connection = LDAPConnection()
     success()
 
-    test("disconnect()")
+    test(LDAPConnection.disconnect)
     connection.disconnect()
     success()
 
-    test("connect()")
+    test(LDAPConnection.connect)
     connection.connect(srvurl, binddn, bindpw, ubase, gbase)
     if not connection.connected():
         fail("not connected")
@@ -543,17 +692,18 @@ if __name__ == '__main__':
 
     try:
         connection.user_delete(tuname)
+        connection.user_delete(tmname)
         connection.group_delete(tgname)
     except LDAPException:
         pass
 
-    test("used_uids()")
+    test(LDAPConnection.used_uids)
     uids = connection.used_uids(minid, maxid)
     if type(uids) is not list:
         fail("list not returned")
     success()
 
-    test("used_gids()")
+    test(LDAPConnection.used_gids)
     gids = connection.used_gids(minid, maxid)
     if type(gids) is not list:
         fail("list not returned")
@@ -576,8 +726,19 @@ if __name__ == '__main__':
             'cn': [ turname ]
             }
 
-    test("user_add()")
-    connection.user_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
+    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()
@@ -586,58 +747,106 @@ if __name__ == '__main__':
             'gidNumber': [ str(tggid) ]
             }
 
-    test("group_add()")
+    test(LDAPConnection.group_add)
     connection.group_add(tgname, tggid)
     success()
 
-    test("user_lookup()")
+    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)
-    del udata['objectClass']
+    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("group_lookup()")
+    test(LDAPConnection.group_lookup)
     gdata = connection.group_lookup(tgname)
-    del gdata['objectClass']
+    if gdata: del gdata['objectClass']
     assert_equal(egdata, gdata)
     success()
 
-    test("user_search_id()")
+    test(LDAPConnection.account_search_id)
     eulist = [ tuname ]
-    ulist = connection.user_search_id(tuuid)
+    ulist = connection.account_search_id(tuuid).keys()
     assert_equal(eulist, ulist)
     success()
 
-    test("user_search_gid()")
-    ulist = connection.user_search_gid(tugid)
+    test(LDAPConnection.account_search_gid)
+    ulist = connection.account_search_gid(tugid)
     if tuname not in ulist:
-        fail("(%s) not in (%s)" % (tuname, 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()
 
-    ecudata = connection.user_lookup(tuname)
+    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("user_modify")
+    test(LDAPConnection.user_modify)
     connection.user_modify(tuname, ecudata)
-    cudata = connection.user_lookup(tuname)
+    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("group_modify()")
+    test(LDAPConnection.group_modify)
     connection.group_modify(tgname, ecgdata)
     cgdata = connection.group_lookup(tgname)
     assert_equal(ecgdata, cgdata)
     success()
 
-    test("user_delete()")
+    test(LDAPConnection.group_delete)
     connection.group_delete(tgname)
     success()
 
-    test("disconnect()")
+    test(LDAPConnection.disconnect)
     connection.disconnect()
     success()