From de0f47388148d297bbdb2d6347238d1c714ab5c7 Mon Sep 17 00:00:00 2001 From: Max Erenberg Date: Mon, 19 Jul 2021 05:47:39 +0000 Subject: [PATCH] add base classes for users and groups --- ceo/.gitignore | 1 - ceo/__init__.py | 1 - ceo/conf.py | 162 ------ ceo/console/__init__.py | 1 - ceo/console/expiredaccounts.py | 40 -- ceo/console/inactive.py | 27 - ceo/console/main.py | 49 -- ceo/console/memberlist.py | 24 - ceo/console/mysql.py | 38 -- ceo/console/updateprograms.py | 49 -- ceo/excep.py | 13 - ceo/ldapi.py | 148 ------ ceo/members.py | 609 ---------------------- ceo/mysql.py | 54 -- ceo/ops.py | 24 - ceo/pymazon.py | 155 ------ ceo/remote.py | 18 - ceo/terms.py | 254 --------- ceo/test.py | 42 -- ceo/urwid/__init__.py | 1 - ceo/urwid/databases.py | 84 --- ceo/urwid/groups.py | 135 ----- ceo/urwid/info.py | 47 -- ceo/urwid/library.py | 8 - ceo/urwid/main.py | 192 ------- ceo/urwid/newmember.py | 267 ---------- ceo/urwid/positions.py | 97 ---- ceo/urwid/renew.py | 240 --------- ceo/urwid/search.py | 83 --- ceo/urwid/shell.py | 95 ---- ceo/urwid/widgets.py | 247 --------- ceo/urwid/window.py | 80 --- ceo/uwldap.py | 8 - ceo_common/__init__.py | 0 ceo_common/interfaces/IConfig.py | 8 + ceo_common/interfaces/IGroup.py | 39 ++ ceo_common/interfaces/IKerberosService.py | 14 + ceo_common/interfaces/ILDAPService.py | 32 ++ ceo_common/interfaces/IUser.py | 65 +++ ceo_common/interfaces/__init__.py | 5 + ceo_common/model/Config.py | 24 + ceo_common/model/__init__.py | 1 + ceod/__init__.py | 0 ceod/model/Group.py | 84 +++ ceod/model/KerberosService.py | 44 ++ ceod/model/LDAPService.py | 139 +++++ ceod/model/User.py | 169 ++++++ ceod/model/__init__.py | 4 + ceod/model/utils.py | 34 ++ 49 files changed, 662 insertions(+), 3293 deletions(-) delete mode 100644 ceo/.gitignore delete mode 100644 ceo/conf.py delete mode 100644 ceo/console/__init__.py delete mode 100644 ceo/console/expiredaccounts.py delete mode 100644 ceo/console/inactive.py delete mode 100644 ceo/console/main.py delete mode 100644 ceo/console/memberlist.py delete mode 100644 ceo/console/mysql.py delete mode 100644 ceo/console/updateprograms.py delete mode 100644 ceo/excep.py delete mode 100644 ceo/ldapi.py delete mode 100644 ceo/members.py delete mode 100644 ceo/mysql.py delete mode 100644 ceo/ops.py delete mode 100644 ceo/pymazon.py delete mode 100644 ceo/remote.py delete mode 100644 ceo/terms.py delete mode 100644 ceo/test.py delete mode 100644 ceo/urwid/__init__.py delete mode 100644 ceo/urwid/databases.py delete mode 100644 ceo/urwid/groups.py delete mode 100644 ceo/urwid/info.py delete mode 100644 ceo/urwid/library.py delete mode 100644 ceo/urwid/main.py delete mode 100644 ceo/urwid/newmember.py delete mode 100644 ceo/urwid/positions.py delete mode 100644 ceo/urwid/renew.py delete mode 100644 ceo/urwid/search.py delete mode 100644 ceo/urwid/shell.py delete mode 100644 ceo/urwid/widgets.py delete mode 100644 ceo/urwid/window.py delete mode 100644 ceo/uwldap.py create mode 100644 ceo_common/__init__.py create mode 100644 ceo_common/interfaces/IConfig.py create mode 100644 ceo_common/interfaces/IGroup.py create mode 100644 ceo_common/interfaces/IKerberosService.py create mode 100644 ceo_common/interfaces/ILDAPService.py create mode 100644 ceo_common/interfaces/IUser.py create mode 100644 ceo_common/interfaces/__init__.py create mode 100644 ceo_common/model/Config.py create mode 100644 ceo_common/model/__init__.py create mode 100644 ceod/__init__.py create mode 100644 ceod/model/Group.py create mode 100644 ceod/model/KerberosService.py create mode 100644 ceod/model/LDAPService.py create mode 100644 ceod/model/User.py create mode 100644 ceod/model/__init__.py create mode 100644 ceod/model/utils.py diff --git a/ceo/.gitignore b/ceo/.gitignore deleted file mode 100644 index ad2d2dc..0000000 --- a/ceo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/ceo_pb2.py diff --git a/ceo/__init__.py b/ceo/__init__.py index c6f4cce..e69de29 100644 --- a/ceo/__init__.py +++ b/ceo/__init__.py @@ -1 +0,0 @@ -"""CSC Electronic Office""" diff --git a/ceo/conf.py b/ceo/conf.py deleted file mode 100644 index f9e6972..0000000 --- a/ceo/conf.py +++ /dev/null @@ -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)) diff --git a/ceo/console/__init__.py b/ceo/console/__init__.py deleted file mode 100644 index 2d956cc..0000000 --- a/ceo/console/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Console Interface""" diff --git a/ceo/console/expiredaccounts.py b/ceo/console/expiredaccounts.py deleted file mode 100644 index 898e437..0000000 --- a/ceo/console/expiredaccounts.py +++ /dev/null @@ -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) diff --git a/ceo/console/inactive.py b/ceo/console/inactive.py deleted file mode 100644 index bf85167..0000000 --- a/ceo/console/inactive.py +++ /dev/null @@ -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) diff --git a/ceo/console/main.py b/ceo/console/main.py deleted file mode 100644 index 463fb21..0000000 --- a/ceo/console/main.py +++ /dev/null @@ -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 '' diff --git a/ceo/console/memberlist.py b/ceo/console/memberlist.py deleted file mode 100644 index 10237ad..0000000 --- a/ceo/console/memberlist.py +++ /dev/null @@ -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] - ) diff --git a/ceo/console/mysql.py b/ceo/console/mysql.py deleted file mode 100644 index f617751..0000000 --- a/ceo/console/mysql.py +++ /dev/null @@ -1,38 +0,0 @@ -from ceo import members, terms, mysql - -class MySQL: - help = ''' -mysql create - -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 - diff --git a/ceo/console/updateprograms.py b/ceo/console/updateprograms.py deleted file mode 100644 index e8331c1..0000000 --- a/ceo/console/updateprograms.py +++ /dev/null @@ -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) diff --git a/ceo/excep.py b/ceo/excep.py deleted file mode 100644 index 71465b7..0000000 --- a/ceo/excep.py +++ /dev/null @@ -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) diff --git a/ceo/ldapi.py b/ceo/ldapi.py deleted file mode 100644 index 6e5eb5a..0000000 --- a/ceo/ldapi.py +++ /dev/null @@ -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 '' diff --git a/ceo/members.py b/ceo/members.py deleted file mode 100644 index e0365d3..0000000 --- a/ceo/members.py +++ /dev/null @@ -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", "", "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' diff --git a/ceo/mysql.py b/ceo/mysql.py deleted file mode 100644 index d4febbd..0000000 --- a/ceo/mysql.py +++ /dev/null @@ -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) - diff --git a/ceo/ops.py b/ceo/ops.py deleted file mode 100644 index 4d383e9..0000000 --- a/ceo/ops.py +++ /dev/null @@ -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 diff --git a/ceo/pymazon.py b/ceo/pymazon.py deleted file mode 100644 index d27cc0c..0000000 --- a/ceo/pymazon.py +++ /dev/null @@ -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) - year = property(fget=__get_year) - isbn10 = property(fget=__get_isbn10) - isbn13 = property(fget=__get_isbn13) - edition = property(fget=__get_edition) - - -class PyMazon: - """A method of looking up book information on Amazon.""" - def __init__(self, accesskey, secretkey): - self.__key = accesskey - self.__secret = secretkey - self.__last_query_time = 0 - - def __form_request(self, isbn): - content = {} - dstamp = datetime.datetime.utcfromtimestamp(time.time()) - content['Timestamp'] = dstamp.strftime('%Y-%m-%dT%H:%M:%S.000Z') - content['Service'] = 'AWSECommerceService' - content['Version'] = '2008-08-19' - content['Operation'] = 'ItemLookup' - content['ResponseGroup'] = 'ItemAttributes' - content['IdType'] = 'ISBN' - content['SearchIndex'] = 'Books' - content['ItemId'] = isbn - content['AWSAccessKeyId'] = self.__key - - URI_String = [] - - for key, value in sorted(content.items()): - URI_String.append('%s=%s' % (key, urllib.quote(value))) - - req = '&'.join(URI_String) - to_sign_req = 'GET\necs.amazonaws.com\n/onca/xml\n' + req - - h = hmac.new(self.__secret, to_sign_req, hashlib.sha256) - sig = base64.b64encode(h.digest()) - req += '&Signature=%s' % urllib.quote(sig) - - return 'http://ecs.amazonaws.com/onca/xml?' + req - - def __elements_text(self, element, name): - result = [] - matching = element.getElementsByTagName(name) - for match in matching: - if len(match.childNodes) != 1: - continue - child = match.firstChild - if child.nodeType != Node.TEXT_NODE: - continue - result.append(child.nodeValue.strip()) - return result - - def __format_errors(self, errors): - error_list = [] - for error in errors: - error_list.extend(self.__elements_text(error, 'Message')) - return error_list - - def __extract_single(self, element, name): - matches = self.__elements_text(element, name) - if len(matches) == 0: - return '' - return matches[0] - - def lookup(self, isbn): - file = urllib.urlretrieve(self.__form_request(isbn))[0] - xmldoc = minidom.parse(file) - - cur_time = time.time() - while cur_time - self.__last_query_time < 1.0: - sleep(cur_time - self.__last_query_time) - cur_time = time.time() - self.__last_query_time = cur_time - - errors = xmldoc.getElementsByTagName('Errors') - if len(errors) != 0: - raise PyMazonError, self.__format_errors(errors) - - title = self.__extract_single(xmldoc, 'Title') - authors = self.__elements_text(xmldoc, 'Author') - publisher = self.__extract_single(xmldoc, 'Publisher') - year = self.__extract_single(xmldoc, 'PublicationDate')[0:4] - isbn10 = self.__extract_single(xmldoc, 'ISBN') - isbn13 = self.__extract_single(xmldoc, 'EAN') - edition = self.__extract_single(xmldoc, 'Edition') - - return PyMazonBook(title, authors, publisher, year, isbn10, isbn13, edition) diff --git a/ceo/remote.py b/ceo/remote.py deleted file mode 100644 index ba792fe..0000000 --- a/ceo/remote.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -import subprocess - -class RemoteException(Exception): - """Exception class for bad argument values.""" - def __init__(self, status, stdout, stderr): - self.status, self.stdout, self.stderr = status, stdout, stderr - def __str__(self): - return 'Error executing ceoc (%d)\n\n%s' % (self.status, self.stderr) - -def run_remote(op, data): - ceoc = '%s/ceoc' % os.environ.get('CEO_LIB_DIR', '/usr/lib/ceod') - addmember = subprocess.Popen([ceoc, op], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = addmember.communicate(data) - status = addmember.wait() - if status: - raise RemoteException(status, out, err) - return out diff --git a/ceo/terms.py b/ceo/terms.py deleted file mode 100644 index 63b9bb5..0000000 --- a/ceo/terms.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -Terms Routines - -This module contains functions for manipulating terms, such as determining -the current term, finding the next or previous term, converting dates to -terms, and more. -""" -import time, datetime, re - -# year to count terms from -EPOCH = 1970 - -# seasons list -SEASONS = [ 'w', 's', 'f' ] - - -def validate(term): - """ - Determines whether a term is well-formed. - - Parameters: - term - the term string - - Returns: whether the term is valid (boolean) - - Example: validate("f2006") -> True - """ - - regex = '^[wsf][0-9]{4}$' - return re.match(regex, term) is not None - - -def parse(term): - """Helper function to convert a term string to the number of terms - since the epoch. Such numbers are intended for internal use only.""" - - if not validate(term): - raise Exception("malformed term: %s" % term) - - year = int( term[1:] ) - season = SEASONS.index( term[0] ) - - return (year - EPOCH) * len(SEASONS) + season - - -def generate(term): - """Helper function to convert a year and season to a term string.""" - - year = int(term / len(SEASONS)) + EPOCH - season = term % len(SEASONS) - - return "%s%04d" % ( SEASONS[season], year ) - - -def next(term): - """ - Returns the next term. (convenience function) - - Parameters: - term - the term string - - Retuns: the term string of the following term - - Example: next("f2006") -> "w2007" - """ - - return add(term, 1) - - -def previous(term): - """ - Returns the previous term. (convenience function) - - Parameters: - term - the term string - - Returns: the term string of the preceding term - - Example: previous("f2006") -> "s2006" - """ - - return add(term, -1) - - -def add(term, offset): - """ - Calculates a term relative to some base term. - - Parameters: - term - the base term - offset - the number of terms since term (may be negative) - - Returns: the term that comes offset terms after term - """ - - return generate(parse(term) + offset) - - -def delta(initial, final): - """ - Calculates the distance between two terms. - It should be true that add(a, delta(a, b)) == b. - - Parameters: - initial - the base term - final - the term at some offset from the base term - - Returns: the offset of final relative to initial - """ - - return parse(final) - parse(initial) - - -def compare(first, second): - """ - Compares two terms. This function is suitable - for use with list.sort(). - - Parameters: - first - base term for comparison - second - term to compare to - - Returns: > 0 (if first > second) - = 0 (if first == second) - < 0 (if first < second) - """ - return delta(second, first) - - -def interval(base, count): - """ - Returns a list of adjacent terms. - - Parameters: - base - the first term in the interval - count - the number of terms to include - - Returns: a list of count terms starting with initial - - Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ] - """ - - terms = [] - - for num in xrange(count): - terms.append( add(base, num) ) - - return terms - - -def tstamp(timestamp): - """Helper to convert seconds since the epoch - to terms since the epoch.""" - - # let python determine the month and year - date = datetime.date.fromtimestamp(timestamp) - - # determine season - if date.month <= 4: - season = SEASONS.index('w') - elif date.month <= 8: - season = SEASONS.index('s') - else: - season = SEASONS.index('f') - - return (date.year - EPOCH) * len(SEASONS) + season - - -def from_timestamp(timestamp): - """ - Converts a number of seconds since - the epoch to a number of terms since - the epoch. - - This function notes that: - WINTER = JANUARY to APRIL - SPRING = MAY to AUGUST - FALL = SEPTEMBER to DECEMBER - - Parameters: - timestamp - number of seconds since the epoch - - Returns: the number of terms since the epoch - - Example: from_timestamp(1166135779) -> 'f2006' - """ - - return generate( tstamp(timestamp) ) - - -def curr(): - """Helper to determine the current term.""" - - return tstamp( time.time() ) - - -def current(): - """ - Determines the current term. - - Returns: current term - - Example: current() -> 'f2006' - """ - - return generate( curr() ) - - -def next_unregistered(registered): - """ - Find the first future or current unregistered term. - Intended as the 'default' for registrations. - - Parameters: - registered - a list of terms a member is registered for - - Returns: the next unregistered term - """ - - # get current term number - now = curr() - - # never registered -> current term is next - if len( registered) < 1: - return generate( now ) - - # return the first unregistered, or the current term (whichever is greater) - return generate(max([max(map(parse, registered))+1, now])) - - - -### Tests ### - -if __name__ == '__main__': - - from ceo.test import test, assert_equal, success - - test(parse); assert_equal(110, parse('f2006')); success() - test(generate); assert_equal('f2006', generate(110)); success() - test(next); assert_equal('w2007', next('f2006')); success() - test(previous); assert_equal('s2006', previous('f2006')); success() - test(delta); assert_equal(1, delta('f2006', 'w2007')); success() - test(compare); assert_equal(-1, compare('f2006', 'w2007')); success() - test(add); assert_equal('w2010', add('f2006', delta('f2006', 'w2010'))); success() - test(interval); assert_equal(['f2006', 'w2007', 's2007'], interval('f2006', 3)); success() - test(from_timestamp); assert_equal('f2006', from_timestamp(1166135779)); success() - test(current); assert_equal(True, parse( current() ) >= 110 ); success() - - test(next_unregistered) - assert_equal( next(current()), next_unregistered([ current() ])) - assert_equal( current(), next_unregistered([])) - assert_equal( current(), next_unregistered([ previous(current()) ])) - assert_equal( current(), next_unregistered([ add(current(), -2) ])) - success() diff --git a/ceo/test.py b/ceo/test.py deleted file mode 100644 index a60b2ed..0000000 --- a/ceo/test.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Common Test Routines - -This module contains helpful functions called by each module's test suite. -""" -from types import FunctionType, MethodType, ClassType, TypeType - - -class TestException(Exception): - """Exception class for test failures.""" - - -def test(subject): - """Print a test message.""" - if type(subject) in (MethodType, FunctionType, ClassType, TypeType): - print "testing %s()..." % subject.__name__, - else: - print "testing %s..." % subject, - - -def success(): - """Print a success message.""" - print "pass." - - -def assert_equal(expected, actual): - if expected != actual: - message = "Expected (%s)\nWas (%s)" % (repr(expected), repr(actual)) - fail(message) - - -def fail(message): - print "failed!" - raise TestException("Test failed:\n%s" % message) - - -def negative(call, args, excep, message): - try: - call(*args) - fail(message) - except excep: - pass diff --git a/ceo/urwid/__init__.py b/ceo/urwid/__init__.py deleted file mode 100644 index c868e93..0000000 --- a/ceo/urwid/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Urwid User Interface""" diff --git a/ceo/urwid/databases.py b/ceo/urwid/databases.py deleted file mode 100644 index 3825a96..0000000 --- a/ceo/urwid/databases.py +++ /dev/null @@ -1,84 +0,0 @@ -import urwid -from ceo import members, mysql -from ceo.urwid import search -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -class IntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text("MySQL databases"), - urwid.Divider(), - urwid.Text("Members and hosted clubs may have one MySQL database each. You may " - "create a database for an account if: \n" - "\n" - "- It is your personal account,\n" - "- It is a club account, and you are in the club group, or\n" - "- You are on the CSC systems committee\n" - "\n" - "You may also use this to reset your database password." - ) - ] - def focusable(self): - return False - -class UserPage(WizardPanel): - def init_widgets(self): - self.userid = LdapWordEdit(csclub_uri, csclub_base, 'uid', - "Username: ") - - self.widgets = [ - urwid.Text("Member Information"), - urwid.Divider(), - urwid.Text("Enter the user which will own the new database."), - urwid.Divider(), - self.userid, - ] - def check(self): - self.state['userid'] = self.userid.get_edit_text() - self.state['member'] = None - if self.state['userid']: - self.state['member'] = members.get(self.userid.get_edit_text()) - if not self.state['member']: - set_status("Member not found") - self.focus_widget(self.userid) - return True - -class EndPage(WizardPanel): - def init_widgets(self): - self.headtext = urwid.Text("") - self.midtext = urwid.Text("") - - self.widgets = [ - self.headtext, - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def activate(self): - problem = None - try: - password = mysql.create_mysql(self.state['userid']) - - try: - mysql.write_mysql_info(self.state['userid'], password) - helpfiletext = "Settings written to ~%s/ceo-mysql-info." % self.state['userid'] - except (KeyError, IOError, OSError), e: - helpfiletext = "An error occured writing the settings file: %s" % e - self.headtext.set_text("MySQL database created") - self.midtext.set_text("Connection Information: \n" - "\n" - "Database: %s\n" - "Username: %s\n" - "Hostname: localhost\n" - "Password: %s\n" - "\n" - "%s\n" - % (self.state['userid'], self.state['userid'], password, helpfiletext)) - except mysql.MySQLException, e: - self.headtext.set_text("Failed to create MySQL database") - self.midtext.set_text("We failed to create the database. The error was:\n\n%s" % e) - - def check(self): - pop_window() diff --git a/ceo/urwid/groups.py b/ceo/urwid/groups.py deleted file mode 100644 index 7fa93fe..0000000 --- a/ceo/urwid/groups.py +++ /dev/null @@ -1,135 +0,0 @@ -import urwid -from ceo import members -from ceo.urwid import search -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -def change_group_member(data): - push_wizard("%s %s Member" % (data["action"], data["name"]), [ - (ChangeMember, data), - EndPage, - ]) - -def list_group_members(data): - mlist = members.list_group( data["group"] ).values() - search.member_list( mlist ) - -def group_members(data): - add_data = data.copy() - add_data['action'] = 'Add' - remove_data = data.copy() - remove_data['action'] = 'Remove' - menu = make_menu([ - ("Add %s member" % data["name"].lower(), - change_group_member, add_data), - ("Remove %s member" % data["name"].lower(), - change_group_member, remove_data), - ("List %s members" % data["name"].lower(), list_group_members, data), - ("Back", raise_back, None), - ]) - push_window(menu, "Manage %s" % data["name"]) - -class IntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Managing Club or Group" ), - urwid.Divider(), - urwid.Text( "Adding a member to a club will also grant them " - "access to the club's files and allow them to " - "become_club." - "\n\n") - ] - def focusable(self): - return False - -class InfoPage(WizardPanel): - def init_widgets(self): - self.group = LdapWordEdit(csclub_uri, csclub_base, 'uid', - "Club or Group: ") - self.widgets = [ - urwid.Text( "Club or Group Information"), - urwid.Divider(), - self.group, - ] - def check(self): - group = self.group.get_edit_text() - - # check if group is valid - if not group or not members.get_group(group): - set_status("Group not found") - self.focus_widget(self.group) - return True - - data = { - "name" : group, - "group" : group, - "groups" : [], - } - - # Office Staff and Syscom get added to more groups - if group == "syscom": - data["name"] = "Systems Committee" - data["groups"] = [ "office", "staff", "adm", "src" ] - elif group == "office": - data["name"] = "Office Staff" - data["groups"] = [ "cdrom", "audio", "video", "www" ] - - group_members(data) - -class ChangeMember(WizardPanel): - def __init__(self, state, data): - state['data'] = data - WizardPanel.__init__(self, state) - def init_widgets(self): - self.userid = LdapWordEdit(csclub_uri, csclub_base, 'uid', - "Username: ") - - data = self.state['data'] - self.widgets = [ - urwid.Text( "%s %s Member" % (data['action'], data['name']) ), - urwid.Divider(), - self.userid, - ] - def check(self): - self.state['userid'] = self.userid.get_edit_text() - if self.state['userid']: - self.state['member'] = members.get(self.userid.get_edit_text()) - if not self.state['member']: - set_status("Member not found") - self.focus_widget(self.userid) - return True - clear_status() - -class EndPage(WizardPanel): - def init_widgets(self): - self.headtext = urwid.Text("") - self.midtext = urwid.Text("") - self.widgets = [ - self.headtext, - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def check(self): - pop_window() - def activate(self): - data = self.state['data'] - action = data['action'].lower() - failed = [] - for group in data['groups'] + [data['group']]: - try: - members.change_group_member(action, group, self.state['userid']) - except ldap.LDAPError: - failed.append(group) - if len(failed) == 0: - self.headtext.set_text("%s succeeded" % data['action']) - self.midtext.set_text("Congratulations, the group modification " - "has succeeded.") - else: - self.headtext.set_text("%s Results" % data['action']) - self.midtext.set_text("Failed to %s member to %s for the " - "following groups: %s. This may indicate an attempt to add a " - "duplicate group member or to delete a member that was not in " - "the group." % (data['action'].lower(), data['name'], - ', '.join(failed))) diff --git a/ceo/urwid/info.py b/ceo/urwid/info.py deleted file mode 100644 index c33354e..0000000 --- a/ceo/urwid/info.py +++ /dev/null @@ -1,47 +0,0 @@ -import urwid -from ceo.urwid.widgets import * -from ceo.urwid.window import * -from ceo import terms - -class InfoPage(WizardPanel): - def init_widgets(self): - self.userid = urwid.Text("") - self.name = urwid.Text("") - self.terms = urwid.Text("") - self.nmterms = urwid.Text("") - self.program = urwid.Text("") - - self.widgets = [ - urwid.Text( "Member Details" ), - urwid.Divider(), - self.name, - self.userid, - self.program, - urwid.Divider(), - self.terms, - self.nmterms, - ] - def focusable(self): - return False - def activate(self): - member = self.state.get('member', {}) - name = member.get('cn', [''])[0] - userid = self.state['userid'] - program = member.get('program', [''])[0] - shell = member.get('loginShell', [''])[0] - mterms = member.get('term', []) - nmterms = member.get('nonMemberTerm', []) - - mterms.sort(terms.compare) - nmterms.sort(terms.compare) - - self.name.set_text("Name: %s" % name) - self.userid.set_text("User: %s" % userid) - self.program.set_text("Program: %s" % program) - self.program.set_text("Shell: %s" % shell) - if terms: - self.terms.set_text("Terms: %s" % ", ".join(mterms)) - if nmterms: - self.nmterms.set_text("Rep Terms: %s" % ", ".join(nmterms)) - def check(self): - pop_window() diff --git a/ceo/urwid/library.py b/ceo/urwid/library.py deleted file mode 100644 index fac59b2..0000000 --- a/ceo/urwid/library.py +++ /dev/null @@ -1,8 +0,0 @@ -import os -from ceo.urwid.window import * - - -def library(data): - os.system("librarian") - ui.stop() - ui.start() diff --git a/ceo/urwid/main.py b/ceo/urwid/main.py deleted file mode 100644 index 53d3ba2..0000000 --- a/ceo/urwid/main.py +++ /dev/null @@ -1,192 +0,0 @@ -import os, grp, pwd, sys, random, urwid.curses_display -from ceo.urwid.widgets import * -from ceo.urwid.window import * -from ceo.urwid import newmember, renew, info, search, positions, groups, \ - shell, library, databases - -def program_name(): - cwords = [ "CSC" ] * 20 + [ "Club" ] * 10 + [ "Campus" ] * 5 + \ - [ "Communist", "Canadian", "Celestial", "Cryptographic", "Calum's", - "Canonical", "Capitalist", "Catastrophic", "Ceremonial", "Chaotic", "Civic", - "City", "County", "Caffeinated" ] - ewords = [ "Embellished", "Ergonomic", "Electric", "Eccentric", "European", "Economic", - "Evil", "Egotistical", "Elliptic", "Emasculating", "Embalming", - "Embryonic", "Emigrant", "Emissary's", "Emoting", "Employment", "Emulated", - "Enabling", "Enamoring", "Encapsulated", "Enchanted", "Encoded", "Encrypted", - "Encumbered", "Endemic", "Enhanced", "Enigmatic", "Enlightened", "Enormous", - "Enrollment", "Enshrouded", "Ephemeral", "Epidemic", "Episodic", "Epsilon", - "Equitable", "Equestrian", "Equilateral", "Erroneous", "Erratic", - "Espresso", "Essential", "Estate", "Esteemed", "Eternal", "Ethical", "Eucalyptus", - "Euphemistic", "Evangelist", "Evasive", "Everyday", "Evidence", "Eviction", "Evildoer's", - "Evolution", "Exacerbation", "Exalted", "Examiner's", "Excise", "Exciting", "Exclusion", - "Exec", "Executioner's", "Exile", "Existential", "Expedient", "Expert", "Expletive", - "Exploiter's", "Explosive", "Exponential", "Exposing", "Extortion", "Extraction", - "Extraneous", "Extravaganza", "Extreme", "Extraterrestrial", "Extremist", "Eerie" ] - owords = [ "Office" ] * 50 + [ "Outhouse", "Outpost" ] - - cword = random.choice(cwords) - eword = random.choice(ewords) - oword = random.choice(owords) - - return "%s %s %s" % (cword, eword, oword) - -def new_member(*args, **kwargs): - push_wizard("New Member", [ - newmember.IntroPage, - newmember.InfoPage, - newmember.NumberOfTermsPage, - newmember.SignPage, - newmember.PassPage, - newmember.EndPage, - ], (60, 15)) - -def new_club(*args, **kwargs): - push_wizard("New Club Account", [ - newmember.ClubIntroPage, - newmember.ClubInfoPage, - (newmember.EndPage, "club"), - ], (60, 15)) - -def new_club_user(*args, **kwargs): - push_wizard("New Club Rep Account", [ - newmember.ClubUserIntroPage, - newmember.ClubNoPayPage, - newmember.InfoPage, - newmember.NumberOfTermsPage, - newmember.SignPage, - newmember.PassPage, - (newmember.EndPage, "clubuser"), - ], (60, 15)) - -def manage_group(*args, **kwargs): - push_wizard("Manage Club or Group Members", [ - groups.IntroPage, - groups.InfoPage, - ], (60, 15)) - -def renew_member(*args, **kwargs): - push_wizard("Renew Membership", [ - renew.IntroPage, - renew.UserPage, - renew.EmailPage, - renew.EmailDonePage, - renew.TermPage, - renew.PayPage, - renew.EndPage, - ], (60, 15)) - -def renew_club_user(*args, **kwargs): - push_wizard("Renew Club Rep Account", [ - renew.ClubUserIntroPage, - newmember.ClubNoPayPage, - renew.UserPage, - renew.EmailPage, - renew.EmailDonePage, - (renew.TermPage, "clubuser"), - (renew.EndPage, "clubuser"), - ], (60, 15)) - -def display_member(data): - push_wizard("Display Member", [ - renew.UserPage, - info.InfoPage, - ], (60, 15)) - -def search_members(data): - menu = make_menu([ - ("Members by term", search_term, None), - ("Members by name", search_name, None), - ("Members by group", search_group, None), - ("Back", raise_back, None), - ]) - push_window(menu, "Search Members") - -def search_name(data): - push_wizard("By Name", [ search.NamePage ]) - -def search_term(data): - push_wizard("By Term", [ search.TermPage ]) - -def search_group(data): - push_wizard("By Group", [ search.GroupPage ]) - -def manage_positions(data): - push_wizard("Manage Positions", [ - positions.IntroPage, - positions.InfoPage, - positions.EndPage, - ], (50, 15)) - -def change_shell(data): - push_wizard("Change Shell", [ - shell.IntroPage, - shell.YouPage, - shell.ShellPage, - shell.EndPage - ], (50, 20)) - -def create_mysql_db(data): - push_wizard("Create MySQL database", [ - databases.IntroPage, - databases.UserPage, - databases.EndPage, - ], (60, 15)) - -def check_group(group): - try: - me = pwd.getpwuid(os.getuid()).pw_name - return me in grp.getgrnam(group).gr_mem - except KeyError: - pass - -def top_menu(): - office_only = [ - ("New Member", new_member, None), - ("New Club Rep", new_club_user, None), - ("Renew Membership", renew_member, None), - ("Renew Club Rep", renew_club_user, None), - ("New Club", new_club, None), - ("Library", library.library, None), - ] - syscom_only = [ - ("Manage Club or Group Members", manage_group, None), - ("Manage Positions", manage_positions, None), - ] - unrestricted = [ - ("Display Member", display_member, None), - ("Search Members", search_members, None), - ("Change Shell", change_shell, None), - ("Create MySQL database", create_mysql_db, None), - ] - footer = [ - ("Exit", raise_abort, None), - ] - menu = None - - # reorder the menu for convenience - if not check_group('office') and not check_group('syscom'): - menu = labelled_menu([ - ('Unrestricted', unrestricted), - ('Office Staff', office_only), - ('Systems Committee', syscom_only), - (None, footer) - ]) - else: - menu = labelled_menu([ - ('Office Staff', office_only), - ('Unrestricted', unrestricted), - ('Systems Committee', syscom_only), - (None, footer) - ]) - - return menu - -def run(): - push_window(top_menu(), program_name()) - event_loop(ui) - -def start(): - ui.run_wrapper( run ) - -if __name__ == '__main__': - start() diff --git a/ceo/urwid/newmember.py b/ceo/urwid/newmember.py deleted file mode 100644 index 012d7b1..0000000 --- a/ceo/urwid/newmember.py +++ /dev/null @@ -1,267 +0,0 @@ -import ldap, urwid #, re -from ceo import members, terms, remote, uwldap -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -class IntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Joining the Computer Science Club" ), - urwid.Divider(), - urwid.Text( "CSC membership is $2.00 per term. Please ensure " - "the fee is deposited into the cup before continuing." ), - ] - def focusable(self): - return False - -class ClubIntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Club Accounts" ), - urwid.Divider(), - urwid.Text( "We provide other UW clubs accounts for email and " - "web hosting, free of charge. Like members, clubs " - "get web hosting at %s. We can also arrange for " - "uwaterloo.ca subdomains; please instruct the club " - "representative to contact the systems committee " - "for more information. Club accounts do not have " - "passwords, and exist primarily to own club data. " - % "http://csclub.uwaterloo.ca/~clubid/" ), - ] - def focusable(self): - return False - -class ClubNoPayPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Club representative accounts are free. Please ensure " - "that no money was paid for this account." ), - ] - def focusable(self): - return False - -class ClubUserIntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Club Rep Account" ), - urwid.Divider(), - urwid.Text( "This is for people who need access to a club account, " - "but are not currently interested in full CSC membership. " - "Registering a user in this way grants one term of free " - "access to our machines, without any other membership " - "privileges (they can't vote, hold office, etc). If such " - "a user later decides to join, use the Renew Membership " - "option." ), - ] - def focusable(self): - return False - -class InfoPage(WizardPanel): - def init_widgets(self): - self.name = SingleEdit("Full name: ") - self.program = SingleEdit("Program of Study: ") - self.email = SingleEdit("Email: ") - self.userid = LdapFilterWordEdit(uwldap.uri(), uwldap.base(), 'uid', - {'cn':self.name, 'ou':self.program}, "Username: ") - self.widgets = [ - urwid.Text( "Member Information" ), - urwid.Divider(), - self.userid, - self.name, - self.program, - self.email, - urwid.Divider(), - urwid.Text("Notes:"), - urwid.Text("- Make sure to check ID (watcard, drivers license)"), - urwid.Text("- Make sure to use UW userids for current students\n (we check uwldap to see if you are a full member)"), - ] - def check(self): - self.state['userid'] = self.userid.get_edit_text() - self.state['name'] = self.name.get_edit_text() - self.state['program'] = self.program.get_edit_text() - self.state['email'] = self.email.get_edit_text() - if len( self.state['userid'] ) < 2: - self.focus_widget( self.userid ) - set_status("Username is too short") - return True - elif len( self.state['name'] ) < 4: - self.focus_widget( self.name ) - set_status("Name is too short") - return True - elif self.state['userid'] == self.state['name']: - self.focus_widget(self.name) - set_status("Name matches username") - return True - clear_status() - -class ClubInfoPage(WizardPanel): - def init_widgets(self): - self.userid = WordEdit("Username: ") - self.name = SingleEdit("Club Name: ") - self.widgets = [ - urwid.Text( "Club Information" ), - urwid.Divider(), - self.userid, - self.name, - ] - def check(self): - self.state['userid'] = self.userid.get_edit_text() - self.state['name'] = self.name.get_edit_text() - - if len( self.state['userid'] ) < 3: - self.focus_widget( self.userid ) - set_status("Username is too short") - return True - elif len( self.state['name'] ) < 4: - self.focus_widget( self.name ) - set_status("Name is too short") - return True - elif self.state['userid'] == self.state['name']: - self.focus_widget(self.name) - set_status("Name matches username") - return True - clear_status() - -class NumberOfTermsPage(WizardPanel): - def init_widgets(self): - self.count = SingleIntEdit("Count: ") - self.widgets = [ - urwid.Text("Number of Terms"), - urwid.Divider(), - urwid.Text("The member will be initially registered for this many " - "consecutive terms.\n"), - self.count - ] - - def activate(self): - self.count.set_edit_text("1") - self.focus_widget(self.count) - - def check(self): - self.state['terms'] = terms.interval(terms.current(), self.count.value()) - - if len(self.state['terms']) == 0: - self.focus_widget(self.count) - set_status("Registering for zero terms?") - return True - clear_status() - -class SignPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Machine Usage Policy" ), - urwid.Divider(), - urwid.Text( "Ensure the new member has signed the " - "Machine Usage Policy. Accounts of users who have not " - "signed will be suspended if discovered." ), - ] - def focusable(self): - return False - -class PassPage(WizardPanel): - def init_widgets(self): - self.password = PassEdit("Password: ") - self.pwcheck = PassEdit("Re-enter: ") - self.widgets = [ - urwid.Text( "Member Password" ), - urwid.Divider(), - self.password, - self.pwcheck, - ] - def focus_widget(self, widget): - self.box.set_focus( self.widgets.index( widget ) ) - def clear_password(self): - self.focus_widget( self.password ) - self.password.set_edit_text("") - self.pwcheck.set_edit_text("") - def check(self): - self.state['password'] = self.password.get_edit_text() - pwcheck = self.pwcheck.get_edit_text() - - if self.state['password'] != pwcheck: - self.clear_password() - set_status("Passwords do not match") - return True - elif len(self.state['password']) < 5: - self.clear_password() - set_status("Password is too short") - return True - clear_status() - -class EndPage(WizardPanel): - def __init__(self, state, utype='member'): - self.utype = utype - WizardPanel.__init__(self, state) - def init_widgets(self): - self.headtext = urwid.Text("") - self.midtext = urwid.Text("") - self.widgets = [ - self.headtext, - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def check(self): - pop_window() - def activate(self): - self.headtext.set_text("Adding %s" % self.state['userid']) - self.midtext.set_text("Please be patient while the user is added. " - "If more than a few seconds pass, check for a " - "phase variance and try inverting the polarity.") - set_status("Contacting the gibson...") - - redraw() - - problem = None - try: - if self.utype == 'member': - members.create_member( - self.state['userid'], - self.state['password'], - self.state['name'], - self.state['program'], - self.state['email']) - members.register(self.state['userid'], self.state['terms']) - - mailman_result = members.subscribe_to_mailing_list(self.state['userid']) - if mailman_result != 'None': - problem = mailman_result - - elif self.utype == 'clubuser': - members.create_member( - self.state['userid'], - self.state['password'], - self.state['name'], - self.state['program'], - self.state['email'], - club_rep=True) - members.register_nonmember(self.state['userid'], self.state['terms']) - elif self.utype == 'club': - members.create_club(self.state['userid'], self.state['name']) - else: - raise Exception("Internal Error") - except members.InvalidArgument, e: - problem = str(e) - except ldap.LDAPError, e: - problem = str(e) - except members.MemberException, e: - problem = str(e) - except remote.RemoteException, e: - problem = str(e) - - clear_status() - - if problem: - self.headtext.set_text("Failures Occured Adding User") - self.midtext.set_text("The error was:\n\n%s\n\nThe account may be partially added " - "and you may or may not be able to log in. Or perhaps you are not office staff. " - "If this was not expected please contact systems committee." % problem) - return - else: - set_status("Strombola Delivers") - self.headtext.set_text("User Added") - self.midtext.set_text("Congratulations, %s has been added " - "successfully. You should also rebuild the website in " - "order to update the memberlist." - % self.state['userid']) diff --git a/ceo/urwid/positions.py b/ceo/urwid/positions.py deleted file mode 100644 index 6f0f6e0..0000000 --- a/ceo/urwid/positions.py +++ /dev/null @@ -1,97 +0,0 @@ -import urwid -from ceo import members -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -position_data = [ - ('president', 'President'), - ('vice-president', 'Vice-president'), - ('treasurer', 'Treasurer'), - ('secretary', 'Secretary'), - ('sysadmin', 'System Administrator'), - ('cro', 'Chief Returning Officer'), - ('librarian', 'Librarian'), - ('imapd', 'Imapd'), - ('webmaster', 'Web Master'), - ('offsck', 'Office Manager'), -] - -class IntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Managing Positions" ), - urwid.Divider(), - urwid.Text( "Enter a username for each position. If a position is " - "held by multiple people, enter a comma-separated " - "list of usernames. If a position is held by nobody " - "leave the username blank." ), - ] - def focusable(self): - return False - -class InfoPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Positions" ), - urwid.Divider(), - ] - positions = members.list_positions() - self.position_widgets = {} - for (position, text) in position_data: - widget = LdapWordEdit(csclub_uri, csclub_base, 'uid', - "%s: " % text) - if position in positions: - widget.set_edit_text(','.join(positions[position].keys())) - else: - widget.set_edit_text('') - self.position_widgets[position] = widget - self.widgets.append(widget) - - def parse(self, entry): - if len(entry) == 0: - return [] - return entry.split(',') - - def check(self): - self.state['positions'] = {} - for (position, widget) in self.position_widgets.iteritems(): - self.state['positions'][position] = \ - self.parse(widget.get_edit_text()) - for p in self.state['positions'][position]: - if members.get(p) == None: - self.focus_widget(widget) - set_status( "Invalid username: '%s'" % p ) - return True - clear_status() - -class EndPage(WizardPanel): - def init_widgets(self): - old = members.list_positions() - self.headtext = urwid.Text("") - self.midtext = urwid.Text("") - - self.widgets = [ - self.headtext, - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def activate(self): - failed = [] - for (position, info) in self.state['positions'].iteritems(): - try: - members.set_position(position, info) - except ldap.LDAPError: - failed.append(position) - if len(failed) == 0: - self.headtext.set_text("Positions Updated") - self.midtext.set_text("Congratulations, positions have been " - "updated. You should rebuild the website in order to update " - "the Positions page.") - else: - self.headtext.set_text("Positions Results") - self.midtext.set_text("Failed to update the following positions: " - "%s." % join(failed)) - def check(self): - pop_window() diff --git a/ceo/urwid/renew.py b/ceo/urwid/renew.py deleted file mode 100644 index 2734cef..0000000 --- a/ceo/urwid/renew.py +++ /dev/null @@ -1,240 +0,0 @@ -import urwid, ldap -from ceo import members, terms, ldapi -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -class IntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Renewing Membership" ), - urwid.Divider(), - urwid.Text( "CSC membership is $2.00 per term. You may pre-register " - "for future terms if desired." ) - ] - def focusable(self): - return False - -class ClubUserIntroPage(IntroPage): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Renewing Club User Account" ), - urwid.Divider(), - urwid.Text( "In order for clubs to maintain websites hosted by " - "the Computer Science Club, they need access to our " - "machines. We grant accounts to club users at no charge " - "in order to provide this access. Registering a user " - "in this way grants one term of free access to our " - "machines, without any other membership privileges " - "(they can't vote, hold office, etc). If such a user " - "decides to join, use the Renew Membership option." ) - ] - -class UserPage(WizardPanel): - def init_widgets(self): - self.userid = LdapWordEdit(csclub_uri, csclub_base, 'uid', - "Username: ") - - self.widgets = [ - urwid.Text( "Member Information" ), - urwid.Divider(), - self.userid, - ] - def check(self): - self.state['userid'] = self.userid.get_edit_text() - self.state['member'] = None - if self.state['userid']: - self.state['member'] = members.get(self.userid.get_edit_text()) - if not self.state['member']: - set_status("Member not found") - self.focus_widget(self.userid) - return True - -class EmailPage(WizardPanel): - def init_widgets(self): - self.email = SingleEdit("Email: ") - - self.widgets = [ - urwid.Text( "Mail Forwarding" ), - urwid.Divider(), - urwid.Text("Please ensure the forwarding address for your CSC " - "email is up to date. You may leave this blank if you do not " - "want your CSC email forwarded, and intend to log in " - "regularly to check it."), - urwid.Divider(), - urwid.Text("Warning: Changing this overwrites ~/.forward"), - urwid.Divider(), - self.email, - ] - def activate(self): - cfwd = members.current_email(self.state['userid']) - if cfwd: - self.state['old_forward'] = cfwd - else: - self.state['old_forward'] = '' - self.email.set_edit_text(self.state['old_forward']) - def check(self): - fwd = self.email.get_edit_text().strip().lower() - if fwd: - msg = members.check_email(fwd) - if msg: - set_status(msg) - return True - if fwd == '%s@csclub.uwaterloo.ca' % self.state['userid']: - set_status('You cannot forward your address to itself. Leave it blank to disable forwarding.') - return True - self.state['new_forward'] = fwd - -class EmailDonePage(WizardPanel): - def init_widgets(self): - self.status = urwid.Text("") - self.widgets = [ - urwid.Text("Mail Forwarding"), - urwid.Divider(), - self.status, - ] - def focusable(self): - return False - def activate(self): - if self.state['old_forward'] == self.state['new_forward']: - if self.state['old_forward']: - self.status.set_text( - 'You have chosen to leave your forwarding address ' - 'as %s. Make sure to check this email for updates ' - 'from the CSC.' % self.state['old_forward']) - else: - self.status.set_text( - 'You have chosen not to set a forwarding address. ' - 'Please check your CSC email regularly (via IMAP, POP, or locally) ' - 'for updates from the CSC.' - '\n\n' - 'Note: If you do have a ~/.forward, we were not able to read it or ' - 'it was not a single email address. Do not worry, we have left it ' - 'as is.') - else: - try: - msg = members.change_email(self.state['userid'], self.state['new_forward']) - if msg: - self.status.set_text("Errors occured updating your forwarding address:" - "\n\n%s" % msg) - else: - if self.state['new_forward']: - self.status.set_text( - 'Your email forwarding address has been successfully set ' - 'to %s. Test it out by emailing %s@csclub.uwaterloo.ca and ' - 'making sure you receive it at your forwarding address.' - % (self.state['new_forward'], self.state['userid'])) - else: - self.status.set_text( - 'Your email forwarding address has been successfully cleared. ' - 'Please check your CSC email regularly (via IMAP, POP, or locally) ' - 'for updates from the CSC.') - except Exception, e: - self.status.set_text( - 'An exception occured updating your email:\n\n%s' % e) - -class TermPage(WizardPanel): - def __init__(self, state, utype='member'): - self.utype = utype - WizardPanel.__init__(self, state) - def init_widgets(self): - self.start = SingleEdit("Start: ") - self.count = SingleIntEdit("Count: ") - - self.widgets = [ - urwid.Text( "Terms to Register" ), - urwid.Divider(), - self.start, - self.count, - ] - def activate(self): - if not self.start.get_edit_text(): - self.terms = self.state['member'].get('term', []) - self.nmterms = self.state['member'].get('nonMemberTerm', []) - - if self.utype == 'member': - self.start.set_edit_text( terms.next_unregistered( self.terms ) ) - else: - self.start.set_edit_text( terms.next_unregistered( self.terms + self.nmterms ) ) - - self.count.set_edit_text( "1" ) - def check(self): - try: - self.state['terms'] = terms.interval( self.start.get_edit_text(), self.count.value() ) - except Exception, e: - self.focus_widget( self.start ) - set_status( "Invalid start term" ) - return True - for term in self.state['terms']: - if self.utype == 'member': - already = term in self.terms - else: - already = term in self.terms or term in self.nmterms - - if already: - self.focus_widget( self.start ) - set_status( "Already registered for " + term ) - return True - if len(self.state['terms']) == 0: - self.focus_widget(self.count) - set_status( "Registering for zero terms?" ) - return True - -class PayPage(WizardPanel): - def init_widgets(self): - self.midtext = urwid.Text("") - - self.widgets = [ - urwid.Text("Membership Fee"), - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def activate(self): - regterms = self.state['terms'] - plural = "term" - if len(self.state['terms']) > 1: - plural = "terms" - self.midtext.set_text("You are registering for %d %s, and owe the " - "Computer Science Club $%d.00 in membership fees. " - "Please deposit the money in the safe before " - "continuing. " % ( len(regterms), plural, len(regterms * 2))) - -class EndPage(WizardPanel): - def __init__(self, state, utype='member'): - self.utype = utype - WizardPanel.__init__(self, state) - def init_widgets(self): - self.headtext = urwid.Text("") - self.midtext = urwid.Text("") - - self.widgets = [ - self.headtext, - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def activate(self): - problem = None - try: - self.headtext.set_text("Registration Succeeded") - if self.utype == 'member': - members.register( self.state['userid'], self.state['terms'] ) - self.midtext.set_text("The member has been registered for the following " - "terms: " + ", ".join(self.state['terms']) + ".") - else: - members.register_nonmember( self.state['userid'], self.state['terms'] ) - self.midtext.set_text("The club user has been registered for the following " - "terms: " + ", ".join(self.state['terms']) + ".") - except ldap.LDAPError, e: - problem = ldapi.format_ldaperror(e) - except members.MemberException, e: - problem = str(e) - if problem: - self.headtext.set_text("Failed to Register") - self.midtext.set_text("You may refund any fees paid or retry. " - "The error was:\n\n%s" % problem) - - def check(self): - pop_window() diff --git a/ceo/urwid/search.py b/ceo/urwid/search.py deleted file mode 100644 index 3fb57b7..0000000 --- a/ceo/urwid/search.py +++ /dev/null @@ -1,83 +0,0 @@ -import urwid -from ceo import members, terms -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -class TermPage(WizardPanel): - def init_widgets(self): - self.term = SingleEdit("Term: ") - - self.widgets = [ - urwid.Text( "Terms Members" ), - urwid.Divider(), - self.term, - ] - def check(self): - try: - self.state['term'] = self.term.get_edit_text() - terms.parse( self.state['term'] ) - except: - self.focus_widget( self.term ) - set_status( "Invalid term" ) - return True - mlist = members.list_term( self.state['term'] ).values() - pop_window() - member_list( mlist ) - -class NamePage(WizardPanel): - def init_widgets(self): - self.name = SingleEdit("Name: ") - - self.widgets = [ - urwid.Text( "Members by Name" ), - urwid.Divider(), - self.name, - ] - def check(self): - self.state['name'] = self.name.get_edit_text() - if not self.state['name']: - self.focus_widget( self.name ) - set_status( "Invalid name" ) - return True - mlist = members.list_name( self.state['name'] ).values() - pop_window() - member_list( mlist ) - -class GroupPage(WizardPanel): - def init_widgets(self): - self.group = SingleEdit("Group: ") - - self.widgets = [ - urwid.Text( "Members by Group" ), - urwid.Divider(), - self.group, - ] - def check(self): - self.state['group'] = self.group.get_edit_text() - if not self.state['group']: - self.focus_widget( self.group ) - set_status( "Invalid group" ) - return True - mlist = members.list_group( self.state['group'] ).values() - pop_window() - member_list( mlist ) - -def member_list(mlist): - mlist = list(mlist) - mlist.sort( lambda x, y: cmp(x['uid'], y['uid']) ) - buf = '' - for member in mlist: - if 'uid' in member: - uid = member['uid'][0] - else: - uid = None - if 'program' in member: - program = member['program'][0] - else: - program = None - attrs = ( uid, member['cn'][0], program ) - buf += "%10s %30s\n%41s\n\n" % attrs - set_status("Press escape to return to the menu") - push_window(urwid.ListBox([urwid.Text(buf)])) - - diff --git a/ceo/urwid/shell.py b/ceo/urwid/shell.py deleted file mode 100644 index d4be161..0000000 --- a/ceo/urwid/shell.py +++ /dev/null @@ -1,95 +0,0 @@ -import urwid, ldap, pwd, os -from ceo import members, terms, ldapi -from ceo.urwid.widgets import * -from ceo.urwid.window import * - -class IntroPage(WizardPanel): - def init_widgets(self): - self.widgets = [ - urwid.Text( "Changing Login Shell" ), - urwid.Divider(), - urwid.Text( "You can change your shell here. Request more shells " - "by emailing systems-committee." ) - ] - def focusable(self): - return False - -class YouPage(WizardPanel): - def init_widgets(self): - you = pwd.getpwuid(os.getuid()).pw_name - self.userid = LdapWordEdit(csclub_uri, csclub_base, 'uid', - "Username: ", you) - - self.widgets = [ - urwid.Text( "Member Information" ), - urwid.Divider(), - self.userid, - ] - def check(self): - self.state['userid'] = self.userid.get_edit_text() - self.state['member'] = None - if self.state['userid']: - self.state['member'] = members.get(self.userid.get_edit_text()) - if not self.state['member']: - set_status("Member not found") - self.focus_widget(self.userid) - return True - -class ShellPage(WizardPanel): - def init_widgets(self): - self.midtext = urwid.Text("") - - self.widgets = [ - urwid.Text("Choose a Shell"), - urwid.Divider(), - ] - - def set_shell(radio_button, new_state, shell): - if new_state: - self.state['shell'] = shell - - radio_group = [] - self.shells = members.get_shells() - self.shellw = [ urwid.RadioButton(radio_group, shell, - on_state_change=set_shell, user_data=shell) - for shell in self.shells ] - - self.widgets.extend(self.shellw) - def set_shell(self, shell): - i = self.shells.index(shell) - self.shellw[i].set_state(True) - def focusable(self): - return True - def activate(self): - self.set_shell(self.state['member']['loginShell'][0]) - -class EndPage(WizardPanel): - def init_widgets(self): - self.headtext = urwid.Text("") - self.midtext = urwid.Text("") - - self.widgets = [ - self.headtext, - urwid.Divider(), - self.midtext, - ] - def focusable(self): - return False - def activate(self): - problem = None - try: - user, shell = self.state['userid'], self.state['shell'] - members.set_shell(user, shell) - self.headtext.set_text("Login Shell Changed") - self.midtext.set_text("The shell for %s has been changed to %s." - % (user, shell)) - except ldap.LDAPError, e: - problem = ldapi.format_ldaperror(e) - except members.MemberException, e: - problem = str(e) - if problem: - self.headtext.set_text("Failed to Change Shell") - self.midtext.set_text("Perhaps you don't have permission to change %s's shell? " - "The error was:\n\n%s" % (user, problem)) - def check(self): - pop_window() diff --git a/ceo/urwid/widgets.py b/ceo/urwid/widgets.py deleted file mode 100644 index fd17172..0000000 --- a/ceo/urwid/widgets.py +++ /dev/null @@ -1,247 +0,0 @@ -import urwid, ldap, sys -from ceo.urwid.window import raise_back, push_window -import ceo.ldapi as ldapi - -#Todo: kill ButtonText because no one uses it except one place and we can probably do that better anyway - -csclub_uri = "ldap://ldap1.csclub.uwaterloo.ca/ ldap://ldap2.csclub.uwaterloo.ca" -csclub_base = "dc=csclub,dc=uwaterloo,dc=ca" - -def make_menu(items): - items = [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ] - return ShortcutListBox(items) - -def labelled_menu(itemses): - widgets = [] - for label, items in itemses: - if label: - widgets.append(urwid.Text(label)) - widgets += (urwid.AttrWrap(ButtonText(cb, data, txt), 'menu', 'selected') for (txt, cb, data) in items) - widgets.append(urwid.Divider()) - widgets.pop() - return ShortcutListBox(widgets) - -def push_wizard(name, pages, dimensions=(50, 10)): - state = {} - wiz = Wizard() - for page in pages: - if type(page) != tuple: - page = (page, ) - wiz.add_panel( page[0](state, *page[1:]) ) - push_window( urwid.Filler( urwid.Padding( - urwid.LineBox(wiz), 'center', dimensions[0]), - 'middle', dimensions[1] ), name ) - -class ButtonText(urwid.Text): - def __init__(self, callback, data, *args, **kwargs): - self.callback = callback - self.data = data - urwid.Text.__init__(self, *args, **kwargs) - def selectable(self): - return True - def keypress(self, size, key): - if key == 'enter' and self.callback: - self.callback(self.data) - else: - return key - -#DONTUSE -class CaptionedText(urwid.Text): - def __init__(self, caption, *args, **kwargs): - self.caption = caption - urwid.Text.__init__(self, *args, **kwargs) - def render(self, *args, **kwargs): - self.set_text(self.caption + self.get_text()[0]) - urwid.Text.render(*args, **kwargs) - -class SingleEdit(urwid.Edit): - def keypress(self, size, key): - key_mappings = { - 'enter': 'down', - 'tab': 'down', - 'shift tab': 'up', - 'ctrl a': 'home', - 'ctrl e': 'end' - } - - if key in key_mappings: - return urwid.Edit.keypress(self, size, key_mappings[key]) - else: - return urwid.Edit.keypress(self, size, key) - -class SingleIntEdit(urwid.IntEdit): - def keypress(self, size, key): - if key == 'enter': - return urwid.Edit.keypress(self, size, 'down') - else: - return urwid.Edit.keypress(self, size, key) - -class WordEdit(SingleEdit): - def valid_char(self, ch): - return urwid.Edit.valid_char(self, ch) and ch != ' ' - -class LdapWordEdit(WordEdit): - ldap = None - index = None - - def __init__(self, uri, base, attr, *args): - try: - self.ldap = ldap.initialize(uri) - self.ldap.simple_bind_s("", "") - except ldap.LDAPError: - return WordEdit.__init__(self, *args) - self.base = base - self.attr = ldapi.escape(attr) - return WordEdit.__init__(self, *args) - - def keypress(self, size, key): - if (key == 'tab' or key == 'shift tab') and self.ldap != None: - if self.index != None: - if key == 'tab': - self.index = (self.index + 1) % len(self.choices) - elif key == 'shift tab': - self.index = (self.index - 1) % len(self.choices) - text = self.choices[self.index] - self.set_edit_text(text) - self.set_edit_pos(len(text)) - else: - try: - text = self.get_edit_text() - search = ldapi.escape(text) - matches = self.ldap.search_s(self.base, - ldap.SCOPE_SUBTREE, '(%s=%s*)' % (self.attr, search)) - self.choices = [ text ] - for match in matches: - (_, attrs) = match - self.choices += attrs['uid'] - self.choices.sort() - self.index = 0 - self.keypress(size, key) - except ldap.LDAPError, e: - pass - else: - self.index = None - return WordEdit.keypress(self, size, key) - -class LdapFilterWordEdit(LdapWordEdit): - def __init__(self, uri, base, attr, map, *args): - LdapWordEdit.__init__(self, uri, base, attr, *args) - self.map = map - def keypress(self, size, key): - if self.ldap != None: - if key == 'enter' or key == 'down' or key == 'up': - search = ldapi.escape(self.get_edit_text()) - try: - matches = self.ldap.search_s(self.base, - ldap.SCOPE_SUBTREE, '(%s=%s)' % (self.attr, search)) - if len(matches) > 0: - (_, attrs) = matches[0] - for (k, v) in self.map.items(): - if attrs.has_key(k) and len(attrs[k]) > 0: - v.set_edit_text(attrs[k][0]) - except ldap.LDAPError: - pass - return LdapWordEdit.keypress(self, size, key) - -class PassEdit(SingleEdit): - def get_text(self): - text = urwid.Edit.get_text(self) - return (self.caption + " " * len(self.get_edit_text()), text[1]) - -class EnhancedButton(urwid.Button): - def keypress(self, size, key): - if key == 'tab': - return urwid.Button.keypress(self, size, 'down') - elif key == 'shift tab': - return urwid.Button.keypress(self, size, 'up') - else: - return urwid.Button.keypress(self, size, key) - -class DumbColumns(urwid.Columns): - """Dumb columns widget - - The normal one tries to focus the "nearest" widget to the cursor. - This makes the Back button default instead of the Next button. - """ - def move_cursor_to_coords(self, size, col, row): - pass - -class Wizard(urwid.WidgetWrap): - def __init__(self): - self.selected = None - self.panels = [] - - self.panelwrap = urwid.WidgetWrap( urwid.SolidFill() ) - self.back = EnhancedButton("Back", self.back) - self.next = EnhancedButton("Next", self.next) - self.buttons = DumbColumns( [ self.back, self.next ], dividechars=3, focus_column=1 ) - pad = urwid.Padding( self.buttons, ('fixed right', 2), 19 ) - self.pile = urwid.Pile( [self.panelwrap, ('flow', pad)], 0 ) - urwid.WidgetWrap.__init__(self, self.pile) - - def add_panel(self, panel): - self.panels.append( panel ) - if len(self.panels) == 1: - self.select(0) - - def select(self, panelno, set_focus=True): - if 0 <= panelno < len(self.panels): - self.selected = panelno - self.panelwrap._w = self.panels[panelno] - self.panelwrap._invalidate() - self.panels[panelno].activate() - - if set_focus: - if self.panels[panelno].focusable(): - self.pile.set_focus( 0 ) - else: - self.pile.set_focus( 1 ) - - def next(self, *args, **kwargs): - if self.panels[self.selected].check(): - self.select( self.selected ) - return - self.select(self.selected + 1) - - def back(self, *args, **kwargs): - if self.selected == 0: - raise_back() - self.select(self.selected - 1, False) - -class WizardPanel(urwid.WidgetWrap): - def __init__(self, state): - self.state = state - self.init_widgets() - self.box = urwid.ListBox( urwid.SimpleListWalker( self.widgets ) ) - urwid.WidgetWrap.__init__( self, self.box ) - def init_widgets(self): - self.widgets = [] - def focus_widget(self, widget): - self.box.set_focus( self.widgets.index( widget ) ) - def focusable(self): - return True - def check(self): - return - def activate(self): - return - -# assumes that a SimpleListWalker containing -# urwid.Text or subclass is used -class ShortcutListBox(urwid.ListBox): - def keypress(self, size, key): - # only process single letters; pass all else to super - if len(key) == 1 and key.isalpha(): - next = self.get_focus()[1] + 1 - shifted_contents = self.body.contents[next:] + self.body.contents[:next] - - # find the next item matching the letter requested - try: - new_focus = (i for i,w in enumerate(shifted_contents) - if w.selectable() and w.text[0].upper() == key.upper()).next() - new_focus = (new_focus + next) % len(self.body.contents) - self.set_focus(new_focus) - except: - # ring the bell if it isn't found - sys.stdout.write('\a') - else: - urwid.ListBox.keypress(self, size, key) diff --git a/ceo/urwid/window.py b/ceo/urwid/window.py deleted file mode 100644 index cc13a60..0000000 --- a/ceo/urwid/window.py +++ /dev/null @@ -1,80 +0,0 @@ -import urwid - -window_stack = [] -window_names = [] - -header = urwid.Text( "" ) -footer = urwid.Text( "" ) - -ui = urwid.curses_display.Screen() - -ui.register_palette([ - # name, foreground, background, mono - ('banner', 'light gray', 'default', None), - ('menu', 'light gray', 'default', 'bold'), - ('selected', 'black', 'light gray', 'bold'), -]) - -top = urwid.Frame( urwid.SolidFill(), header, footer ) - -def push_window( frame, name=None ): - window_stack.append( frame ) - window_names.append( name ) - update_top() - -def pop_window(): - if len(window_stack) == 1: - return False - window_stack.pop() - window_names.pop() - update_top() - clear_status() - return True - -def update_top(): - names = [ n for n in window_names if n ] - header.set_text(" - ".join( names ) + "\n") - top.set_body( window_stack[-1] ) - -def set_status(message): - footer.set_text(message) - -def clear_status(): - footer.set_text("") - -class Abort(Exception): - pass - -class Back(Exception): - pass - -def raise_abort(*args, **kwargs): - raise Abort() - -def raise_back(*args, **kwarg): - raise Back() - -def redraw(): - cols, rows = ui.get_cols_rows() - canvas = top.render( (cols, rows), focus=True ) - ui.draw_screen( (cols, rows), canvas ) - return cols, rows - -def event_loop(ui): - while True: - try: - cols, rows = redraw() - - keys = ui.get_input() - for k in keys: - if k == "esc": - if not pop_window(): - break - elif k == "window resize": - (cols, rows) = ui.get_cols_rows() - else: - top.keypress( (cols, rows), k ) - except Back: - pop_window() - except (Abort, KeyboardInterrupt): - return diff --git a/ceo/uwldap.py b/ceo/uwldap.py deleted file mode 100644 index 37aabd8..0000000 --- a/ceo/uwldap.py +++ /dev/null @@ -1,8 +0,0 @@ -def uri(): - return "ldap://uwldap.uwaterloo.ca/" - -def base(): - return "dc=uwaterloo,dc=ca" - -def domain(): - return 'uwaterloo.ca' diff --git a/ceo_common/__init__.py b/ceo_common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ceo_common/interfaces/IConfig.py b/ceo_common/interfaces/IConfig.py new file mode 100644 index 0000000..fcb69e9 --- /dev/null +++ b/ceo_common/interfaces/IConfig.py @@ -0,0 +1,8 @@ +from zope.interface import Interface + + +class IConfig(Interface): + """Represents a config store.""" + + def get(key: str) -> str: + """Get the config value for the given key.""" diff --git a/ceo_common/interfaces/IGroup.py b/ceo_common/interfaces/IGroup.py new file mode 100644 index 0000000..c2daa88 --- /dev/null +++ b/ceo_common/interfaces/IGroup.py @@ -0,0 +1,39 @@ +from typing import Dict, List + +from zope.interface import Interface, Attribute + +from .IUser import IUser + + +class IGroup(Interface): + """Represents a Unix group.""" + + cn = Attribute('common name') + gid_number = Attribute('gid number') + unique_members = Attribute('DNs of group members') + dn = Attribute('distinguished name') + + def add_to_ldap(): + """Add a new record to LDAP for this group.""" + + def add_member(username: str): + """Add the member to this group in LDAP.""" + + def remove_member(username: str): + """Remove the member from this group in LDAP.""" + + def get_members() -> List[IUser]: + """Get a list of the members in this group.""" + + def serialize_for_modlist() -> Dict: + """ + Serialize this group into a dict to be passed to + ldap.modlist.addModlist(). + """ + + # static method + def deserialize_from_dict(data: Dict): + """Deserialize this group from a dict returned by ldap.search_s(). + + :returns: IGroup + """ diff --git a/ceo_common/interfaces/IKerberosService.py b/ceo_common/interfaces/IKerberosService.py new file mode 100644 index 0000000..43811b3 --- /dev/null +++ b/ceo_common/interfaces/IKerberosService.py @@ -0,0 +1,14 @@ +from zope.interface import Interface + + +class IKerberosService(Interface): + """A utility wrapper around kinit/kadmin.""" + + def kinit(): + """Acquire and cache a new TGT.""" + + def addprinc(principal: str, password: str): + """Add a new principal with the specified password.""" + + def change_password(principal: str, password: str): + """Set and expire the principal's password.""" diff --git a/ceo_common/interfaces/ILDAPService.py b/ceo_common/interfaces/ILDAPService.py new file mode 100644 index 0000000..af3c092 --- /dev/null +++ b/ceo_common/interfaces/ILDAPService.py @@ -0,0 +1,32 @@ +from zope.interface import Interface + +from .IUser import IUser +from .IGroup import IGroup + + +class ILDAPService(Interface): + """An interface to the LDAP database.""" + + def get_user(username: str) -> IUser: + """Retrieve the user with the given username.""" + + def save_user(user: IUser) -> IUser: + """ + Save the user in the database. + A new UID and GID will be generated and returned in the new user. + """ + + def get_group(cn: str, is_club: bool = False) -> IGroup: + """Retrieve the group with the given cn (Unix group name).""" + + def save_group(group: IGroup) -> IGroup: + """ + Save the group in the database. + The GID will not be changed and must be valid. + """ + + def modify_user(old_user: IUser, new_user: IUser): + """Replace old_user with new_user.""" + + def modify_group(old_group: IGroup, new_group: IGroup): + """Replace old_group with new_group.""" diff --git a/ceo_common/interfaces/IUser.py b/ceo_common/interfaces/IUser.py new file mode 100644 index 0000000..238c330 --- /dev/null +++ b/ceo_common/interfaces/IUser.py @@ -0,0 +1,65 @@ +from typing import List, Dict + +from zope.interface import Interface, Attribute + + +class IUser(Interface): + """Represents a Unix user.""" + + # LDAP attributes + uid = Attribute('user identifier') + cn = Attribute('common name') + login_shell = Attribute('login shell') + uid_number = Attribute('uid number') + gid_number = Attribute('gid number') + home_directory = Attribute('home directory') + program = Attribute('academic program') + position = Attribute('executive position') + terms = Attribute('list of terms for which this person was a member') + non_member_terms = Attribute('list of terms for which this person was ' + 'a club rep') + mail_local_addresses = Attribute('email aliases') + dn = Attribute('distinguished name') + + # Non-LDAP attributes + forwarding_addresses = Attribute('list of email forwarding addresses') + + def is_club() -> bool: + """ + Returns True if this is the Unix user for a club. + Returns False if this is the Unix user for a member. + """ + + def add_to_ldap(): + """Add a new record to LDAP for this user.""" + + def add_to_kerberos(password: str): + """Add a new Kerberos principal for this user.""" + + def add_terms(terms: List[str]): + """Add member terms for this user.""" + + def add_non_member_terms(terms: List[str]): + """Add non-member terms for this user.""" + + def add_position(position: str): + """Add a position to this user.""" + + def remove_position(position: str): + """Remove a position from this user.""" + + def change_password(password: str): + """Replace the user's password.""" + + def serialize_for_modlist() -> Dict: + """ + Serialize this user into a dict to be passed to + ldap.modlist.addModlist(). + """ + + # static method + def deserialize_from_dict(data: Dict): + """Deserialize this user from a dict returned by ldap.search_s(). + + :returns: IUser + """ diff --git a/ceo_common/interfaces/__init__.py b/ceo_common/interfaces/__init__.py new file mode 100644 index 0000000..98243da --- /dev/null +++ b/ceo_common/interfaces/__init__.py @@ -0,0 +1,5 @@ +from .IKerberosService import IKerberosService +from .IConfig import IConfig +from .IUser import IUser +from .ILDAPService import ILDAPService +from .IGroup import IGroup diff --git a/ceo_common/model/Config.py b/ceo_common/model/Config.py new file mode 100644 index 0000000..0993408 --- /dev/null +++ b/ceo_common/model/Config.py @@ -0,0 +1,24 @@ +from zope.interface import implementer + +from ceo_common.interfaces import IConfig + + +@implementer(IConfig) +class Config: + # TODO: read from a config file + _domain = 'csclub.internal' + _ldap_base = ','.join(['dc=' + dc for dc in _domain.split('.')]) + _config = { + 'ldap_admin_principal': 'ceod/admin', + 'ldap_server_url': 'ldap://ldap-master.' + _domain, + 'ldap_users_base': 'ou=People,' + _ldap_base, + 'ldap_groups_base': 'ou=Group,' + _ldap_base, + 'ldap_sasl_realm': _domain.upper(), + 'member_min_id': 20001, + 'member_max_id': 29999, + 'club_min_id': 30001, + 'club_max_id': 39999, + } + + def get(self, key: str) -> str: + return self._config[key] diff --git a/ceo_common/model/__init__.py b/ceo_common/model/__init__.py new file mode 100644 index 0000000..385a327 --- /dev/null +++ b/ceo_common/model/__init__.py @@ -0,0 +1 @@ +from .Config import Config diff --git a/ceod/__init__.py b/ceod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ceod/model/Group.py b/ceod/model/Group.py new file mode 100644 index 0000000..421691e --- /dev/null +++ b/ceod/model/Group.py @@ -0,0 +1,84 @@ +import copy +from typing import List, Dict, Union + +from zope import component +from zope.interface import implementer + +from .utils import strings_to_bytes, bytes_to_strings, dn_to_uid +from ceo_common.interfaces import ILDAPService, IGroup, IConfig, IUser + + +@implementer(IGroup) +class Group: + def __init__( + self, cn: str, gid_number: int, + members: Union[List[str], None] = None, + ): + self.cn = cn + self.gid_number = gid_number + # this is a list of the DNs of the members in this group + self.member_DNs = members or [] + + cfg = component.getUtility(IConfig) + self.dn = f'cn={cn},{cfg.get("ldap_groups_base")}' + self.ldap_users_base = cfg.get('ldap_users_base') + self.ldap_srv = component.getUtility(ILDAPService) + + def __repr__(self) -> str: + lines = [ + 'dn: ' + self.dn, + 'cn: ' + self.cn, + 'gidNumber: ' + str(self.gid_number), + 'objectClass: top', + 'objectClass: group', + 'objectClass: posixGroup', + ] + for member_dn in self.member_DNs: + lines.append('uniqueMember: ' + member_dn) + return '\n'.join(lines) + + def add_to_ldap(self): + self.ldap_srv.add_group(self) + + def serialize_for_modlist(self) -> Dict: + data = { + 'cn': [self.cn], + 'gidNumber': [str(self.gid_number)], + 'objectClass': [ + 'top', + 'group', + 'posixGroup', + ], + } + if self.member_DNs: + data['uniqueMember'] = self.member_DNs + return strings_to_bytes(data) + + @staticmethod + def deserialize_from_dict(data: Dict) -> IGroup: + data = bytes_to_strings(data) + return Group( + cn=data['cn'][0], + gid_number=int(data['gidNumber'][0]), + members=data.get('uniqueMember'), + ) + + def add_member(self, username: str): + new_group = copy.copy(self) + new_group.member_DNs = self.member_DNs.copy() + new_group.member_DNs.append(f'uid={username},{self.ldap_users_base}') + self.ldap_srv.modify_group(self, new_group) + self.member_DNs = new_group.member_DNs + + def remove_member(self, username: str): + new_group = copy.copy(self) + new_group.member_DNs = self.member_DNs.copy() + new_group.member_DNs.remove(f'uid={username},{self.ldap_users_base}') + self.ldap_srv.modify_group(self, new_group) + self.member_DNs = new_group.member_DNs + + def get_members(self) -> List[IUser]: + members = [] + for dn in self.member_DNs: + members.append(self.ldap_srv.get_user(dn_to_uid(dn))) + return members diff --git a/ceod/model/KerberosService.py b/ceod/model/KerberosService.py new file mode 100644 index 0000000..260caee --- /dev/null +++ b/ceod/model/KerberosService.py @@ -0,0 +1,44 @@ +import os +import subprocess + +from zope import component +from zope.interface import implementer + +from ceo_common.interfaces import IKerberosService +from ceo_common.interfaces import IConfig + + +@implementer(IKerberosService) +class KerberosService: + def __init__(self): + cfg = component.getUtility(IConfig) + self.admin_principal = cfg.get('ldap_admin_principal') + + cache_file = '/run/ceod/krb5_cache' + os.makedirs('/run/ceod', exist_ok=True) + os.putenv('KRB5CCNAME', 'FILE:' + cache_file) + self.kinit() + + def kinit(self): + subprocess.run(['kinit', '-k', 'ceod/admin'], check=True) + + def addprinc(self, principal: str, password: str): + subprocess.run([ + 'kadmin', '-k', '-p', self.admin_principal, 'addprinc', + '-pw', password, + '-policy', 'default', + '+needchange', + principal + ], check=True) + + def change_password(self, principal: str, password: str): + subprocess.run([ + 'kadmin', '-k', '-p', self.admin_principal, 'cpw', + '-pw', password, + principal + ], check=True) + subprocess.run([ + 'kadmin', '-k', '-p', self.admin_principal, 'modprinc', + '+needchange', + principal + ], check=True) diff --git a/ceod/model/LDAPService.py b/ceod/model/LDAPService.py new file mode 100644 index 0000000..ffdd6eb --- /dev/null +++ b/ceod/model/LDAPService.py @@ -0,0 +1,139 @@ +import copy +import grp +import pwd + +import ldap +import ldap.modlist +from zope import component +from zope.interface import implementer + +from ceo_common.interfaces import ILDAPService, IKerberosService, IConfig, IUser, IGroup +from .User import User +from .Group import Group + + +class UserNotFoundError: + pass + + +class GroupNotFoundError: + pass + + +@implementer(ILDAPService) +class LDAPService: + def __init__(self): + cfg = component.getUtility(IConfig) + self.ldap_admin_principal = cfg.get('ldap_admin_principal') + self.ldap_server_url = cfg.get('ldap_server_url') + self.ldap_users_base = cfg.get('ldap_users_base') + self.ldap_groups_base = cfg.get('ldap_groups_base') + self.member_min_id = cfg.get('member_min_id') + self.member_max_id = cfg.get('member_max_id') + self.club_min_id = cfg.get('club_min_id') + self.club_max_id = cfg.get('club_max_id') + + def _get_ldap_conn(self, gssapi_bind: bool = True) -> ldap.ldapobject.LDAPObject: + conn = ldap.initialize(self.ldap_server_url) + if gssapi_bind: + self._gssapi_bind(conn) + return conn + + def _gssapi_bind(self, conn: ldap.ldapobject.LDAPObject): + krb_srv = component.getUtility(IKerberosService) + for i in range(2): + try: + conn.sasl_gssapi_bind_s() + return + except ldap.LOCAL_ERROR as err: + if 'Ticket expired' in err.args[0]['info']: + krb_srv.kinit() + continue + raise err + raise Exception('could not perform GSSAPI bind') + + def get_user(self, username: str) -> IUser: + conn = self._get_ldap_conn(False) + base = f'uid={username},{self.ldap_users_base}' + try: + _, result = conn.search_s(base, ldap.SCOPE_BASE)[0] + return User.deserialize_from_dict(result) + except ldap.NO_SUCH_OBJECT: + raise UserNotFoundError() + + def get_group(self, cn: str) -> IGroup: + conn = self._get_ldap_conn(False) + base = f'cn={cn},{self.ldap_groups_base}' + try: + _, result = conn.search_s(base, ldap.SCOPE_BASE)[0] + return Group.deserialize_from_dict(result) + except ldap.NO_SUCH_OBJECT: + raise GroupNotFoundError() + + def _get_next_uid(self, conn: ldap.ldapobject.LDAPObject, min_id: int, max_id: int) -> int: + """Gets the next available UID number between min_id and max_id, inclusive.""" + def uid_exists(uid: int) -> bool: + try: + pwd.getpwuid(uid) + return True + except KeyError: + return False + + def gid_exists(gid: int) -> bool: + try: + grp.getgrgid(gid) + return True + except KeyError: + return False + + def ldap_uid_or_gid_exists(uid: int) -> bool: + results = conn.search_s( + self.ldap_users_base, ldap.SCOPE_ONELEVEL, + f'(|(uidNumber={uid})(gidNumber={uid}))') + return len(results) > 0 + + # TODO: replace this with binary search + for uid in range(min_id, max_id + 1): + if uid_exists(uid) or gid_exists(uid) or ldap_uid_or_gid_exists(uid): + continue + return uid + raise Exception('no UIDs remaining') + + def save_user(self, user: IUser) -> IUser: + if user.is_club(): + min_id, max_id = self.club_min_id, self.club_max_id + else: + min_id, max_id = self.member_min_id, self.member_max_id + conn = self._get_ldap_conn() + uid_number = self._get_next_uid(conn, min_id, max_id) + new_user = copy.deepcopy(user) + new_user.uid_number = uid_number + new_user.gid_number = uid_number + + modlist = ldap.modlist.addModlist(new_user.serialize_for_modlist()) + conn.add_s(new_user.dn, modlist) + return new_user + + def save_group(self, group: IGroup) -> IGroup: + conn = self._get_ldap_conn() + # make sure that the caller initialized the GID number + assert group.gid_number + modlist = ldap.modlist.addModlist(group.serialize_for_modlist()) + conn.add_s(group.dn, modlist) + return group + + def modify_user(self, old_user: IUser, new_user: IUser): + conn = self._get_ldap_conn() + modlist = ldap.modlist.modifyModlist( + old_user.serialize_for_modlist(), + new_user.serialize_for_modlist(), + ) + conn.modify_s(old_user.dn, modlist) + + def modify_group(self, old_group: IGroup, new_group: IGroup): + conn = self._get_ldap_conn() + modlist = ldap.modlist.modifyModlist( + old_group.serialize_for_modlist(), + new_group.serialize_for_modlist(), + ) + conn.modify_s(old_group.dn, modlist) diff --git a/ceod/model/User.py b/ceod/model/User.py new file mode 100644 index 0000000..6b05732 --- /dev/null +++ b/ceod/model/User.py @@ -0,0 +1,169 @@ +import copy +import os +from typing import List, Dict, Union + +from zope import component +from zope.interface import implementer + +from .utils import strings_to_bytes, bytes_to_strings +from ceo_common.interfaces import ILDAPService, IKerberosService, IUser, IConfig + + +@implementer(IUser) +class User: + def __init__( + self, uid: str, cn: str, + program: Union[str, None] = None, + terms: Union[List[str], None] = None, + non_member_terms: Union[List[str], None] = None, + login_shell: str = '/bin/bash', + uid_number: Union[int, None] = None, + gid_number: Union[int, None] = None, + home_directory: Union[str, None] = None, + positions: Union[List[str], None] = None, + mail_local_addresses: Union[List[str], None] = None, + is_club: bool = False, + ): + if not is_club and not terms and not non_member_terms: + raise Exception('terms and non_member_terms cannot both be empty') + self.uid = uid + self.cn = cn + self.program = program + self.terms = terms or [] + self.non_member_terms = non_member_terms or [] + self.login_shell = login_shell + self.uid_number = uid_number + self.gid_number = gid_number + self.home_directory = home_directory or os.path.join('/users', uid) + self.positions = positions or [] + self.mail_local_addresses = mail_local_addresses or [] + self._is_club = is_club + + cfg = component.getUtility(IConfig) + self.ldap_sasl_realm = cfg.get('ldap_sasl_realm') + self.dn = f'uid={uid},{cfg.get("ldap_users_base")}' + self.ldap_srv = component.getUtility(ILDAPService) + self.krb_srv = component.getUtility(IKerberosService) + + @property + def forwarding_addresses(self): + raise NotImplementedError() + + def __repr__(self) -> str: + lines = [ + 'dn: ' + self.dn, + 'cn: ' + self.cn, + 'uid: ' + self.uid, + 'objectClass: top', + 'objectClass: account', + 'objectClass: posixAccount', + 'objectClass: shadowAccount', + 'objectClass: ' + ('club' if self.is_club() else 'member'), + 'uidNumber: ' + str(self.uid_number), + 'gidNumber: ' + str(self.gid_number), + 'loginShell: ' + self.login_shell, + 'homeDirectory: ' + self.home_directory, + ] + if self.program: + lines.append('program: ' + self.program) + for term in self.terms: + lines.append('term: ' + term) + for term in self.non_member_terms: + lines.append('nonMemberTerm: ' + term) + for position in self.positions: + lines.append('position: ' + position) + for address in self.mail_local_addresses: + lines.append('mailLocalAddress: ' + address) + return '\n'.join(lines) + + def is_club(self) -> bool: + return self._is_club + + def add_to_ldap(self): + new_member = self.ldap_srv.save_user(self) + self.uid_number = new_member.uid_number + self.gid_number = new_member.gid_number + + def add_to_kerberos(self, password: str): + self.krb_srv.addprinc(self.uid, password) + + def change_password(self, password: str): + self.krb_srv.change_password(self.uid, password) + + def serialize_for_modlist(self) -> Dict: + data = { + 'cn': [self.cn], + 'loginShell': [self.login_shell], + 'homeDirectory': [self.home_directory], + 'uid': [self.uid], + 'uidNumber': [str(self.uid_number)], + 'gidNumber': [str(self.gid_number)], + 'objectClass': [ + 'top', + 'account', + 'posixAccount', + 'shadowAccount', + ], + } + if self.is_club(): + data['objectClass'].append('club') + else: + data['objectClass'].append('member') + data['userPassword'] = ['{SASL}%s@%s' % (self.uid, self.ldap_sasl_realm)] + if self.program: + data['program'] = [self.program] + if self.terms: + data['term'] = self.terms + if self.non_member_terms: + data['nonMemberTerm'] = self.non_member_terms + if self.positions: + data['position'] = self.positions + if self.mail_local_addresses: + data['mailLocalAddress'] = self.mail_local_addresses + data['objectClass'].append('inetLocalMailRecipient') + return strings_to_bytes(data) + + @staticmethod + def deserialize_from_dict(data: Dict) -> IUser: + data = bytes_to_strings(data) + return User( + uid=data['uid'][0], + cn=data['cn'][0], + program=data.get('program', [None])[0], + terms=data.get('term'), + non_member_terms=data.get('nonUserTerm'), + login_shell=data['loginShell'][0], + uid_number=int(data['uidNumber'][0]), + gid_number=int(data['gidNumber'][0]), + home_directory=data['homeDirectory'][0], + positions=data.get('position'), + mail_local_addresses=data.get('mailLocalAddress', []), + is_club=('club' in data['objectClass']), + ) + + def add_terms(self, terms: List[str]): + new_user = copy.copy(self) + new_user.terms = self.terms.copy() + new_user.terms.extend(terms) + self.ldap_srv.modify_user(self, new_user) + self.terms = new_user.terms + + def add_non_member_terms(self, terms: List[str]): + new_user = copy.copy(self) + new_user.non_member_terms = self.non_member_terms.copy() + new_user.non_member_terms.extend(terms) + self.ldap_srv.modify_user(self, new_user) + self.non_member_terms = new_user.non_member_terms + + def add_position(self, position: str): + new_user = copy.copy(self) + new_user.positions = [*self.positions, position] + self.ldap_srv.modify_user(self, new_user) + self.positions = new_user.positions + + def remove_position(self, position: str): + new_user = copy.copy(self) + new_user.positions = self.positions.copy() + new_user.positions.remove(position) + self.ldap_srv.modify_user(self, new_user) + self.positions = new_user.positions diff --git a/ceod/model/__init__.py b/ceod/model/__init__.py new file mode 100644 index 0000000..6d09fe5 --- /dev/null +++ b/ceod/model/__init__.py @@ -0,0 +1,4 @@ +from .KerberosService import KerberosService +from .LDAPService import LDAPService, UserNotFoundError, GroupNotFoundError +from .User import User +from .Group import Group diff --git a/ceod/model/utils.py b/ceod/model/utils.py new file mode 100644 index 0000000..47d8d16 --- /dev/null +++ b/ceod/model/utils.py @@ -0,0 +1,34 @@ +import base64 +import os +from typing import Dict, List + + +def bytes_to_strings(data: Dict[str, List[bytes]]) -> Dict[str, List[str]]: + """Convert the attribute values from bytes to strings""" + return { + key: [b.decode() for b in val] + for key, val in data.items() + } + + +def strings_to_bytes(data: Dict[str, List[str]]) -> Dict[str, List[bytes]]: + """Convert the attribute values from strings to bytes""" + return { + key: [b.encode() for b in val] + for key, val in data.items() + } + + +def dn_to_uid(dn: str) -> str: + """Extract the UID from an LDAP DN. + + Examples: + dn_to_uid('uid=ctdalek,ou=People,dc=csclub,dc=uwaterloo,dc=ca') + -> 'ctdalek' + """ + return dn.split(',', 1)[0].split('=')[1] + + +def gen_password() -> str: + # good enough + return base64.b64encode(os.urandom(18)).decode()