diff --git a/pylib/csc/adm/accounts.py b/pylib/csc/adm/accounts.py index 288fc7c..26e5fff 100644 --- a/pylib/csc/adm/accounts.py +++ b/pylib/csc/adm/accounts.py @@ -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,9 +793,9 @@ 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 if ldap_connection.group_lookup(name): raise NameConflict(name, "group", "LDAP") @@ -815,10 +822,10 @@ def check_name_usage(name): def check_account_status(username, require_ldap=True, require_krb=False): """Helper function to verify that an account exists.""" - - if not connected(): + + 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") diff --git a/pylib/csc/adm/members.py b/pylib/csc/adm/members.py index 63998fb..a9c3f62 100644 --- a/pylib/csc/adm/members.py +++ b/pylib/csc/adm/members.py @@ -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,10 +259,10 @@ 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() - + def list_name(name): """ @@ -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) @@ -346,12 +373,12 @@ def update(member): # see if member exists if not get(memberid): raise NoSuchMember(memberid) - + # do the update - connection.update_member(member) + db_connection.update_member(member) # commit the transaction - connection.commit() + db_connection.commit() @@ -376,16 +403,31 @@ 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 if not re.match('^[wsf][0-9]{4}$', term): raise InvalidTerm(term) - - # add term to database - connection.insert_term(memberid, term) - connection.commit() + # add term to database + db_connection.insert_term(memberid, term) + + # 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) diff --git a/pylib/csc/apps/legacy/main.py b/pylib/csc/apps/legacy/main.py index e9a463e..64b2d50 100644 --- a/pylib/csc/apps/legacy/main.py +++ b/pylib/csc/apps/legacy/main.py @@ -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 == '': diff --git a/pylib/csc/backends/ldapi.py b/pylib/csc/backends/ldapi.py index c949e68..c246667 100644 --- a/pylib/csc/backends/ldapi.py +++ b/pylib/csc/backends/ldapi.py @@ -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: @@ -122,109 +126,160 @@ class LDAPConnection(object): # 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 - 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 - - # 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] ) + dn, attrs = match + uid = attrs['uid'][0] + results[uid] = attrs - return uids + return results - def user_search_id(self, uidNumber): + def user_modify(self, uid, attrs): """ - Retrieves a list of users with a certain UNIX uid number. + Update user attributes in the directory. - 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: + 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) + + + def user_delete(self, uid): + """ + Removes a user from the directory. + + Example: connection.user_delete('mspang') + """ + + try: + dn = 'uid=' + uid + ',' + self.user_base + self.ldap.delete_s(dn) + except ldap.LDAPError, e: + raise LDAPException("unable to delete: %s" % e) + + + + ### 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: the list of uids matched (usernames) + Returns: a dictionary mapping uids to attributes - Example: connection.user_search_id(21292) -> ['mspang'] + Example: connection.account_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) + search_filter = '(&(objectClass=posixAccount)(uidNumber=%s))' + return self.user_search(search_filter, [ uidNumber ]) - def user_search_gid(self, gidNumber): + def account_search_gid(self, gidNumber): """ - Retrieves a list of users with a certain UNIX gid + Retrieves a list of accounts with a certain UNIX gid number (search by default group). - Returns: the list of uids matched (usernames) + Returns: a dictionary mapping uids to attributes """ - # search for posixAccount entries with the specified gidNumber - search_filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber - return self.user_search(search_filter) + search_filter = '(&(objectClass=posixAccount)(gidNumber=%s))' + return self.user_search(search_filter, [ gidNumber ]) - def user_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None): + def account_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None, update=False): """ - Adds a user to the directory. + Adds a user account to the directory. Parameters: uid - the UNIX username for the account @@ -235,6 +290,7 @@ class LDAPConnection(object): 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', @@ -254,69 +310,35 @@ class LDAPConnection(object): 'homeDirectory': [ homeDirectory ], 'gecos': [ gecos ], } - + if loginShell: - attrs['loginShell'] = loginShell + attrs['loginShell'] = [ loginShell ] if description: attrs['description'] = [ description ] try: - modlist = ldap.modlist.addModlist(attrs) - self.ldap.add_s(dn, modlist) + + 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) - 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) - """ - - if not self.connected(): raise LDAPException("Not connected!") - - # 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) - - - def user_delete(self, uid): - """ - Removes a user from the directory. - - 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) - except ldap.LDAPError, e: - raise LDAPException("unable to delete: %s" % e) - - ### Group-related Methods ### @@ -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 ]