Fix CEO group add for rfc2307bis
[public/pyceo-broken.git] / pylib / csc / backends / ldapi.py
index c949e68..002cfbe 100644 (file)
@@ -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.
 
@@ -113,7 +113,11 @@ class LDAPConnection(object):
 
         # 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:
@@ -122,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 ...}
         """
 
         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 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.
+
+        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)
+
+        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)
 
-        return uids
 
 
-    def user_search_id(self, uidNumber):
+    ### Account-related Methods ###
+
+    def account_lookup(self, uid):
         """
-        Retrieves a list of users with a certain UNIX uid number.
+        Retrieve the attributes of an account.
 
-        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.
+        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
@@ -235,6 +290,7 @@ 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',
@@ -254,67 +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.
-
-        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)
-        """
+            old_entry = self.user_lookup(uid)
+            if old_entry and 'posixAccount' not in old_entry['objectClass'] and update:
 
-        if not self.connected(): raise LDAPException("Not connected!")
+                attrs.update(old_entry)
+                attrs['objectClass'] = list(attrs['objectClass'])
+                attrs['objectClass'].append('posixAccount')
+                if not 'shadowAccount' in attrs['objectClass']:
+                    attrs['objectClass'].append('shadowAccount')
 
-        # distinguished name of the entry to modify
-        dn = 'uid=' + uid + ',' + self.user_base
+                modlist = ldap.modlist.modifyModlist(old_entry, attrs)
+                self.ldap.modify_s(dn, modlist)
 
-        # retrieve current state of user
-        old_user = self.user_lookup(uid)
+            else:
 
-        try:
-            
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_user, attrs)
-
-            # apply changes
-            self.ldap.modify_s(dn, changes)
+                modlist = ldap.modlist.addModlist(attrs)
+                self.ldap.add_s(dn, modlist)
 
         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')
-        """
-
-        if not self.connected(): raise LDAPException("Not connected!")
-
-        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)
 
 
 
@@ -355,27 +377,19 @@ class LDAPConnection(object):
         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')
+            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')
-
-            # append the sole uid of this match to the list
-            group_cns.append( attributes['cn'][0] )
-
-        return group_cns
+        return results
 
 
     def group_add(self, cn, gidNumber, description=None):
@@ -389,7 +403,7 @@ class LDAPConnection(object):
 
         dn = 'cn=' + cn + ',' + self.group_base
         attrs = {
-            'objectClass': [ 'top', 'posixGroup' ],
+            'objectClass': [ 'top', 'posixGroup', 'group' ],
             'cn': [ cn ],
             'gidNumber': [ str(gidNumber) ],
         }
@@ -457,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.
@@ -543,9 +665,16 @@ 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)
     connection = LDAPConnection()
@@ -563,6 +692,7 @@ if __name__ == '__main__':
 
     try:
         connection.user_delete(tuname)
+        connection.user_delete(tmname)
         connection.group_delete(tgname)
     except LDAPException:
         pass
@@ -596,8 +726,19 @@ if __name__ == '__main__':
             'cn': [ turname ]
             }
 
-    test(LDAPConnection.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()
@@ -610,41 +751,89 @@ if __name__ == '__main__':
     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)
-    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(LDAPConnection.group_lookup)
     gdata = connection.group_lookup(tgname)
-    del gdata['objectClass']
+    if gdata: del gdata['objectClass']
     assert_equal(egdata, gdata)
     success()
 
-    test(LDAPConnection.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(LDAPConnection.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()
 
-    ecudata = connection.user_lookup(tuname)
+    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.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 ]