Use python-ldap directly in members
authorMichael Spang <mspang@csclub.uwaterloo.ca>
Thu, 13 Dec 2007 09:20:25 +0000 (04:20 -0500)
committerMichael Spang <mspang@csclub.uwaterloo.ca>
Fri, 14 Dec 2007 05:46:09 +0000 (00:46 -0500)
This leaves only utility functions in ldapi.

ceo/ldapi.py
ceo/members.py

index 23e2ee0..c2fc690 100644 (file)
 """
-LDAP Backend Interface
-
-This module is intended to be a thin wrapper around LDAP operations.
-Methods on the connection object correspond in a straightforward way
-to LDAP queries and updates.
-
-A LDAP entry is the most important component of a CSC UNIX account.
-The entry contains the username, user id number, real name, shell,
-and other important information. All non-local UNIX accounts must
-have an LDAP entry, even if the account does not log in directly.
+LDAP Utilities
 
 This module makes use of python-ldap, a Python module with bindings
 to libldap, OpenLDAP's native C client library.
 """
 import ldap.modlist
-from subprocess import Popen, PIPE
-
-
-class LDAPException(Exception):
-    """Exception class for LDAP-related errors."""
-
-
-class LDAPConnection(object):
-    """
-    Connection to the LDAP directory. All directory
-    queries and updates are made via this class.
-
-    Exceptions: (all methods)
-        LDAPException - on directory query failure
-
-    Example:
-         connection = LDAPConnection()
-         connection.connect(...)
-
-         # make queries and updates, e.g.
-         connection.user_delete('mspang')
-
-         connection.disconnect()
-    """
-
-    def __init__(self):
-        self.ldap = None
-
-
-    def connect_anon(self, uri, user_base, group_base):
-        """
-        Establish a connection to the LDAP Server.
-
-        Parameters:
-            uri        - connection string (e.g. ldap://foo.com, ldaps://bar.com)
-            user_base  - base of the users subtree
-            group_base - baes of the group subtree
-
-        Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
-                     'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
-                     'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
-
-        """
-
-        # open the connection
-        self.ldap = ldap.initialize(uri)
-
-        # authenticate
-        self.ldap.simple_bind_s('', '')
-
-        self.user_base = user_base
-        self.group_base = group_base
 
 
-    def connect_sasl(self, uri, mech, realm, user_base, group_base):
+def connect_sasl(uri, mech, realm):
 
-        # open the connection
-        self.ldap = ldap.initialize(uri)
+    # open the connection
+    ld = ldap.initialize(uri)
 
-        # authenticate
-        sasl = Sasl(mech, realm)
-        self.ldap.sasl_interactive_bind_s('', sasl)
+    # authenticate
+    sasl = Sasl(mech, realm)
+    ld.sasl_interactive_bind_s('', sasl)
 
-        self.user_base = user_base
-        self.group_base = group_base
+    return ld
 
 
-    def disconnect(self):
-        """Close the connection to the LDAP server."""
-        
-        if self.ldap:
+def abslookup(ld, dn, objectclass=None):
 
-            # close connection
-            try:
-                self.ldap.unbind_s()
-                self.ldap = None
-            except ldap.LDAPError, e:
-                raise LDAPException("unable to disconnect: %s" % e)
-
-
-    def connected(self):
-        """Determine whether the connection has been established."""
-
-        return self.ldap is not None
-
-
-
-    ### Helper Methods ###
-
-    def lookup(self, dn, objectClass=None):
-        """
-        Helper method to retrieve the attributes of an entry.
-
-        Parameters:
-            dn - the distinguished name of the directory entry
-
-        Returns: a dictionary of attributes of the matched dn, or
-                 None of the dn does not exist in the directory
-        """
-
-        if not self.connected(): raise LDAPException("Not connected!")
-
-        # search for the specified dn
-        try:
-            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:
-            raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
+    # search for the specified dn
+    try:
+        if objectclass:
+            search_filter = '(objectclass=%s)' % escape(objectclass)
+            matches = ld.search_s(dn, ldap.SCOPE_BASE, search_filter)
+        else:
+            matches = ld.search_s(dn, ldap.SCOPE_BASE)
+    except ldap.NO_SUCH_OBJECT:
+        return None
             
-        # 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
-        match = matches[0]
-        match_dn, match_attributes = match
-        return match_attributes
-
-
-
-    ### User-related Methods ###
-
-    def user_lookup(self, uid, objectClass=None):
-        """
-        Retrieve the attributes of a user.
-
-        Parameters:
-            uid - the uid to look up
-
-        Returns: attributes of user with uid
-        """
-
-        dn = 'uid=' + uid + ',' + self.user_base
-        return self.lookup(dn, objectClass)
-
+    # dn was found, but didn't match the objectclass filter
+    if len(matches) < 1:
+        return None
 
-    def user_search(self, search_filter, params):
-        """
-        Search for users with a filter.
+    # return the attributes of the single successful match
+    match = matches[0]
+    match_dn, match_attributes = match
+    return match_attributes
 
-        Parameters:
-            search_filter - LDAP filter string to match users against
 
-        Returns: a dictionary mapping uids to attributes
-        """
+def lookup(ld, rdntype, rdnval, base, objectclass=None):
+    dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
+    return abslookup(ld, dn, objectclass)
 
-        if not self.connected(): raise LDAPException("Not connected!")
 
-        search_filter = search_filter % tuple(self.escape(x) for x in params)
+def search(ld, base, search_filter, params, scope=ldap.SCOPE_SUBTREE, attrlist=None, attrsonly=0):
 
-        # 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)
+    real_filter = search_filter % tuple(escape(x) for x in params)
 
-        results = {}
-        for match in matches:
-            dn, attrs = match
-            uid = attrs['uid'][0]
-            results[uid] = attrs
+    # search for entries that match the filter
+    matches = ld.search_s(base, scope, real_filter, attrlist, attrsonly)
+    return matches
 
-        return results
 
+def modify(ld, rdntype, rdnval, base, mlist):
+    dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
+    ld.modify_s(dn, mlist)
 
-    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 modify_attrs(ld, rdntype, rdnval, base, old, attrs):
+    dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
 
-        Example: user = user_lookup('mspang')
-                 user['uidNumber'] = [ '0' ]
-                 connection.user_modify('mspang', user)
-        """
+    # build list of modifications to make
+    changes = ldap.modlist.modifyModlist(old, attrs)
 
-        # distinguished name of the entry to modify
-        dn = 'uid=' + uid + ',' + self.user_base
+    # apply changes
+    ld.modify_s(dn, changes)
 
-        # retrieve current state of user
-        old_user = self.user_lookup(uid)
 
-        try:
+def modify_diff(ld, rdntype, rdnval, base, old, new):
+    dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
 
-            # build list of modifications to make
-            changes = ldap.modlist.modifyModlist(old_user, attrs)
+    # build list of modifications to make
+    changes = make_modlist(old, new)
 
-            # apply changes
-            self.ldap.modify_s(dn, changes)
+    # apply changes
+    ld.modify_s(dn, changes)
 
-        except ldap.LDAPError, e:
-            raise LDAPException("unable to modify: %s" % e)
-
-
-
-    ### Group-related Methods ###
-
-    def group_lookup(self, cn):
-        """
-        Retrieves the attributes of a group.
-
-        Parameters:
-            cn - the UNIX group name to lookup
-
-        Returns: attributes of the group's LDAP entry
-
-        Example: connection.group_lookup('office') -> {
-                     'cn': 'office',
-                     'gidNumber', '1001',
-                     ...
-                 }
-        """
-
-        dn = 'cn=' + cn + ',' + self.group_base
-        return self.lookup(dn, 'posixGroup')
-
-
-    ### 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)
-
-
-
-    ### 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 escape(value):
+    """
+    Escapes special characters in a value so that it may be safely inserted
+    into an LDAP search filter.
+    """
 
-    def make_modlist(self, old, new):
-        keys = set(old.keys()).union(set(new))
-        mlist = []
-        for key in keys:
-            if key in old and not key in new:
-                mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
-            elif key in new and not key in old:
-                mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
-            else:
-                to_add = list(set(new[key]) - set(old[key]))
-                if len(to_add) > 0:
-                    mlist.append((ldap.MOD_ADD, key, to_add))
-                to_del = list(set(old[key]) - set(new[key]))
-                if len(to_del) > 0:
-                    mlist.append((ldap.MOD_DELETE, key, to_del))
-        return mlist
+    value = str(value)
+    value = value.replace('\\', '\\5c').replace('*', '\\2a')
+    value = value.replace('(', '\\28').replace(')', '\\29')
+    value = value.replace('\x00', '\\00')
+    return value
+
+
+def make_modlist(old, new):
+    keys = set(old.keys()).union(set(new))
+    mlist = []
+    for key in keys:
+        if key in old and not key in new:
+            mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
+        elif key in new and not key in old:
+            mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
+        else:
+            to_add = list(set(new[key]) - set(old[key]))
+            if len(to_add) > 0:
+                mlist.append((ldap.MOD_ADD, key, to_add))
+            to_del = list(set(old[key]) - set(new[key]))
+            if len(to_del) > 0:
+                mlist.append((ldap.MOD_DELETE, key, to_del))
+    return mlist
 
 
 class Sasl:
index 9da1e61..373dc00 100644 (file)
@@ -10,7 +10,7 @@ Future changes to the members database that need to be atomic
 must also be moved into this module.
 """
 import re, subprocess, ldap
-from ceo import conf, excep, ldapi
+from ceo import conf, ldapi
 from ceo.excep import InvalidArgument
 
 
@@ -43,8 +43,8 @@ def configure():
 
 ### Exceptions ###
 
+LDAPException = ldap.LDAPError
 ConfigurationException = conf.ConfigurationException
-LDAPException = ldapi.LDAPException
 
 class MemberException(Exception):
     """Base exception class for member-related errors."""
@@ -76,26 +76,30 @@ class ChildFailed(MemberException):
 ### Connection Management ###
 
 # global directory connection
-ldap_connection = ldapi.LDAPConnection()
+ld = None
 
 def connect():
     """Connect to LDAP."""
 
     configure()
 
-    ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
-        cfg['sasl_realm'], cfg['users_base'], cfg['groups_base'])
+    global ld
+    ld = ldapi.connect_sasl(cfg['server_url'],
+            cfg['sasl_mech'], cfg['sasl_realm'])
+
 
 def disconnect():
     """Disconnect from LDAP."""
 
-    ldap_connection.disconnect()
+    global ld
+    ld.unbind_s()
+    ld = None
 
 
 def connected():
     """Determine whether the connection has been established."""
 
-    return ldap_connection.connected()
+    return ld and ld.connected()
 
 
 
@@ -149,7 +153,7 @@ def get(userid):
              }
     """
 
-    return ldap_connection.user_lookup(userid)
+    return ldapi.lookup(ld, 'uid', userid, cfg['users_base'])
 
 
 def list_term(term):
@@ -168,7 +172,10 @@ def list_term(term):
              }
     """
 
-    return ldap_connection.member_search_term(term)
+    members = ldapi.search(ld, cfg['users_base'],
+            '(&(objectClass=member)(term=%s))', [ term ])
+
+    return dict([(member['uid'], member) for member in members])
 
 
 def list_name(name):
@@ -177,7 +184,8 @@ def list_name(name):
 
     Parameters:
         name - the name to match members against
-Returns: a list of member dictionaries
+
+    Returns: a list of member dictionaries
 
     Example: list_name('Spang'): -> {
                  'mspang': { 'cn': 'Michael Spang', ... },
@@ -185,7 +193,10 @@ Returns: a list of member dictionaries
              ]
     """
 
-    return ldap_connection.member_search_name(name)
+    members = ldapi.search(ld, cfg['users_base'],
+            '(&(objectClass=member)(cn~=%s))', [ name ])
+
+    return dict([(member['uid'], member) for member in members])
 
 
 def list_group(group):
@@ -225,10 +236,7 @@ def list_positions():
              ]
     """
 
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-
-    members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
+    members = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
     positions = {}
     for (_, member) in members:
         for position in member['position']:
@@ -237,6 +245,7 @@ def list_positions():
             positions[position][member['uid'][0]] = member
     return positions
 
+
 def set_position(position, members):
     """
     Sets a position
@@ -248,12 +257,8 @@ def set_position(position, members):
     Example: set_position('president', ['dtbartle'])
     """
 
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-    escape = ldap_connection.escape
-
-    res = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE,
-        '(&(objectClass=member)(position=%s))' % escape(position))
+    res = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE,
+        '(&(objectClass=member)(position=%s))' % ldapi.escape(position))
     old = set([ member['uid'][0] for (_, member) in res ])
     new = set(members)
     mods = {
@@ -265,7 +270,7 @@ def set_position(position, members):
 
     for action in ['del', 'add']:
         for userid in mods[action]:
-            dn = 'uid=%s,%s' % (escape(userid), user_base)
+            dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
             entry1 = {'position' : [position]}
             entry2 = {} #{'position' : []}
             entry = ()
@@ -273,19 +278,13 @@ def set_position(position, members):
                 entry = (entry1, entry2)
             elif action == 'add':
                 entry = (entry2, entry1)
-            mlist = ldap_connection.make_modlist(entry[0], entry[1])
-            ceo_ldap.modify_s(dn, mlist)
+            mlist = ldapi.make_modlist(entry[0], entry[1])
+            ld.modify_s(dn, mlist)
 
 
 def change_group_member(action, group, userid):
-
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-    group_base = ldap_connection.group_base
-    escape = ldap_connection.escape
-
-    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
-    group_dn = 'cn=%s,%s' % (escape(group), group_base)
+    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
+    group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['groups_base'])
     entry1 = {'uniqueMember' : []}
     entry2 = {'uniqueMember' : [user_dn]}
     entry = []
@@ -295,8 +294,8 @@ def change_group_member(action, group, userid):
         entry = (entry2, entry1)
     else:
         raise InvalidArgument("action", action, "invalid action")
-    mlist = ldap_connection.make_modlist(entry[0], entry[1])
-    ceo_ldap.modify_s(group_dn, mlist)
+    mlist = ldapi.make_modlist(entry[0], entry[1])
+    ld.modify_s(group_dn, mlist)
 
 
 
@@ -350,15 +349,12 @@ def register(userid, term_list):
     Example: register(3349, ["w2007", "s2007"])
     """
 
-    ceo_ldap = ldap_connection.ldap
-    user_base = ldap_connection.user_base
-    escape = ldap_connection.escape
-    user_dn = 'uid=%s,%s' % (escape(userid), user_base)
+    user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
 
     if type(term_list) in (str, unicode):
         term_list = [ term_list ]
 
-    ldap_member = ldap_connection.member_lookup(userid)
+    ldap_member = get(userid)
     if ldap_member and 'term' not in ldap_member:
         ldap_member['term'] = []
 
@@ -378,8 +374,8 @@ def register(userid, term_list):
         if not term in ldap_member['term']:
             new_member['term'].append(term)
 
-    mlist = ldap_connection.make_modlist(ldap_member, new_member)
-    ceo_ldap.modify_s(user_dn, mlist)
+    mlist = ldapi.make_modlist(ldap_member, new_member)
+    ld.modify_s(user_dn, mlist)
 
 
 def registered(userid, term):
@@ -396,7 +392,7 @@ def registered(userid, term):
     Example: registered("mspang", "f2006") -> True
     """
 
-    member = ldap_connection.member_lookup(userid)
+    member = get(userid)
     return 'term' in member and term in member['term']
 
 
@@ -406,7 +402,8 @@ def group_members(group):
     Returns a list of group members
     """
 
-    group = ldap_connection.group_lookup(group)
+    group = ldapi.lookup(ld, 'cn', group, cfg['groups_base'])
+
     if group:
         if 'uniqueMember' in group:
             r = re.compile('^uid=([^,]*)')