PgSQL to LDAP transition - Phase 1: Added LDAP support for member data.

This is the first major update for the PgSQL to LDAP transition. We will be
deprecating the members table in PostgreSQL and moving that information into
the LDAP directory.

With this commit, all newly created members will have their attributes added
to LDAP in addition to the members table in the database. The database is still
used as the canonical source of this information: the information in LDAP is
kept up-to-date but almost never referenced.
This commit is contained in:
Michael Spang 2007-02-18 19:46:15 -05:00
parent a8268ce266
commit 7b81a13f78
4 changed files with 474 additions and 194 deletions

View File

@ -190,8 +190,15 @@ def create(username, name, minimum_id, maximum_id, home, password=None, descript
### User creation ###
# create the LDAP entry
ldap_connection.user_add(username, name, userid, gid, home, shell, gecos, description)
if not ldap_connection.user_lookup(username):
# create the LDAP entry
ldap_connection.account_add(username, name, userid, gid, home, shell, gecos, description)
else:
# add the required attribute to the LDAP entry
ldap_connection.member_add_account(username, userid, gid, home, shell, gecos)
# create a user group if no other group was specified
if not group:
@ -257,7 +264,7 @@ def status(username):
Returns: a boolean 2-tuple (exists, has_password)
"""
ldap_state = ldap_connection.user_lookup(username)
ldap_state = ldap_connection.account_lookup(username)
krb_state = krb_connection.get_principal(username)
return (ldap_state is not None, krb_state is not None)
@ -786,7 +793,7 @@ def check_name_usage(name):
"""
# see if user exists in LDAP
if ldap_connection.user_lookup(name):
if ldap_connection.account_lookup(name):
raise NameConflict(name, "account", "LDAP")
# see if group exists in LDAP
@ -818,7 +825,7 @@ def check_account_status(username, require_ldap=True, require_krb=False):
if not connected():
raise AccountException("Not connected to LDAP and Kerberos")
if require_ldap and not ldap_connection.user_lookup(username):
if require_ldap and not ldap_connection.account_lookup(username):
raise NoSuchAccount(username, "LDAP")
if require_krb and not krb_connection.get_principal(username):
raise NoSuchAccount(username, "KRB")

View File

@ -11,8 +11,9 @@ must also be moved into this module.
"""
import re
from csc.adm import terms
from csc.backends import db
from csc.backends import db, ldapi
from csc.common import conf
from csc.common.excep import InvalidArgument
### Configuration ###
@ -25,7 +26,8 @@ def load_configuration():
"""Load Members Configuration"""
string_fields = [ 'studentid_regex', 'realname_regex', 'server',
'database', 'user', 'password' ]
'database', 'user', 'password', 'server_url', 'users_base',
'groups_base', 'admin_bind_dn', 'admin_bind_pw' ]
# read configuration file
cfg_tmp = conf.read(CONFIG_FILE)
@ -86,41 +88,46 @@ class NoSuchMember(MemberException):
### Connection Management ###
# global database connection
connection = db.DBConnection()
db_connection = db.DBConnection()
# global directory connection
ldap_connection = ldapi.LDAPConnection()
def connect():
"""Connect to PostgreSQL."""
load_configuration()
connection.connect(cfg['server'], cfg['database'])
db_connection.connect(cfg['server'], cfg['database'])
ldap_connection.connect(cfg['server_url'], cfg['admin_bind_dn'], cfg['admin_bind_pw'], cfg['users_base'], cfg['groups_base'])
def disconnect():
"""Disconnect from PostgreSQL."""
connection.disconnect()
db_connection.disconnect()
ldap_connection.disconnect()
def connected():
"""Determine whether the connection has been established."""
"""Determine whether the db_connection has been established."""
return connection.connected()
return db_connection.connected() and ldap_connection.connected()
### Member Table ###
def new(realname, studentid=None, program=None, mtype='user', userid=None):
def new(uid, realname, studentid=None, program=None, mtype='user'):
"""
Registers a new CSC member. The member is added to the members table
and registered for the current term.
Parameters:
uid - the initial user id
realname - the full real name of the member
studentid - the student id number of the member
program - the program of study of the member
mtype - a string describing the type of member ('user', 'club')
userid - the initial user id
Returns: the memberid of the new member
@ -135,7 +142,7 @@ def new(realname, studentid=None, program=None, mtype='user', userid=None):
# blank attributes should be NULL
if studentid == '': studentid = None
if program == '': program = None
if userid == '': userid = None
if uid == '': uid = None
if mtype == '': mtype = None
# check the student id format
@ -147,18 +154,33 @@ def new(realname, studentid=None, program=None, mtype='user', userid=None):
raise InvalidRealName(realname)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
member = db_connection.select_member_by_studentid(studentid) or \
ldap_connection.member_search_studentid(studentid)
if member:
raise DuplicateStudentID(studentid)
# add the member
memberid = connection.insert_member(realname, studentid, program)
# check for duplicate userid
member = db_connection.select_member_by_userid(uid) or \
ldap_connection.user_lookup(uid)
if member:
raise InvalidArgument("uid", uid, "duplicate uid")
# register them for this term
connection.insert_term(memberid, terms.current())
# add the member to the database
memberid = db_connection.insert_member(realname, studentid, program, userid=uid)
# commit the transaction
connection.commit()
# add the member to the directory
ldap_connection.member_add(uid, realname, studentid, program)
# register them for this term in the database
db_connection.insert_term(memberid, terms.current())
# register them for this term in the directory
member = ldap_connection.member_lookup(uid)
member['term'] = [ terms.current() ]
ldap_connection.user_modify(uid, member)
# commit the database transaction
db_connection.commit()
return memberid
@ -177,7 +199,7 @@ def get(memberid):
}
"""
return connection.select_member_by_id(memberid)
return db_connection.select_member_by_id(memberid)
def get_userid(userid):
@ -197,7 +219,7 @@ def get_userid(userid):
}
"""
return connection.select_member_by_userid(userid)
return db_connection.select_member_by_userid(userid)
def get_studentid(studentid):
@ -217,7 +239,7 @@ def get_studentid(studentid):
}
"""
return connection.select_member_by_studentid(studentid)
return db_connection.select_member_by_studentid(studentid)
def list_term(term):
@ -237,7 +259,7 @@ def list_term(term):
"""
# retrieve a list of memberids in term
memberlist = connection.select_members_by_term(term)
memberlist = db_connection.select_members_by_term(term)
return memberlist.values()
@ -259,7 +281,7 @@ def list_name(name):
"""
# retrieve a list of memberids matching name
memberlist = connection.select_members_by_name(name)
memberlist = db_connection.select_members_by_name(name)
return memberlist.values()
@ -272,7 +294,7 @@ def list_all():
"""
# retrieve a list of members
memberlist = connection.select_all_members()
memberlist = db_connection.select_all_members()
return memberlist.values()
@ -292,18 +314,23 @@ def delete(memberid):
"""
# save member data
member = connection.select_member_by_id(memberid)
member = db_connection.select_member_by_id(memberid)
# bail if not found
if not member:
raise NoSuchMember(memberid)
term_list = connection.select_terms(memberid)
term_list = db_connection.select_terms(memberid)
# remove data from the db
connection.delete_term_all(memberid)
connection.delete_member(memberid)
connection.commit()
db_connection.delete_term_all(memberid)
db_connection.delete_member(memberid)
db_connection.commit()
# remove data from the directory
if member and member['userid']:
uid = member['userid']
ldap_connection.user_delete(uid)
return (member, term_list)
@ -334,7 +361,7 @@ def update(member):
raise InvalidStudentID(studentid)
# check for duplicate student id
dupmember = connection.select_member_by_studentid(studentid)
dupmember = db_connection.select_member_by_studentid(studentid)
if dupmember:
raise DuplicateStudentID(studentid)
@ -348,10 +375,10 @@ def update(member):
raise NoSuchMember(memberid)
# do the update
connection.update_member(member)
db_connection.update_member(member)
# commit the transaction
connection.commit()
db_connection.commit()
@ -376,6 +403,14 @@ def register(memberid, term_list):
if type(term_list) in (str, unicode):
term_list = [ term_list ]
ldap_member = None
db_member = get(memberid)
if db_member['userid']:
uid = db_member['userid']
ldap_member = ldap_connection.member_lookup(uid)
if ldap_member and 'term' not in ldap_member:
ldap_member['term'] = []
for term in term_list:
# check term syntax
@ -383,9 +418,16 @@ def register(memberid, term_list):
raise InvalidTerm(term)
# add term to database
connection.insert_term(memberid, term)
db_connection.insert_term(memberid, term)
connection.commit()
# add the term to the directory
if ldap_member:
ldap_member['term'].append(term)
if ldap_member:
ldap_connection.user_modify(uid, ldap_member)
db_connection.commit()
def registered(memberid, term):
@ -402,7 +444,7 @@ def registered(memberid, term):
Example: registered(3349, "f2006") -> True
"""
return connection.select_term(memberid, term) is not None
return db_connection.select_term(memberid, term) is not None
def member_terms(memberid):
@ -418,7 +460,7 @@ def member_terms(memberid):
Example: registered(0) -> 's1993'
"""
terms_list = connection.select_terms(memberid)
terms_list = db_connection.select_terms(memberid)
terms_list.sort(terms.compare)
return terms_list
@ -432,18 +474,19 @@ if __name__ == '__main__':
# t=test m=member s=student u=updated
tmname = 'Test Member'
tmuid = 'testmember'
tmprogram = 'Metaphysics'
tmsid = '00000000'
tm2name = 'Test Member 2'
tm2uid = 'testmember2'
tm2sid = '00000001'
tm2uname = 'Test Member II'
tm2usid = '00000002'
tm2uprogram = 'Pseudoscience'
tm2uuserid = 'testmember'
tmdict = {'name': tmname, 'userid': None, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
tm2dict = {'name': tm2name, 'userid': None, 'program': None, 'type': 'user', 'studentid': tm2sid }
tm2udict = {'name': tm2uname, 'userid': tm2uuserid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid }
tmdict = {'name': tmname, 'userid': tmuid, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
tm2dict = {'name': tm2name, 'userid': tm2uid, 'program': None, 'type': 'user', 'studentid': tm2sid }
tm2udict = {'name': tm2uname, 'userid': tm2uid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid }
thisterm = terms.current()
nextterm = terms.next(thisterm)
@ -464,8 +507,8 @@ if __name__ == '__main__':
if dmid: delete(dmid['memberid'])
test(new)
tmid = new(tmname, tmsid, tmprogram)
tm2id = new(tm2name, tm2sid)
tmid = new(tmuid, tmname, tmsid, tmprogram)
tm2id = new(tm2uid, tm2name, tm2sid)
success()
tmdict['memberid'] = tmid
@ -495,7 +538,7 @@ if __name__ == '__main__':
success()
test(register)
register(tmid, terms.next(terms.current()))
register(tmid, nextterm)
assert_equal(True, registered(tmid, nextterm))
success()
@ -517,7 +560,7 @@ if __name__ == '__main__':
success()
test(get_userid)
assert_equal(tm2udict, get_userid(tm2uuserid))
assert_equal(tm2udict, get_userid(tm2uid))
success()
test(get_studentid)

View File

@ -12,6 +12,7 @@ This frontend is poorly documented, deprecated, and undoubtedly full of bugs.
import curses.ascii, re, os
from helpers import menu, inputbox, msgbox, reset
from csc.adm import accounts, members, terms
from csc.common.excep import InvalidArgument
# color of the ceo border
BORDER_COLOR = curses.COLOR_RED
@ -20,7 +21,7 @@ BORDER_COLOR = curses.COLOR_RED
def action_new_member(wnd):
"""Interactively add a new member."""
studentid, program = None, ''
userid, studentid, program = '', None, ''
# read the name
prompt = " Name: "
@ -50,12 +51,21 @@ def action_new_member(wnd):
if program is None or program.lower() == 'exit':
return False
# read user id
prompt = "Userid:"
while userid == '':
userid = inputbox(wnd, prompt, 18)
# user abort
if userid is None or userid.lower() == 'exit':
return False
# connect the members module to its backend if necessary
if not members.connected(): members.connect()
# attempt to create the member
try:
memberid = members.new(realname, studentid, program)
memberid = members.new(userid, realname, studentid, program)
msgbox(wnd, "Success! Your memberid is %s. You are now registered\n"
% memberid + "for the " + terms.current() + " term.")
@ -69,6 +79,12 @@ def action_new_member(wnd):
except members.InvalidRealName:
msgbox(wnd, 'Invalid real name: "%s"' % realname)
return False
except InvalidArgument, e:
if e.argname == 'uid' and e.explanation == 'duplicate uid':
msgbox(wnd, 'A member with this user ID exists.')
return False
else:
raise
def action_term_register(wnd):
@ -196,7 +212,7 @@ def repair_account(wnd, memberid, userid):
password = input_password(wnd)
accounts.create_member(userid, password, member['name'], memberid)
msgbox(wnd, "Account created (where the hell did it go, anyway?)\n"
"If you're homedir still exists, it will not be inaccessible to you,\n"
"If your homedir still exists, it will not be inaccessible to you,\n"
"please contact systems-committee@csclub.uwaterloo.ca to get this resolved.\n")
elif not haspw:
@ -267,7 +283,8 @@ def action_create_account(wnd):
return False
# member already has an account?
if member['userid']:
if not accounts.connected(): accounts.connect()
if member['userid'] and accounts.status(member['userid'])[0]:
userid = member['userid']
msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
@ -278,6 +295,9 @@ def action_create_account(wnd):
return False
if member['userid']:
userid = member['userid']
# read user id
prompt = "Userid:"
while userid == '':

View File

@ -98,7 +98,7 @@ class LDAPConnection(object):
### Helper Methods ###
def lookup(self, dn):
def lookup(self, dn, objectClass=None):
"""
Helper method to retrieve the attributes of an entry.
@ -113,7 +113,11 @@ class LDAPConnection(object):
# search for the specified dn
try:
matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
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:
@ -123,148 +127,60 @@ class LDAPConnection(object):
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
else:
match = matches[0]
match_dn, match_attributes = match
return match_attributes
match = matches[0]
match_dn, match_attributes = match
return match_attributes
### User-related Methods ###
def user_lookup(self, uid):
def user_lookup(self, uid, objectClass=None):
"""
Retrieve the attributes of a user.
Parameters:
uid - the UNIX username to look up
uid - the uid to look up
Returns: attributes of user with uid
Example: connection.user_lookup('mspang') ->
{ 'uid': 'mspang', 'uidNumber': 21292 ...}
"""
dn = 'uid=' + uid + ',' + self.user_base
return self.lookup(dn)
return self.lookup(dn, objectClass)
def user_search(self, search_filter):
def user_search(self, search_filter, params):
"""
Helper for user searches.
Search for users with a filter.
Parameters:
search_filter - LDAP filter string to match users against
Returns: the list of uids matched (usernames)
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)
# list for uids found
uids = []
results = {}
for match in matches:
dn, attributes = match
dn, attrs = match
uid = attrs['uid'][0]
results[uid] = attrs
# uid is a required attribute of posixAccount
if not attributes.has_key('uid'):
raise LDAPException(dn + ' (posixAccount) has no uid')
# do not handle the case of multiple usernames in one entry (yet)
elif len(attributes['uid']) > 1:
raise LDAPException(dn + ' (posixAccount) has multiple uids')
# append the sole uid of this match to the list
uids.append( attributes['uid'][0] )
return uids
def user_search_id(self, uidNumber):
"""
Retrieves a list of users with a certain UNIX uid number.
LDAP (or passwd for that matter) does not enforce any
restriction on the number of accounts that can have
a certain UID. Therefore this method returns a list of matches.
Parameters:
uidNumber - the user id of the accounts desired
Returns: the list of uids matched (usernames)
Example: connection.user_search_id(21292) -> ['mspang']
"""
# search for posixAccount entries with the specified uidNumber
search_filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
return self.user_search(search_filter)
def user_search_gid(self, gidNumber):
"""
Retrieves a list of users with a certain UNIX gid
number (search by default group).
Returns: the list of uids matched (usernames)
"""
# search for posixAccount entries with the specified gidNumber
search_filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
return self.user_search(search_filter)
def user_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None):
"""
Adds a user to the directory.
Parameters:
uid - the UNIX username for the account
cn - the real name of the member
uidNumber - the UNIX user id number
gidNumber - the UNIX group id number (default group)
homeDirectory - home directory for the user
loginShell - login shell for the user
gecos - comment field (usually stores name etc)
description - description field (optional and unimportant)
Example: connection.user_add('mspang', 'Michael Spang',
21292, 100, '/users/mspang', '/bin/bash',
'Michael Spang,,,')
"""
if not self.connected(): raise LDAPException("Not connected!")
dn = 'uid=' + uid + ',' + self.user_base
attrs = {
'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
'uid': [ uid ],
'cn': [ cn ],
'loginShell': [ loginShell ],
'uidNumber': [ str(uidNumber) ],
'gidNumber': [ str(gidNumber) ],
'homeDirectory': [ homeDirectory ],
'gecos': [ gecos ],
}
if loginShell:
attrs['loginShell'] = loginShell
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)
return results
def user_modify(self, uid, attrs):
@ -281,8 +197,6 @@ class LDAPConnection(object):
connection.user_modify('mspang', user)
"""
if not self.connected(): raise LDAPException("Not connected!")
# distinguished name of the entry to modify
dn = 'uid=' + uid + ',' + self.user_base
@ -308,8 +222,6 @@ class LDAPConnection(object):
Example: connection.user_delete('mspang')
"""
if not self.connected(): raise LDAPException("Not connected!")
try:
dn = 'uid=' + uid + ',' + self.user_base
self.ldap.delete_s(dn)
@ -318,6 +230,116 @@ class LDAPConnection(object):
### Account-related Methods ###
def account_lookup(self, uid):
"""
Retrieve the attributes of an account.
Parameters:
uid - the uid to look up
Returns: attributes of user with uid
"""
return self.user_lookup(uid, 'posixAccount')
def account_search_id(self, uidNumber):
"""
Retrieves a list of accounts with a certain UNIX uid number.
LDAP (or passwd for that matter) does not enforce any restriction on
the number of accounts that can have a certain UID number. Therefore
this method returns a list of matches.
Parameters:
uidNumber - the user id of the accounts desired
Returns: a dictionary mapping uids to attributes
Example: connection.account_search_id(21292) -> {'mspang': { ... }}
"""
search_filter = '(&(objectClass=posixAccount)(uidNumber=%s))'
return self.user_search(search_filter, [ uidNumber ])
def account_search_gid(self, gidNumber):
"""
Retrieves a list of accounts with a certain UNIX gid
number (search by default group).
Returns: a dictionary mapping uids to attributes
"""
search_filter = '(&(objectClass=posixAccount)(gidNumber=%s))'
return self.user_search(search_filter, [ gidNumber ])
def account_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None, update=False):
"""
Adds a user account to the directory.
Parameters:
uid - the UNIX username for the account
cn - the real name of the member
uidNumber - the UNIX user id number
gidNumber - the UNIX group id number (default group)
homeDirectory - home directory for the user
loginShell - login shell for the user
gecos - comment field (usually stores name etc)
description - description field (optional and unimportant)
update - if True, will update existing entries
Example: connection.user_add('mspang', 'Michael Spang',
21292, 100, '/users/mspang', '/bin/bash',
'Michael Spang,,,')
"""
if not self.connected(): raise LDAPException("Not connected!")
dn = 'uid=' + uid + ',' + self.user_base
attrs = {
'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
'uid': [ uid ],
'cn': [ cn ],
'loginShell': [ loginShell ],
'uidNumber': [ str(uidNumber) ],
'gidNumber': [ str(gidNumber) ],
'homeDirectory': [ homeDirectory ],
'gecos': [ gecos ],
}
if loginShell:
attrs['loginShell'] = [ loginShell ]
if description:
attrs['description'] = [ description ]
try:
old_entry = self.user_lookup(uid)
if old_entry and 'posixAccount' not in old_entry['objectClass'] and update:
attrs.update(old_entry)
attrs['objectClass'] = list(attrs['objectClass'])
attrs['objectClass'].append('posixAccount')
if not 'shadowAccount' in attrs['objectClass']:
attrs['objectClass'].append('shadowAccount')
modlist = ldap.modlist.modifyModlist(old_entry, attrs)
self.ldap.modify_s(dn, modlist)
else:
modlist = ldap.modlist.addModlist(attrs)
self.ldap.add_s(dn, modlist)
except ldap.LDAPError, e:
raise LDAPException("unable to add: %s" % e)
### Group-related Methods ###
def group_lookup(self, cn):
@ -355,27 +377,19 @@ class LDAPConnection(object):
try:
search_filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, search_filter)
except ldap.LDAPError,e :
except ldap.LDAPError, e:
raise LDAPException("group search failed: %s" % e)
# list for groups found
group_cns = []
results = {}
for match in matches:
dn, attributes = match
dn, attrs = match
uid = attrs['cn'][0]
results[uid] = attrs
# cn is a required attribute of posixGroup
if not attributes.has_key('cn'):
raise LDAPException(dn + ' (posixGroup) has no cn')
# do not handle the case of multiple cns for one group (yet)
elif len(attributes['cn']) > 1:
raise LDAPException(dn + ' (posixGroup) has multiple cns')
# append the sole uid of this match to the list
group_cns.append( attributes['cn'][0] )
return group_cns
return results
def group_add(self, cn, gidNumber, description=None):
@ -457,8 +471,129 @@ class LDAPConnection(object):
raise LDAPException("unable to delete group: %s" % e)
### 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_studentid(self, studentid):
"""
Retrieves a list of members with a certain studentid.
Returns: a dictionary mapping uids to attributes
"""
search_filter = '(&(objectClass=member)(studentid=%s))'
return self.user_search(search_filter, [ studentid ] )
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, studentid, 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
studentid - the member's student ID number
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 ],
'studentid': [ studentid ],
}
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)
def member_add_account(self, uid, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None):
"""
Adds login privileges to a member.
"""
return self.account_add(uid, None, uidNumber, gidNumber, homeDirectory, loginShell, gecos, None, True)
### 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 used_uids(self, minimum=None, maximum=None):
"""
Compiles a list of used UIDs in a range.
@ -543,9 +678,17 @@ if __name__ == '__main__':
tushell = '/bin/false'
tugecos = 'Test User,,,'
tgname = 'testgroup'
tmname = 'testmember'
tmrname = 'Test Member'
tmstudentid = '99999999'
tmprogram = 'UBW'
tmdesc = 'Test Description'
cushell = '/bin/true'
cuhome = '/home/changed'
curname = 'Test Modified User'
cmhome = '/home/testmember'
cmshell = '/bin/false'
cmgecos = 'Test Member,,,'
test(LDAPConnection)
connection = LDAPConnection()
@ -563,6 +706,7 @@ if __name__ == '__main__':
try:
connection.user_delete(tuname)
connection.user_delete(tmname)
connection.group_delete(tgname)
except LDAPException:
pass
@ -596,8 +740,20 @@ if __name__ == '__main__':
'cn': [ turname ]
}
test(LDAPConnection.user_add)
connection.user_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
test(LDAPConnection.account_add)
connection.account_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
success()
emdata = {
'uid': [ tmname ],
'cn': [ tmrname ],
'studentid': [ tmstudentid ],
'program': [ tmprogram ],
'description': [ tmdesc ],
}
test(LDAPConnection.member_add)
connection.member_add(tmname, tmrname, tmstudentid, tmprogram, tmdesc)
success()
tggid = unusedids.pop()
@ -610,41 +766,95 @@ if __name__ == '__main__':
connection.group_add(tgname, tggid)
success()
test(LDAPConnection.account_lookup)
udata = connection.account_lookup(tuname)
if udata: del udata['objectClass']
assert_equal(eudata, udata)
success()
test(LDAPConnection.member_lookup)
mdata = connection.member_lookup(tmname)
if mdata: del mdata['objectClass']
assert_equal(emdata, mdata)
success()
test(LDAPConnection.user_lookup)
udata = connection.user_lookup(tuname)
del udata['objectClass']
mdata = connection.user_lookup(tmname)
if udata: del udata['objectClass']
if mdata: del mdata['objectClass']
assert_equal(eudata, udata)
assert_equal(emdata, mdata)
success()
test(LDAPConnection.group_lookup)
gdata = connection.group_lookup(tgname)
del gdata['objectClass']
if gdata: del gdata['objectClass']
assert_equal(egdata, gdata)
success()
test(LDAPConnection.user_search_id)
test(LDAPConnection.account_search_id)
eulist = [ tuname ]
ulist = connection.user_search_id(tuuid)
ulist = connection.account_search_id(tuuid).keys()
assert_equal(eulist, ulist)
success()
test(LDAPConnection.user_search_gid)
ulist = connection.user_search_gid(tugid)
test(LDAPConnection.account_search_gid)
ulist = connection.account_search_gid(tugid)
if tuname not in ulist:
fail("(%s) not in (%s)" % (tuname, ulist))
fail("%s not in %s" % (tuname, ulist))
success()
ecudata = connection.user_lookup(tuname)
test(LDAPConnection.member_search_studentid)
mlist = connection.member_search_studentid(tmstudentid).keys()
emlist = [ tmname ]
assert_equal(emlist, mlist)
success()
test(LDAPConnection.member_search_name)
mlist = connection.member_search_name(tmrname)
if tmname not in mlist:
fail("%s not in %s" % (tmname, mlist))
success()
test(LDAPConnection.member_search_program)
mlist = connection.member_search_program(tmprogram)
if tmname not in mlist:
fail("%s not in %s" % (tmname, mlist))
success()
test(LDAPConnection.group_search_id)
glist = connection.group_search_id(tggid).keys()
eglist = [ tgname ]
assert_equal(eglist, glist)
success()
ecudata = connection.account_lookup(tuname)
ecudata['loginShell'] = [ cushell ]
ecudata['homeDirectory'] = [ cuhome ]
ecudata['cn'] = [ curname ]
test(LDAPConnection.user_modify)
connection.user_modify(tuname, ecudata)
cudata = connection.user_lookup(tuname)
cudata = connection.account_lookup(tuname)
assert_equal(ecudata, cudata)
success()
tmuid = unusedids.pop()
tmgid = unusedids.pop()
emadata = emdata.copy()
emadata.update({
'loginShell': [ cmshell ],
'uidNumber': [ str(tmuid) ],
'gidNumber': [ str(tmgid) ],
'gecos': [ cmgecos ],
'homeDirectory': [ cmhome ],
})
test(LDAPConnection.member_add_account)
connection.member_add_account(tmname, tmuid, tmuid, cmhome, cmshell, cmgecos)
success()
ecgdata = connection.group_lookup(tgname)
ecgdata['memberUid'] = [ tuname ]