Use python-ldap directly in members

This leaves only utility functions in ldapi.
This commit is contained in:
Michael Spang 2007-12-13 04:20:25 -05:00
parent b8be0f8149
commit 217c9806f1
2 changed files with 117 additions and 371 deletions

View File

@ -1,365 +1,114 @@
""" """
LDAP Backend Interface LDAP Utilities
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.
This module makes use of python-ldap, a Python module with bindings This module makes use of python-ldap, a Python module with bindings
to libldap, OpenLDAP's native C client library. to libldap, OpenLDAP's native C client library.
""" """
import ldap.modlist import ldap.modlist
from subprocess import Popen, PIPE
class LDAPException(Exception): def connect_sasl(uri, mech, realm):
"""Exception class for LDAP-related errors."""
# open the connection
ld = ldap.initialize(uri)
# authenticate
sasl = Sasl(mech, realm)
ld.sasl_interactive_bind_s('', sasl)
return ld
class LDAPConnection(object): def abslookup(ld, dn, objectclass=None):
"""
Connection to the LDAP directory. All directory
queries and updates are made via this class.
Exceptions: (all methods) # search for the specified dn
LDAPException - on directory query failure try:
if objectclass:
Example: search_filter = '(objectclass=%s)' % escape(objectclass)
connection = LDAPConnection() matches = ld.search_s(dn, ldap.SCOPE_BASE, search_filter)
connection.connect(...) else:
matches = ld.search_s(dn, ldap.SCOPE_BASE)
# make queries and updates, e.g. except ldap.NO_SUCH_OBJECT:
connection.user_delete('mspang') return None
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))
# this should never happen due to the nature of DNs # dn was found, but didn't match the objectclass filter
if len(matches) > 1: if len(matches) < 1:
raise LDAPException("duplicate dn in ldap: " + dn) return None
# dn was found, but didn't match the objectClass filter # return the attributes of the single successful match
elif len(matches) < 1: match = matches[0]
return None 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): real_filter = search_filter % tuple(escape(x) for x in params)
"""
Retrieve the attributes of a user.
Parameters: # search for entries that match the filter
uid - the uid to look up matches = ld.search_s(base, scope, real_filter, attrlist, attrsonly)
return matches
Returns: attributes of user with uid
"""
dn = 'uid=' + uid + ',' + self.user_base def modify(ld, rdntype, rdnval, base, mlist):
return self.lookup(dn, objectClass) dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
ld.modify_s(dn, mlist)
def user_search(self, search_filter, params): def modify_attrs(ld, rdntype, rdnval, base, old, attrs):
""" dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
Search for users with a filter.
Parameters: # build list of modifications to make
search_filter - LDAP filter string to match users against 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 # build list of modifications to make
try: changes = make_modlist(old, new)
matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
except ldap.LDAPError, e:
raise LDAPException("user search failed: %s" % e)
results = {} # apply changes
for match in matches: ld.modify_s(dn, changes)
dn, attrs = match
uid = attrs['uid'][0]
results[uid] = attrs
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): value = str(value)
""" value = value.replace('\\', '\\5c').replace('*', '\\2a')
Update user attributes in the directory. 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') def make_modlist(old, new):
user['uidNumber'] = [ '0' ] keys = set(old.keys()).union(set(new))
connection.user_modify('mspang', user) mlist = []
""" for key in keys:
if key in old and not key in new:
# distinguished name of the entry to modify mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
dn = 'uid=' + uid + ',' + self.user_base elif key in new and not key in old:
mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
# retrieve current state of user else:
old_user = self.user_lookup(uid) to_add = list(set(new[key]) - set(old[key]))
if len(to_add) > 0:
try: mlist.append((ldap.MOD_ADD, key, to_add))
to_del = list(set(old[key]) - set(new[key]))
# build list of modifications to make if len(to_del) > 0:
changes = ldap.modlist.modifyModlist(old_user, attrs) mlist.append((ldap.MOD_DELETE, key, to_del))
return mlist
# 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
class Sasl: class Sasl:

View File

@ -10,7 +10,7 @@ Future changes to the members database that need to be atomic
must also be moved into this module. must also be moved into this module.
""" """
import re, subprocess, ldap import re, subprocess, ldap
from ceo import conf, excep, ldapi from ceo import conf, ldapi
from ceo.excep import InvalidArgument from ceo.excep import InvalidArgument
@ -43,8 +43,8 @@ def configure():
### Exceptions ### ### Exceptions ###
LDAPException = ldap.LDAPError
ConfigurationException = conf.ConfigurationException ConfigurationException = conf.ConfigurationException
LDAPException = ldapi.LDAPException
class MemberException(Exception): class MemberException(Exception):
"""Base exception class for member-related errors.""" """Base exception class for member-related errors."""
@ -76,26 +76,30 @@ class ChildFailed(MemberException):
### Connection Management ### ### Connection Management ###
# global directory connection # global directory connection
ldap_connection = ldapi.LDAPConnection() ld = None
def connect(): def connect():
"""Connect to LDAP.""" """Connect to LDAP."""
configure() configure()
ldap_connection.connect_sasl(cfg['server_url'], cfg['sasl_mech'], global ld
cfg['sasl_realm'], cfg['users_base'], cfg['groups_base']) ld = ldapi.connect_sasl(cfg['server_url'],
cfg['sasl_mech'], cfg['sasl_realm'])
def disconnect(): def disconnect():
"""Disconnect from LDAP.""" """Disconnect from LDAP."""
ldap_connection.disconnect() global ld
ld.unbind_s()
ld = None
def connected(): def connected():
"""Determine whether the connection has been established.""" """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): 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): def list_name(name):
@ -177,7 +184,8 @@ def list_name(name):
Parameters: Parameters:
name - the name to match members against name - the name to match members against
Returns: a list of member dictionaries
Returns: a list of member dictionaries
Example: list_name('Spang'): -> { Example: list_name('Spang'): -> {
'mspang': { 'cn': 'Michael 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): def list_group(group):
@ -225,10 +236,7 @@ def list_positions():
] ]
""" """
ceo_ldap = ldap_connection.ldap members = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
user_base = ldap_connection.user_base
members = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE, '(position=*)')
positions = {} positions = {}
for (_, member) in members: for (_, member) in members:
for position in member['position']: for position in member['position']:
@ -237,6 +245,7 @@ def list_positions():
positions[position][member['uid'][0]] = member positions[position][member['uid'][0]] = member
return positions return positions
def set_position(position, members): def set_position(position, members):
""" """
Sets a position Sets a position
@ -248,12 +257,8 @@ def set_position(position, members):
Example: set_position('president', ['dtbartle']) Example: set_position('president', ['dtbartle'])
""" """
ceo_ldap = ldap_connection.ldap res = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE,
user_base = ldap_connection.user_base '(&(objectClass=member)(position=%s))' % ldapi.escape(position))
escape = ldap_connection.escape
res = ceo_ldap.search_s(user_base, ldap.SCOPE_SUBTREE,
'(&(objectClass=member)(position=%s))' % escape(position))
old = set([ member['uid'][0] for (_, member) in res ]) old = set([ member['uid'][0] for (_, member) in res ])
new = set(members) new = set(members)
mods = { mods = {
@ -265,7 +270,7 @@ def set_position(position, members):
for action in ['del', 'add']: for action in ['del', 'add']:
for userid in mods[action]: 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]} entry1 = {'position' : [position]}
entry2 = {} #{'position' : []} entry2 = {} #{'position' : []}
entry = () entry = ()
@ -273,19 +278,13 @@ def set_position(position, members):
entry = (entry1, entry2) entry = (entry1, entry2)
elif action == 'add': elif action == 'add':
entry = (entry2, entry1) entry = (entry2, entry1)
mlist = ldap_connection.make_modlist(entry[0], entry[1]) mlist = ldapi.make_modlist(entry[0], entry[1])
ceo_ldap.modify_s(dn, mlist) ld.modify_s(dn, mlist)
def change_group_member(action, group, userid): def change_group_member(action, group, userid):
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
ceo_ldap = ldap_connection.ldap group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['groups_base'])
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)
entry1 = {'uniqueMember' : []} entry1 = {'uniqueMember' : []}
entry2 = {'uniqueMember' : [user_dn]} entry2 = {'uniqueMember' : [user_dn]}
entry = [] entry = []
@ -295,8 +294,8 @@ def change_group_member(action, group, userid):
entry = (entry2, entry1) entry = (entry2, entry1)
else: else:
raise InvalidArgument("action", action, "invalid action") raise InvalidArgument("action", action, "invalid action")
mlist = ldap_connection.make_modlist(entry[0], entry[1]) mlist = ldapi.make_modlist(entry[0], entry[1])
ceo_ldap.modify_s(group_dn, mlist) ld.modify_s(group_dn, mlist)
@ -350,15 +349,12 @@ def register(userid, term_list):
Example: register(3349, ["w2007", "s2007"]) Example: register(3349, ["w2007", "s2007"])
""" """
ceo_ldap = ldap_connection.ldap user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
user_base = ldap_connection.user_base
escape = ldap_connection.escape
user_dn = 'uid=%s,%s' % (escape(userid), user_base)
if type(term_list) in (str, unicode): if type(term_list) in (str, unicode):
term_list = [ term_list ] term_list = [ term_list ]
ldap_member = ldap_connection.member_lookup(userid) ldap_member = get(userid)
if ldap_member and 'term' not in ldap_member: if ldap_member and 'term' not in ldap_member:
ldap_member['term'] = [] ldap_member['term'] = []
@ -378,8 +374,8 @@ def register(userid, term_list):
if not term in ldap_member['term']: if not term in ldap_member['term']:
new_member['term'].append(term) new_member['term'].append(term)
mlist = ldap_connection.make_modlist(ldap_member, new_member) mlist = ldapi.make_modlist(ldap_member, new_member)
ceo_ldap.modify_s(user_dn, mlist) ld.modify_s(user_dn, mlist)
def registered(userid, term): def registered(userid, term):
@ -396,7 +392,7 @@ def registered(userid, term):
Example: registered("mspang", "f2006") -> True Example: registered("mspang", "f2006") -> True
""" """
member = ldap_connection.member_lookup(userid) member = get(userid)
return 'term' in member and term in member['term'] return 'term' in member and term in member['term']
@ -406,7 +402,8 @@ def group_members(group):
Returns a list of group members Returns a list of group members
""" """
group = ldap_connection.group_lookup(group) group = ldapi.lookup(ld, 'cn', group, cfg['groups_base'])
if group: if group:
if 'uniqueMember' in group: if 'uniqueMember' in group:
r = re.compile('^uid=([^,]*)') r = re.compile('^uid=([^,]*)')