diff --git a/ceo/ldapi.py b/ceo/ldapi.py index 23e2ee0..c2fc690 100644 --- a/ceo/ldapi.py +++ b/ceo/ldapi.py @@ -1,365 +1,114 @@ """ -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.""" +def connect_sasl(uri, mech, realm): + + # open the connection + ld = ldap.initialize(uri) + + # authenticate + sasl = Sasl(mech, realm) + ld.sasl_interactive_bind_s('', sasl) + + return ld -class LDAPConnection(object): - """ - Connection to the LDAP directory. All directory - queries and updates are made via this class. +def abslookup(ld, dn, objectclass=None): - 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): - - # open the connection - self.ldap = ldap.initialize(uri) - - # authenticate - sasl = Sasl(mech, realm) - self.ldap.sasl_interactive_bind_s('', sasl) - - self.user_base = user_base - self.group_base = group_base - - - def disconnect(self): - """Close the connection to the LDAP server.""" - - if self.ldap: - - # 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 + if len(matches) < 1: + return None - # 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 - # return the attributes of the single successful match - match = matches[0] - match_dn, match_attributes = match - return match_attributes +def lookup(ld, rdntype, rdnval, base, objectclass=None): + dn = '%s=%s,%s' % (rdntype, escape(rdnval), base) + return abslookup(ld, dn, objectclass) - ### User-related Methods ### +def search(ld, base, search_filter, params, scope=ldap.SCOPE_SUBTREE, attrlist=None, attrsonly=0): - def user_lookup(self, uid, objectClass=None): - """ - Retrieve the attributes of a user. + real_filter = search_filter % tuple(escape(x) for x in params) - Parameters: - uid - the uid to look up + # search for entries that match the filter + matches = ld.search_s(base, scope, real_filter, attrlist, attrsonly) + return matches - Returns: attributes of user with uid - """ - dn = 'uid=' + uid + ',' + self.user_base - return self.lookup(dn, objectClass) +def modify(ld, rdntype, rdnval, base, mlist): + dn = '%s=%s,%s' % (rdntype, escape(rdnval), base) + ld.modify_s(dn, mlist) - def user_search(self, search_filter, params): - """ - Search for users with a filter. +def modify_attrs(ld, rdntype, rdnval, base, old, attrs): + dn = '%s=%s,%s' % (rdntype, escape(rdnval), base) - Parameters: - search_filter - LDAP filter string to match users against + # build list of modifications to make + changes = ldap.modlist.modifyModlist(old, attrs) - Returns: a dictionary mapping uids to attributes - """ + # apply changes + ld.modify_s(dn, changes) - if not self.connected(): raise LDAPException("Not connected!") - search_filter = search_filter % tuple(self.escape(x) for x in params) +def modify_diff(ld, rdntype, rdnval, base, old, new): + dn = '%s=%s,%s' % (rdntype, escape(rdnval), base) - # 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) + # build list of modifications to make + changes = make_modlist(old, new) - results = {} - for match in matches: - dn, attrs = match - uid = attrs['uid'][0] - results[uid] = attrs + # apply changes + ld.modify_s(dn, changes) - return results +def escape(value): + """ + Escapes special characters in a value so that it may be safely inserted + into an LDAP search filter. + """ - def user_modify(self, uid, attrs): - """ - Update user attributes in the directory. + value = str(value) + value = value.replace('\\', '\\5c').replace('*', '\\2a') + value = value.replace('(', '\\28').replace(')', '\\29') + value = value.replace('\x00', '\\00') + return value - 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) - - - - ### 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 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 +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: diff --git a/ceo/members.py b/ceo/members.py index 9da1e61..373dc00 100644 --- a/ceo/members.py +++ b/ceo/members.py @@ -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=([^,]*)')