pyceo-broken/ceo/ldapi.py

373 lines
10 KiB
Python

"""
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.
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):
# 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
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)
def user_search(self, search_filter, params):
"""
Search for users with a filter.
Parameters:
search_filter - LDAP filter string to match users against
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)
results = {}
for match in matches:
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)
### 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:
def __init__(self, mech, realm):
self.mech = mech
self.realm = realm
def callback(self, id, challenge, prompt, defresult):
return ''