pyceo/ceo/members.py

610 lines
16 KiB
Python
Raw Permalink Normal View History

2007-01-27 18:41:51 -05:00
"""
CSC Member Management
This module contains functions for registering new members, registering
members for terms, searching for members, and other member-related
functions.
Transactions are used in each method that modifies the database.
2007-01-27 18:41:51 -05:00
Future changes to the members database that need to be atomic
must also be moved into this module.
"""
import os, re, subprocess, ldap, socket
2009-08-06 01:39:33 -04:00
from ceo import conf, ldapi, terms, remote, ceo_pb2
from ceo.excep import InvalidArgument
import dns.resolver
2007-01-27 18:41:51 -05:00
### Configuration ###
2007-01-27 18:41:51 -05:00
CONFIG_FILE = '/etc/csc/accounts.cf'
2007-01-27 18:41:51 -05:00
cfg = {}
def configure():
2007-01-27 18:41:51 -05:00
"""Load Members Configuration"""
2009-07-29 13:03:32 -04:00
string_fields = [ 'username_regex', 'shells_file', 'ldap_server_url',
'ldap_users_base', 'ldap_groups_base', 'ldap_sasl_mech', 'ldap_sasl_realm',
'expire_hook' ]
numeric_fields = [ 'min_password_length' ]
2007-01-27 18:41:51 -05:00
# read configuration file
cfg_tmp = conf.read(CONFIG_FILE)
2007-01-27 18:41:51 -05:00
# verify configuration
conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
2007-01-27 18:41:51 -05:00
# update the current configuration with the loaded values
cfg.update(cfg_tmp)
### Exceptions ###
class MemberException(Exception):
"""Base exception class for member-related errors."""
def __init__(self, ex=None):
2007-12-13 23:53:15 -05:00
Exception.__init__(self)
self.ex = ex
def __str__(self):
return str(self.ex)
2007-01-27 18:41:51 -05:00
class InvalidTerm(MemberException):
"""Exception class for malformed terms."""
def __init__(self, term):
2007-12-13 23:06:55 -05:00
MemberException.__init__(self)
self.term = term
def __str__(self):
return "Term is invalid: %s" % self.term
2007-01-27 18:41:51 -05:00
class NoSuchMember(MemberException):
"""Exception class for nonexistent members."""
def __init__(self, memberid):
2007-12-13 23:06:55 -05:00
MemberException.__init__(self)
self.memberid = memberid
def __str__(self):
return "Member not found: %d" % self.memberid
2007-01-27 18:41:51 -05:00
### Connection Management ###
# global directory connection
ld = None
2007-01-27 18:41:51 -05:00
2007-12-16 18:06:09 -05:00
def connect(auth_callback):
2007-10-26 00:05:05 -04:00
"""Connect to LDAP."""
global ld
2007-12-16 18:06:09 -05:00
password = None
2007-12-16 18:34:42 -05:00
tries = 0
2007-12-16 18:06:09 -05:00
while ld is None:
try:
2009-07-29 13:03:32 -04:00
ld = ldapi.connect_sasl(cfg['ldap_server_url'], cfg['ldap_sasl_mech'],
cfg['ldap_sasl_realm'], password)
2007-12-16 18:06:09 -05:00
except ldap.LOCAL_ERROR, e:
2007-12-16 18:34:42 -05:00
tries += 1
if tries > 3:
raise e
2007-12-16 18:06:09 -05:00
password = auth_callback.callback(e)
if password == None:
raise e
2009-09-09 17:37:35 -04:00
def connect_anonymous():
"""Connect to LDAP."""
global ld
ld = ldap.initialize(cfg['ldap_server_url'])
2007-01-27 18:41:51 -05:00
def disconnect():
2007-10-26 00:05:05 -04:00
"""Disconnect from LDAP."""
global ld
ld.unbind_s()
ld = None
2007-01-27 18:41:51 -05:00
def connected():
"""Determine whether the connection has been established."""
2007-01-27 18:41:51 -05:00
return ld and ld.connected()
2007-01-27 18:41:51 -05:00
2007-12-12 00:39:44 -05:00
### Members ###
2007-01-27 18:41:51 -05:00
def create_member(username, password, name, program, email, club_rep=False):
"""
Creates a UNIX user account with options tailored to CSC members.
Parameters:
username - the desired UNIX username
password - the desired UNIX password
name - the member's real name
program - the member's program of study
club_rep - whether the user is a club rep
email - email to place in .forward
Exceptions:
InvalidArgument - on bad account attributes provided
Returns: the uid number of the new account
See: create()
"""
# check username format
if not username or not re.match(cfg['username_regex'], username):
raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
# check password length
if not password or len(password) < cfg['min_password_length']:
raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
2007-12-13 23:53:15 -05:00
try:
2009-08-06 01:39:33 -04:00
request = ceo_pb2.AddUser()
request.username = username
request.password = password
request.realname = name
request.program = program
request.email = email
if club_rep:
request.type = ceo_pb2.AddUser.CLUB_REP
else:
request.type = ceo_pb2.AddUser.MEMBER
2009-08-06 01:39:33 -04:00
out = remote.run_remote('adduser', request.SerializeToString())
response = ceo_pb2.AddUserResponse()
response.ParseFromString(out)
if any(message.status != 0 for message in response.messages):
raise MemberException('\n'.join(message.message for message in response.messages))
2009-08-06 01:39:33 -04:00
except remote.RemoteException, e:
raise MemberException(e)
except OSError, e:
raise MemberException(e)
def check_email(email):
match = re.match('^\S+?@(\S+)$', email)
if not match:
return 'Invalid email address'
# some characters are treated specially in .forward
for c in email:
if c in ('"', "'", ',', '|', '$', '/', '#', ':'):
return 'Invalid character in address: %s' % c
# Start by searching for host record
host = match.group(1)
try:
ip = socket.getaddrinfo(host, None)
except:
# Check for MX record
try:
dns.resolver.query(host, 'MX')
except:
return 'Invalid host: %s' % host
def current_email(username):
fwdpath = '%s/%s/.forward' % (cfg['member_home'], username)
try:
fwd = open(fwdpath).read().strip()
if not check_email(fwd):
return fwd
except OSError:
pass
except IOError:
pass
def change_email(username, forward):
try:
request = ceo_pb2.UpdateMail()
request.username = username
request.forward = forward
out = remote.run_remote('mail', request.SerializeToString())
response = ceo_pb2.AddUserResponse()
response.ParseFromString(out)
if any(message.status != 0 for message in response.messages):
return '\n'.join(message.message for message in response.messages)
except remote.RemoteException, e:
2009-08-06 01:39:33 -04:00
raise MemberException(e)
2007-12-13 23:53:15 -05:00
except OSError, e:
raise MemberException(e)
def get(userid):
2007-01-27 18:41:51 -05:00
"""
Look up attributes of a member by userid.
Returns: a dictionary of attributes
Example: get('mspang') -> {
'cn': [ 'Michael Spang' ],
'program': [ 'Computer Science' ],
2007-01-27 18:41:51 -05:00
...
}
"""
2009-07-29 13:03:32 -04:00
return ldapi.lookup(ld, 'uid', userid, cfg['ldap_users_base'])
2007-01-27 18:41:51 -05:00
def get_group(group):
"""
Look up group by groupname
Returns a dictionary of group attributes
"""
return ldapi.lookup(ld, 'cn', group, cfg['ldap_groups_base'])
def uid2dn(uid):
2009-07-29 13:03:32 -04:00
return 'uid=%s,%s' % (ldapi.escape(uid), cfg['ldap_users_base'])
2007-01-27 18:41:51 -05:00
def list_term(term):
"""
Build a list of members in a term.
Parameters:
term - the term to match members against
Returns: a list of members
2007-01-27 18:41:51 -05:00
Example: list_term('f2006'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
'uid=ctdalek, ou=...': { 'cn': 'Calum T. Dalek', ... },
2007-01-27 18:41:51 -05:00
...
}
2007-01-27 18:41:51 -05:00
"""
2009-07-29 13:03:32 -04:00
members = ldapi.search(ld, cfg['ldap_users_base'],
'(&(objectClass=member)(term=%s))', [ term ])
return dict([(member[0], member[1]) for member in members])
2007-01-27 18:41:51 -05:00
def list_name(name):
"""
Build a list of members with matching names.
Parameters:
name - the name to match members against
Returns: a list of member dictionaries
2007-01-27 18:41:51 -05:00
Example: list_name('Spang'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
2007-01-27 18:41:51 -05:00
...
]
"""
2009-07-29 13:03:32 -04:00
members = ldapi.search(ld, cfg['ldap_users_base'],
'(&(objectClass=member)(cn~=%s))', [ name ])
return dict([(member[0], member[1]) for member in members])
2007-02-02 20:29:55 -05:00
def list_group(group):
"""
Build a list of members in a group.
Parameters:
group - the group to match members against
Returns: a list of member dictionaries
Example: list_name('syscom'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
...
]
"""
members = group_members(group)
2007-12-03 03:18:03 -05:00
ret = {}
if members:
for member in members:
info = get(member)
if info:
ret[uid2dn(member)] = info
2007-12-03 03:18:03 -05:00
return ret
def list_all():
"""
Build a list of all members
Returns: a list of member dictionaries
Example: list_name('Spang'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
...
]
"""
2009-07-29 13:03:32 -04:00
members = ldapi.search(ld, cfg['ldap_users_base'], '(objectClass=member)')
return dict([(member[0], member[1]) for member in members])
2007-11-15 05:28:58 -05:00
def list_positions():
"""
Build a list of positions
Returns: a list of positions and who holds them
Example: list_positions(): -> {
'president': { 'mspang': { 'cn': 'Michael Spang', ... } } ],
...
]
"""
2009-07-29 13:03:32 -04:00
members = ld.search_s(cfg['ldap_users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
2007-11-15 05:28:58 -05:00
positions = {}
for (_, member) in members:
for position in member['position']:
if not position in positions:
positions[position] = {}
positions[position][member['uid'][0]] = member
return positions
2007-11-15 05:28:58 -05:00
def set_position(position, members):
"""
Sets a position
Parameters:
position - the position to set
members - an array of members that hold the position
Example: set_position('president', ['dtbartle'])
"""
2009-07-29 13:03:32 -04:00
res = ld.search_s(cfg['ldap_users_base'], ldap.SCOPE_SUBTREE,
'(&(objectClass=member)(position=%s))' % ldapi.escape(position))
2007-11-15 05:28:58 -05:00
old = set([ member['uid'][0] for (_, member) in res ])
new = set(members)
mods = {
'del': set(old) - set(new),
'add': set(new) - set(old),
}
if len(mods['del']) == 0 and len(mods['add']) == 0:
return
2007-12-12 01:15:12 -05:00
for action in ['del', 'add']:
for userid in mods[action]:
2009-07-29 13:03:32 -04:00
dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
2007-11-15 05:28:58 -05:00
entry1 = {'position' : [position]}
entry2 = {} #{'position' : []}
entry = ()
2007-12-12 01:15:12 -05:00
if action == 'del':
2007-11-15 05:28:58 -05:00
entry = (entry1, entry2)
2007-12-12 01:15:12 -05:00
elif action == 'add':
2007-11-15 05:28:58 -05:00
entry = (entry2, entry1)
mlist = ldapi.make_modlist(entry[0], entry[1])
ld.modify_s(dn, mlist)
2007-11-15 05:28:58 -05:00
2007-01-27 18:41:51 -05:00
2007-11-15 05:28:58 -05:00
def change_group_member(action, group, userid):
2009-07-29 13:03:32 -04:00
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['ldap_groups_base'])
2007-11-15 05:28:58 -05:00
entry1 = {'uniqueMember' : []}
entry2 = {'uniqueMember' : [user_dn]}
entry = []
if action == 'add' or action == 'insert':
entry = (entry1, entry2)
elif action == 'remove' or action == 'delete':
entry = (entry2, entry1)
else:
raise InvalidArgument("action", action, "invalid action")
mlist = ldapi.make_modlist(entry[0], entry[1])
ld.modify_s(group_dn, mlist)
2007-11-15 05:28:58 -05:00
2007-01-27 18:41:51 -05:00
### Shells ###
def get_shell(userid):
2009-07-29 13:03:32 -04:00
member = ldapi.lookup(ld, 'uid', userid, cfg['ldap_users_base'])
if not member:
raise NoSuchMember(userid)
if 'loginShell' not in member:
return
return member['loginShell'][0]
def get_shells():
return [ sh for sh in open(cfg['shells_file']).read().split("\n")
if sh
and sh[0] == '/'
and not '#' in sh
and os.access(sh, os.X_OK) ]
def set_shell(userid, shell):
if not shell in get_shells():
raise InvalidArgument("shell", shell, "is not in %s" % cfg['shells_file'])
2009-07-29 13:03:32 -04:00
ldapi.modify(ld, 'uid', userid, cfg['ldap_users_base'], [ (ldap.MOD_REPLACE, 'loginShell', [ shell ]) ])
### Clubs ###
def create_club(username, name):
"""
Creates a UNIX user account with options tailored to CSC-hosted clubs.
Parameters:
username - the desired UNIX username
name - the club name
Exceptions:
InvalidArgument - on bad account attributes provided
Returns: the uid number of the new account
See: create()
"""
# check username format
if not username or not re.match(cfg['username_regex'], username):
raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
2007-12-13 23:53:15 -05:00
try:
2009-08-06 01:39:33 -04:00
request = ceo_pb2.AddUser()
request.type = ceo_pb2.AddUser.CLUB
request.username = username
request.realname = name
2009-08-06 01:39:33 -04:00
out = remote.run_remote('adduser', request.SerializeToString())
response = ceo_pb2.AddUserResponse()
response.ParseFromString(out)
if any(message.status != 0 for message in response.messages):
raise MemberException('\n'.join(message.message for message in response.messages))
except remote.RemoteException, e:
raise MemberException(e)
2007-12-13 23:53:15 -05:00
except OSError, e:
raise MemberException(e)
2007-12-12 00:39:44 -05:00
### Terms ###
2007-01-27 18:41:51 -05:00
def register(userid, term_list):
2007-01-27 18:41:51 -05:00
"""
Registers a member for one or more terms.
Parameters:
userid - the member's username
2007-01-27 18:41:51 -05:00
term_list - the term to register for, or a list of terms
Exceptions:
InvalidTerm - if a term is malformed
Example: register(3349, "w2007")
Example: register(3349, ["w2007", "s2007"])
"""
2009-07-29 13:03:32 -04:00
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
if type(term_list) in (str, unicode):
2007-01-27 18:41:51 -05:00
term_list = [ term_list ]
ldap_member = get(userid)
if ldap_member and 'term' not in ldap_member:
ldap_member['term'] = []
if not ldap_member:
raise NoSuchMember(userid)
new_member = ldap_member.copy()
new_member['term'] = new_member['term'][:]
2007-01-27 18:41:51 -05:00
for term in term_list:
2007-01-27 18:41:51 -05:00
# check term syntax
if not re.match('^[wsf][0-9]{4}$', term):
raise InvalidTerm(term)
# add the term to the entry
if not term in ldap_member['term']:
new_member['term'].append(term)
mlist = ldapi.make_modlist(ldap_member, new_member)
ld.modify_s(user_dn, mlist)
2007-01-27 18:41:51 -05:00
def register_nonmember(userid, term_list):
"""Registers a non-member for one or more terms."""
2009-07-29 13:03:32 -04:00
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
if type(term_list) in (str, unicode):
term_list = [ term_list ]
ldap_member = get(userid)
if not ldap_member:
raise NoSuchMember(userid)
if 'term' not in ldap_member:
ldap_member['term'] = []
if 'nonMemberTerm' not in ldap_member:
ldap_member['nonMemberTerm'] = []
new_member = ldap_member.copy()
new_member['nonMemberTerm'] = new_member['nonMemberTerm'][:]
for term in term_list:
# check term syntax
if not re.match('^[wsf][0-9]{4}$', term):
raise InvalidTerm(term)
# add the term to the entry
if not term in ldap_member['nonMemberTerm'] \
and not term in ldap_member['term']:
new_member['nonMemberTerm'].append(term)
mlist = ldapi.make_modlist(ldap_member, new_member)
ld.modify_s(user_dn, mlist)
def registered(userid, term):
2007-01-27 18:41:51 -05:00
"""
Determines whether a member is registered
for a term.
Parameters:
userid - the member's username
2007-01-27 18:41:51 -05:00
term - the term to check
Returns: whether the member is registered
Example: registered("mspang", "f2006") -> True
2007-01-27 18:41:51 -05:00
"""
member = get(userid)
2009-01-15 18:34:23 -05:00
if not member is None:
return 'term' in member and term in member['term']
else:
return False
2007-01-27 18:41:51 -05:00
def group_members(group):
"""
Returns a list of group members
"""
2009-07-29 13:03:32 -04:00
group = ldapi.lookup(ld, 'cn', group, cfg['ldap_groups_base'])
if group and 'uniqueMember' in group:
r = re.compile('^uid=([^,]*)')
return map(lambda x: r.match(x).group(1), group['uniqueMember'])
return []
2008-01-23 02:11:43 -05:00
def expired_accounts():
2009-07-29 13:03:32 -04:00
members = ldapi.search(ld, cfg['ldap_users_base'],
2008-01-23 02:11:43 -05:00
'(&(objectClass=member)(!(|(term=%s)(nonMemberTerm=%s))))' %
(terms.current(), terms.current()))
return dict([(member[0], member[1]) for member in members])
def send_account_expired_email(name, email):
2009-07-29 13:03:32 -04:00
args = [ cfg['expire_hook'], name, email ]
os.spawnv(os.P_WAIT, cfg['expire_hook'], args)
2010-09-25 01:00:27 -04:00
def subscribe_to_mailing_list(name):
member = get(name)
if member is not None:
return remote.run_remote('mailman', name)
else:
return 'Error: member does not exist'