add base classes for users and groups

pull/5/head
Max Erenberg 1 year ago
parent 0c6dc18085
commit de0f473881
  1. 1
      ceo/.gitignore
  2. 1
      ceo/__init__.py
  3. 162
      ceo/conf.py
  4. 1
      ceo/console/__init__.py
  5. 40
      ceo/console/expiredaccounts.py
  6. 27
      ceo/console/inactive.py
  7. 49
      ceo/console/main.py
  8. 24
      ceo/console/memberlist.py
  9. 38
      ceo/console/mysql.py
  10. 49
      ceo/console/updateprograms.py
  11. 13
      ceo/excep.py
  12. 148
      ceo/ldapi.py
  13. 609
      ceo/members.py
  14. 54
      ceo/mysql.py
  15. 24
      ceo/ops.py
  16. 155
      ceo/pymazon.py
  17. 18
      ceo/remote.py
  18. 254
      ceo/terms.py
  19. 42
      ceo/test.py
  20. 1
      ceo/urwid/__init__.py
  21. 84
      ceo/urwid/databases.py
  22. 135
      ceo/urwid/groups.py
  23. 47
      ceo/urwid/info.py
  24. 8
      ceo/urwid/library.py
  25. 192
      ceo/urwid/main.py
  26. 267
      ceo/urwid/newmember.py
  27. 97
      ceo/urwid/positions.py
  28. 240
      ceo/urwid/renew.py
  29. 83
      ceo/urwid/search.py
  30. 95
      ceo/urwid/shell.py
  31. 247
      ceo/urwid/widgets.py
  32. 80
      ceo/urwid/window.py
  33. 8
      ceo/uwldap.py
  34. 0
      ceo_common/__init__.py
  35. 8
      ceo_common/interfaces/IConfig.py
  36. 39
      ceo_common/interfaces/IGroup.py
  37. 14
      ceo_common/interfaces/IKerberosService.py
  38. 32
      ceo_common/interfaces/ILDAPService.py
  39. 65
      ceo_common/interfaces/IUser.py
  40. 5
      ceo_common/interfaces/__init__.py
  41. 24
      ceo_common/model/Config.py
  42. 1
      ceo_common/model/__init__.py
  43. 0
      ceod/__init__.py
  44. 84
      ceod/model/Group.py
  45. 44
      ceod/model/KerberosService.py
  46. 139
      ceod/model/LDAPService.py
  47. 169
      ceod/model/User.py
  48. 4
      ceod/model/__init__.py
  49. 34
      ceod/model/utils.py

1
ceo/.gitignore vendored

@ -1 +0,0 @@
/ceo_pb2.py

@ -1 +0,0 @@
"""CSC Electronic Office"""

@ -1,162 +0,0 @@
"""
Configuration Utility Module
This module contains functions to load and verify very simple configuration
files. Python supports ".ini" files, which suck, so this module is used
instead.
Example Configuration File:
include /path/to/other.cf
# these values are the same:
name_protected = "Michael Spang"
name_unprotected = Michael Spang
# these values are not the same:
yes_no = " yes"
no_yes = yes
# this value is an integer
arbitrary_number=2
# this value is not an integer
arbitrary_string="2"
# this is a key with no value
csclub
# this key contains whitespace
white space = sure, why not
# these two lines are treated as one
long line = first line \\
second line
Resultant Dictionary:
{
'name_protected': 'Michael Spang',
'name_unprotected:' 'Michael Spang',
'yes_no': ' yes',
'no_yes': 'yes',
'arbirary_number': 2,
'arbitrary_string': '2',
'csclub': None,
'white space': 'sure, why not'
'long line': 'first line \\n second line'
... (data from other.cf) ...
}
"""
from curses.ascii import isspace
class ConfigurationException(Exception):
"""Exception class for incomplete and incorrect configurations."""
def read(filename, included=None):
"""
Function to read a configuration file into a dictionary.
Parmaeters:
filename - the file to read
included - files previously read (internal)
Exceptions:
IOError - when the configuration file cannot be read
"""
if not included:
included = []
if filename in included:
return {}
included.append(filename)
conffile = open(filename)
options = {}
while True:
line = conffile.readline()
if line == '':
break
# remove comments
if '#' in line:
line = line[:line.find('#')]
# combine lines when the newline is escaped with \
while len(line) > 1 and line[-2] == '\\':
line = line[:-2] + line[-1]
next = conffile.readline()
line += next
if next == '':
break
line = line.strip()
# process include statements
if line.find("include") == 0 and isspace(line[7]):
filename = line[8:].strip()
options.update(read(filename, included))
continue
# split 'key = value' into key and value and strip results
pair = map(str.strip, line.split('=', 1))
# found key and value
if len(pair) == 2:
key, val = pair
# found quoted string?
if val and val[0] == val[-1] == '"':
val = val[1:-1]
# unquoted, found num?
elif val:
try:
if "." in val:
val = float(val)
elif val[0] == '0':
val = int(val, 8)
else:
val = int(val)
except ValueError:
pass
# save key and value
options[key] = val
# found only key, value = None
elif len(pair[0]) > 1:
key = pair[0]
options[key] = None
return options
def check_string_fields(filename, field_list, cfg):
"""Function to verify thatfields are strings."""
for field in field_list:
if field not in cfg or type(cfg[field]) is not str:
raise ConfigurationException('expected string value for option "%s" in "%s"' % (field, filename))
def check_integer_fields(filename, field_list, cfg):
"""Function to verify that fields are integers."""
for field in field_list:
if field not in cfg or type(cfg[field]) not in (int, long):
raise ConfigurationException('expected numeric value for option "%s" in "%s"' % (field, filename))
def check_float_fields(filename, field_list, cfg):
"""Function to verify that fields are integers or floats."""
for field in field_list:
if field not in cfg or type(cfg[field]) not in (float, long, int):
raise ConfigurationException('expected float value for option "%s" in "%s"' % (field, filename))

@ -1 +0,0 @@
"""Console Interface"""

@ -1,40 +0,0 @@
import sys, ldap
from ceo import members, uwldap, terms, ldapi
def max_term(term1, term2):
if terms.compare(term1, term2) > 0:
return term1
else:
return term2
class ExpiredAccounts:
help = '''
expiredaccounts [--email]
Displays a list of expired accounts. If --email is specified, expired account
owners will be emailed.
'''
def main(self, args):
send_email = False
if len(args) == 1 and args[0] == '--email':
sys.stderr.write("If you want to send an account expiration notice to " \
"these users then type 'Yes, do this' and hit enter\n")
if raw_input() == 'Yes, do this':
send_email = True
uwl = ldap.initialize(uwldap.uri())
mlist = members.expired_accounts()
for member in mlist.values():
term = "f0000"
term = reduce(max_term, member.get("term", []), term)
term = reduce(max_term, member.get("nonMemberTerm", []), term)
expiredfor = terms.delta(term, terms.current())
if expiredfor <= 3:
uid = member['uid'][0]
name = member['cn'][0]
email = None
print '%s (expired for %d terms)' % (uid.ljust(12), expiredfor)
if send_email:
print " sending mail to %s" % uid
members.send_account_expired_email(name, uid)

@ -1,27 +0,0 @@
from ceo import members, terms
def max_term(term1, term2):
if terms.compare(term1, term2) > 0:
return term1
else:
return term2
class Inactive:
help = '''
inactive delta-terms
Prints a list of accounts that have been inactive (i.e. unpaid) for
delta-terms.
'''
def main(self, args):
if len(args) != 1:
print self.help
return
delta = int(args[0])
mlist = members.list_all()
for member in mlist.values():
term = "f0000"
term = reduce(max_term, member.get("term", []), term)
term = reduce(max_term, member.get("nonMemberTerm", []), term)
if terms.delta(term, terms.current()) >= delta:
print "%s %s" % (member['uid'][0].ljust(12), term)

@ -1,49 +0,0 @@
import sys, ldap, termios
from ceo import members, terms, uwldap, ldapi
from ceo.console.memberlist import MemberList
from ceo.console.updateprograms import UpdatePrograms
from ceo.console.expiredaccounts import ExpiredAccounts
from ceo.console.inactive import Inactive
from ceo.console.mysql import MySQL
commands = {
'memberlist' : MemberList(),
'updateprograms' : UpdatePrograms(),
'expiredaccounts' : ExpiredAccounts(),
'inactive': Inactive(),
'mysql': MySQL(),
}
help_opts = [ '--help', '-h' ]
def start():
args = sys.argv[1:]
if args[0] in help_opts:
help()
elif args[0] in commands:
command = commands[args[0]]
if len(args) >= 2 and args[1] in help_opts:
print command.help
else:
command.main(args[1:])
else:
print "Invalid command '%s'" % args[0]
def help():
args = sys.argv[2:]
if len(args) == 1:
if args[0] in commands:
print commands[args[0]].help
else:
print 'Unknown command %s.' % args[0]
else:
print ''
print 'To run the ceo GUI, type \'ceo\''
print ''
print 'To run a ceo console command, type \'ceo command\''
print ''
print 'Available console commands:'
for c in commands:
print ' %s' % c
print ''
print 'Run \'ceo command --help\' for help on a specific command.'
print ''

@ -1,24 +0,0 @@
from ceo import members, terms
class MemberList:
help = '''
memberlist [term]
Displays a list of members for a term; defaults to the current term if term
is not given.
'''
def main(self, args):
mlist = {}
if len(args) == 1:
mlist = members.list_term(args[0])
else:
mlist = members.list_term(terms.current())
dns = mlist.keys()
dns.sort()
for dn in dns:
member = mlist[dn]
print '%s %s %s' % (
member['uid'][0].ljust(12),
member['cn'][0].ljust(30),
member.get('program', [''])[0]
)

@ -1,38 +0,0 @@
from ceo import members, terms, mysql
class MySQL:
help = '''
mysql create <username>
Creates a mysql database for a user.
'''
def main(self, args):
if len(args) != 2 or args[0] != 'create':
print self.help
return
username = args[1]
problem = None
try:
password = mysql.create_mysql(username)
try:
mysql.write_mysql_info(username, password)
helpfiletext = "Settings written to ~%s/ceo-mysql-info." % username
except (KeyError, IOError, OSError), e:
helpfiletext = "An error occured writing the settings file: %s" % e
print "MySQL database created"
print ("Connection Information: \n"
"\n"
"Database: %s\n"
"Username: %s\n"
"Hostname: localhost\n"
"Password: %s\n"
"\n"
"%s\n"
% (username, username, password, helpfiletext))
except mysql.MySQLException, e:
print "Failed to create MySQL database"
print
print "We failed to create the database. The error was:\n\n%s" % e

@ -1,49 +0,0 @@
import ldap, sys, termios
from ceo import members, uwldap, ldapi
blacklist = ('orphaned', 'expired')
class UpdatePrograms:
help = '''
updateprograms
Interactively updates the program field for an account by querying uwdir.
'''
def main(self, args):
mlist = members.list_all().items()
uwl = ldap.initialize(uwldap.uri())
fd = sys.stdin.fileno()
for (dn, member) in mlist:
uid = member['uid'][0]
user = uwl.search_s(uwldap.base(), ldap.SCOPE_SUBTREE,
'(uid=%s)' % ldapi.escape(uid))
if len(user) == 0:
continue
user = user[0][1]
oldprog = member.get('program', [''])[0]
newprog = user.get('ou', [''])[0]
if oldprog == newprog or newprog == '' or newprog.lower() in blacklist:
continue
sys.stdout.write("%s: '%s' => '%s'? (y/n) " % (uid, oldprog, newprog))
new = old = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON
try:
termios.tcsetattr(fd, termios.TCSANOW, new)
try:
if sys.stdin.read(1) != 'y':
continue
except KeyboardInterrupt:
return ''
finally:
print ''
termios.tcsetattr(fd, termios.TCSANOW, old)
old = new = {}
if oldprog != '':
old = {'program': [oldprog]}
if newprog != '':
new = {'program': [newprog]}
mlist = ldapi.make_modlist(old, new)
# TODO: don't use members.ld directly
#if newprog != '':
# members.set_program(uid, newprog)
members.ld.modify_s(dn, mlist)

@ -1,13 +0,0 @@
"""
Exceptions Module
This module provides some simple but generally useful exception classes.
"""
class InvalidArgument(Exception):
"""Exception class for bad argument values."""
def __init__(self, argname, argval, explanation):
Exception.__init__(self)
self.argname, self.argval, self.explanation = argname, argval, explanation
def __str__(self):
return 'Bad argument value "%s" for %s: %s' % (self.argval, self.argname, self.explanation)

@ -1,148 +0,0 @@
"""
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, os, pwd
from subprocess import Popen, PIPE
def connect_sasl(uri, mech, realm, password):
try:
# open the connection
ld = ldap.initialize(uri)
# authenticate
sasl = Sasl(mech, realm, password)
ld.sasl_interactive_bind_s('', sasl)
except ldap.LOCAL_ERROR, e:
raise e
except:
print "Shit, something went wrong!"
return ld
def abslookup(ld, dn, objectclass=None):
# 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
# dn was found, but didn't match the objectclass filter
if len(matches) < 1:
return None
# return the attributes of the single successful match
match = matches[0]
match_dn, match_attributes = match
return match_attributes
def lookup(ld, rdntype, rdnval, base, objectclass=None):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
return abslookup(ld, dn, objectclass)
def search(ld, base, search_filter, params=[], scope=ldap.SCOPE_SUBTREE, attrlist=None, attrsonly=0):
real_filter = search_filter % tuple(escape(x) for x in params)
# search for entries that match the filter
matches = ld.search_s(base, scope, real_filter, attrlist, attrsonly)
return matches
def modify(ld, rdntype, rdnval, base, mlist):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
ld.modify_s(dn, mlist)
def modify_attrs(ld, rdntype, rdnval, base, old, attrs):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
# build list of modifications to make
changes = ldap.modlist.modifyModlist(old, attrs)
# apply changes
ld.modify_s(dn, changes)
def modify_diff(ld, rdntype, rdnval, base, old, new):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
# build list of modifications to make
changes = make_modlist(old, new)
# apply changes
ld.modify_s(dn, changes)
def escape(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 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
def format_ldaperror(ex):
desc = ex[0].get('desc', '')
info = ex[0].get('info', '')
if desc and info:
return "%s: %s" % (desc, info)
elif desc:
return desc
else:
return str(ex)
class Sasl:
def __init__(self, mech, realm, password):
self.mech = mech
self.realm = realm
if mech == 'GSSAPI' and password is not None:
userid = pwd.getpwuid(os.getuid()).pw_name
kinit = '/usr/bin/kinit'
kinit_args = [ kinit, '%s@%s' % (userid, realm) ]
kinit = Popen(kinit_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
kinit.stdin.write('%s\n' % password)
kinit.wait()
def callback(self, id, challenge, prompt, defresult):
return ''

@ -1,609 +0,0 @@
"""
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.
Future changes to the members database that need to be atomic
must also be moved into this module.
"""
import os, re, subprocess, ldap, socket
from ceo import conf, ldapi, terms, remote, ceo_pb2
from ceo.excep import InvalidArgument
import dns.resolver
### Configuration ###
CONFIG_FILE = '/etc/csc/accounts.cf'
cfg = {}
def configure():
"""Load Members Configuration"""
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' ]
# read configuration file
cfg_tmp = conf.read(CONFIG_FILE)
# verify configuration
conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
# 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):
Exception.__init__(self)
self.ex = ex
def __str__(self):
return str(self.ex)
class InvalidTerm(MemberException):
"""Exception class for malformed terms."""
def __init__(self, term):
MemberException.__init__(self)
self.term = term
def __str__(self):
return "Term is invalid: %s" % self.term
class NoSuchMember(MemberException):
"""Exception class for nonexistent members."""
def __init__(self, memberid):
MemberException.__init__(self)
self.memberid = memberid
def __str__(self):
return "Member not found: %d" % self.memberid
### Connection Management ###
# global directory connection
ld = None
def connect(auth_callback):
"""Connect to LDAP."""
global ld
password = None
tries = 0
while ld is None:
try:
ld = ldapi.connect_sasl(cfg['ldap_server_url'], cfg['ldap_sasl_mech'],
cfg['ldap_sasl_realm'], password)
except ldap.LOCAL_ERROR, e:
tries += 1
if tries > 3:
raise e
password = auth_callback.callback(e)
if password == None:
raise e
def connect_anonymous():
"""Connect to LDAP."""
global ld
ld = ldap.initialize(cfg['ldap_server_url'])
def disconnect():
"""Disconnect from LDAP."""
global ld
ld.unbind_s()
ld = None
def connected():
"""Determine whether the connection has been established."""
return ld and ld.connected()
### Members ###
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'])
try:
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
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)
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:
raise MemberException(e)
except OSError, e:
raise MemberException(e)
def get(userid):
"""
Look up attributes of a member by userid.
Returns: a dictionary of attributes
Example: get('mspang') -> {
'cn': [ 'Michael Spang' ],
'program': [ 'Computer Science' ],
...
}
"""
return ldapi.lookup(ld, 'uid', userid, cfg['ldap_users_base'])
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):
return 'uid=%s,%s' % (ldapi.escape(uid), cfg['ldap_users_base'])
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
Example: list_term('f2006'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
'uid=ctdalek, ou=...': { 'cn': 'Calum T. Dalek', ... },
...
}
"""
members = ldapi.search(ld, cfg['ldap_users_base'],
'(&(objectClass=member)(term=%s))', [ term ])
return dict([(member[0], member[1]) for member in members])
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
Example: list_name('Spang'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
...
]
"""
members = ldapi.search(ld, cfg['ldap_users_base'],
'(&(objectClass=member)(cn~=%s))', [ name ])
return dict([(member[0], member[1]) for member in members])
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)
ret = {}
if members:
for member in members:
info = get(member)
if info:
ret[uid2dn(member)] = info
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', ... },
...
]
"""
members = ldapi.search(ld, cfg['ldap_users_base'], '(objectClass=member)')
return dict([(member[0], member[1]) for member in members])
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', ... } } ],
...
]
"""
members = ld.search_s(cfg['ldap_users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
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
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'])
"""
res = ld.search_s(cfg['ldap_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 = {
'del': set(old) - set(new),
'add': set(new) - set(old),
}
if len(mods['del']) == 0 and len(mods['add']) == 0:
return
for action in ['del', 'add']:
for userid in mods[action]:
dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
entry1 = {'position' : [position]}
entry2 = {} #{'position' : []}
entry = ()
if action == 'del':
entry = (entry1, entry2)
elif action == 'add':
entry = (entry2, entry1)
mlist = ldapi.make_modlist(entry[0], entry[1])
ld.modify_s(dn, mlist)
def change_group_member(action, group, userid):
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['ldap_users_base'])
group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['ldap_groups_base'])
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)
### Shells ###
def get_shell(userid):
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'])
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']))
try:
request = ceo_pb2.AddUser()
request.type = ceo_pb2.AddUser.CLUB
request.username = username
request.realname = name
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)
except OSError, e:
raise MemberException(e)
### Terms ###
def register(userid, term_list):
"""
Registers a member for one or more terms.
Parameters:
userid - the member's username
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"])
"""
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 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'][:]
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['term']:
new_member['term'].append(term)
mlist = ldapi.make_modlist(ldap_member, new_member)
ld.modify_s(user_dn, mlist)
def register_nonmember(userid, term_list):
"""Registers a non-member for one or more terms."""
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):
"""
Determines whether a member is registered
for a term.
Parameters:
userid - the member's username
term - the term to check
Returns: whether the member is registered
Example: registered("mspang", "f2006") -> True
"""
member = get(userid)
if not member is None:
return 'term' in member and term in member['term']
else:
return False
def group_members(group):
"""
Returns a list of group members
"""
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 []
def expired_accounts():
members = ldapi.search(ld, cfg['ldap_users_base'],
'(&(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):
args = [ cfg['expire_hook'], name, email ]
os.spawnv(os.P_WAIT, cfg['expire_hook'], args)
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'

@ -1,54 +0,0 @@
import os, re, subprocess, ldap, socket, pwd
from ceo import conf, ldapi, terms, remote, ceo_pb2
from ceo.excep import InvalidArgument
class MySQLException(Exception):
pass
def write_mysql_info(username, password):
homedir = pwd.getpwnam(username).pw_dir
password_file = '%s/ceo-mysql-info' % homedir
if os.path.exists(password_file):
os.rename(password_file, password_file + '.old')
fd = os.open(password_file, os.O_CREAT|os.O_EXCL|os.O_WRONLY, 0660)
fh = os.fdopen(fd, 'w')
fh.write("""MySQL Database Information for %(username)s
Your new MySQL database was created. To connect, use
the following options:
Database: %(username)s
Username: %(username)s
Password: %(password)s
Hostname: localhost
The command to connect using the MySQL command-line client is
mysql %(username)s -u %(username)s -p
If you prefer a GUI you can use phpmyadmin at
http://csclub.uwaterloo.ca/phpmyadmin
This database is only accessible from caffeine.
""" % { 'username': username, 'password': password })
fh.close()
def create_mysql(username):
try:
request = ceo_pb2.AddMySQLUser()
request.username = username
out = remote.run_remote('mysql', request.SerializeToString())
response = ceo_pb2.AddMySQLUserResponse()
response.ParseFromString(out)
if any(message.status != 0 for message in response.messages):
raise MySQLException('\n'.join(message.message for message in response.messages))
return response.password
except remote.RemoteException, e:
raise MySQLException(e)

@ -1,24 +0,0 @@
import os, syslog, grp
def response_message(response, status, message):
if status:
priority = syslog.LOG_ERR
else:
priority = syslog.LOG_INFO
syslog.syslog(priority, message)
msg = response.messages.add()
msg.status = status
msg.message = message
return status
def get_ceo_user():
user = os.environ.get('CEO_USER')
if not user:
raise Exception("environment variable CEO_USER not set");
return user
def check_group(user, group):
try:
return user in grp.getgrnam(group).gr_mem
except KeyError:
return False

@ -1,155 +0,0 @@
#!/usr/bin/python
from xml.dom import minidom, Node
import urllib
import time
import datetime
import hashlib
import base64
import hmac
class PyMazonError(Exception):
"""Holds information about an error that occured during a pymazon request"""
def __init__(self, messages):
self.__message = '\n'.join(messages)
def __get_message(self):
return self.__message
def __str__(self):
return repr(self.__message)
message = property(fget=__get_message)
class PyMazonBook:
"""Stores information about a book retrieved via PyMazon."""
def __init__(self, title, authors, publisher, year, isbn10, isbn13, edition):
self.__title = title
self.__authors = authors
self.__publisher = publisher
self.__year = year
self.__isbn10 = isbn10
self.__isbn13 = isbn13
self.__edition = edition
def __str__(self):
return 'Title: ' + self.title + '\n' + \
'Author(s): ' + ', '.join(self.authors) + '\n' \
'Publisher: ' + self.publisher + '\n' + \
'Year: ' + self.year + '\n' + \
'ISBN-10: ' + self.isbn10 + '\n' + \
'ISBN-13: ' + self.isbn13 + '\n' + \
'Edition: ' + self.edition
def __get_title(self):
return self.__title
def __get_authors(self):
return self.__authors
def __get_publisher(self):
return self.__publisher
def __get_year(self):
return self.__year
def __get_isbn10(self):
return self.__isbn10
def __get_isbn13(self):
return self.__isbn13
def __get_edition(self):
return self.__edition
title = property(fget=__get_title)
authors = property(fget=__get_authors)
publisher = property(fget=__get_publisher)