Use python-ldap directly in members

This leaves only utility functions in ldapi.
master
Michael Spang 16 years ago
parent b8be0f8149
commit 217c9806f1
  1. 403
      ceo/ldapi.py
  2. 83
      ceo/members.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."""
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:

@ -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=([^,]*)')

Loading…
Cancel
Save