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,139 +1,38 @@
""" """
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."""
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 # open the connection
self.ldap = ldap.initialize(uri) ld = 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 # authenticate
sasl = Sasl(mech, realm) sasl = Sasl(mech, realm)
self.ldap.sasl_interactive_bind_s('', sasl) ld.sasl_interactive_bind_s('', sasl)
self.user_base = user_base return ld
self.group_base = group_base
def disconnect(self): def abslookup(ld, dn, objectclass=None):
"""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 # search for the specified dn
try: try:
if objectClass: if objectclass:
search_filter = '(objectClass=%s)' % self.escape(objectClass) search_filter = '(objectclass=%s)' % escape(objectclass)
matches = self.ldap.search_s(dn, ldap.SCOPE_BASE, search_filter) matches = ld.search_s(dn, ldap.SCOPE_BASE, search_filter)
else: else:
matches = self.ldap.search_s(dn, ldap.SCOPE_BASE) matches = ld.search_s(dn, ldap.SCOPE_BASE)
except ldap.NO_SUCH_OBJECT: except ldap.NO_SUCH_OBJECT:
return None 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)
# dn was found, but didn't match the objectClass filter
elif len(matches) < 1:
return None return None
# return the attributes of the single successful match # return the attributes of the single successful match
@ -142,196 +41,46 @@ class LDAPConnection(object):
return match_attributes return match_attributes
def lookup(ld, rdntype, rdnval, base, objectclass=None):
### User-related Methods ### dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
return abslookup(ld, dn, objectclass)
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): def search(ld, base, search_filter, params, scope=ldap.SCOPE_SUBTREE, attrlist=None, attrsonly=0):
"""
Search for users with a filter.
Parameters: real_filter = search_filter % tuple(escape(x) for x in params)
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 # search for entries that match the filter
try: matches = ld.search_s(base, scope, real_filter, attrlist, attrsonly)
matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter) return matches
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): def modify(ld, rdntype, rdnval, base, mlist):
""" dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
Update user attributes in the directory. ld.modify_s(dn, mlist)
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 modify_attrs(ld, rdntype, rdnval, base, old, attrs):
user['uidNumber'] = [ '0' ] dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
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 # build list of modifications to make
changes = ldap.modlist.modifyModlist(old_user, attrs) changes = ldap.modlist.modifyModlist(old, attrs)
# apply changes # apply changes
self.ldap.modify_s(dn, changes) ld.modify_s(dn, changes)
except ldap.LDAPError, e:
raise LDAPException("unable to modify: %s" % e)
def modify_diff(ld, rdntype, rdnval, base, old, new):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
### Group-related Methods ### # build list of modifications to make
changes = make_modlist(old, new)
def group_lookup(self, cn): # apply changes
""" ld.modify_s(dn, changes)
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 escape(value):
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 Escapes special characters in a value so that it may be safely inserted
into an LDAP search filter. into an LDAP search filter.
@ -344,7 +93,7 @@ class LDAPConnection(object):
return value return value
def make_modlist(self, old, new): def make_modlist(old, new):
keys = set(old.keys()).union(set(new)) keys = set(old.keys()).union(set(new))
mlist = [] mlist = []
for key in keys: for key in keys:

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,6 +184,7 @@ 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'): -> {
@ -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=([^,]*)')