parent
0c6dc18085
commit
de0f473881
@ -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) |
||||