commit
dfc747f9c6
@ -0,0 +1,24 @@ |
||||
#!/usr/bin/python2.4 -- |
||||
|
||||
import os, sys |
||||
|
||||
safe_environment = ['LOGNAME', 'USERNAME', 'USER', 'HOME', |
||||
'TERM', 'LANG', 'LC_ALL', 'LC_COLLATE', |
||||
'LC_CTYPE', 'LC_MESSAGE', 'LC_MONETARY', |
||||
'LC_NUMERIC', 'LC_TIME', 'UID', 'GID', |
||||
'SSH_CONNECTION', 'SSH_AUTH_SOCK', |
||||
'SSH_CLIENT'] |
||||
|
||||
for key in os.environ.keys(): |
||||
if not key in safe_environment: |
||||
del os.environ[key] |
||||
|
||||
os.environ['PATH'] = '/bin:/usr/bin' |
||||
|
||||
for dir in sys.path[:]: |
||||
if not dir.find('/usr') == 0 or dir.find('/usr/local') == 0: |
||||
while dir in sys.path: |
||||
sys.path.remove(dir) |
||||
|
||||
import csc.apps.legacy.main |
||||
csc.apps.legacy.main.run() |
@ -0,0 +1,6 @@ |
||||
csc (0.1) unstable; urgency=low |
||||
|
||||
* Initial Release. |
||||
|
||||
-- Michael Spang <mspang@uwaterloo.ca> Thu, 28 Dec 2006 04:07:03 -0500 |
||||
|
@ -0,0 +1 @@ |
||||
4 |
@ -0,0 +1,14 @@ |
||||
Source: csc |
||||
Section: admin |
||||
Priority: optional |
||||
Maintainer: Michael Spang <mspang@uwaterloo.ca> |
||||
Build-Depends: debhelper (>= 4.0.0) |
||||
Standards-Version: 3.6.1 |
||||
|
||||
Package: csc |
||||
Architecture: any |
||||
Depends: python, python2.4, python2.4-ldap, python2.4-pygresql, krb5-user, less |
||||
Description: Computer Science Club Administrative Utilities |
||||
This package contains the CSC Electronic Office |
||||
and other Computer Science Club administrative |
||||
programs. |
@ -0,0 +1,31 @@ |
||||
This package was debianized by mspang <mspang@uwaterloo.ca> on |
||||
Thu, 28 Dec 2006 04:07:03 -0500. |
||||
|
||||
Copyright (c) 2006, 2007 Michael Spang |
||||
All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
* Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
* Neither the name of the University of Waterloo Computer Science Club |
||||
nor the names of its contributors may be used to endorse or promote |
||||
products derived from this software without specific prior written |
||||
permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
||||
POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,66 @@ |
||||
#!/usr/bin/make -f |
||||
|
||||
PYTHON := python2.4 |
||||
|
||||
configure: |
||||
|
||||
build: build-stamp |
||||
|
||||
build-stamp: |
||||
mkdir build |
||||
$(CC) -DFULL_PATH=\"/usr/lib/csc/ceo\" -o build/ceo misc/setuid-prog.c |
||||
touch build-stamp |
||||
|
||||
clean: |
||||
dh_testdir |
||||
dh_testroot |
||||
dh_clean |
||||
rm -f build-stamp |
||||
rm -rf build/ |
||||
find py/ -name '*.pyc' -print0 | xargs -0 rm -f |
||||
|
||||
install: build |
||||
dh_testdir |
||||
dh_testroot |
||||
dh_clean -k |
||||
|
||||
# configuration files will contain sensitive information |
||||
chmod 600 etc/csc/* |
||||
|
||||
dh_installdirs etc usr/lib/$(PYTHON)/site-packages usr/share/csc \ |
||||
usr/lib/csc usr/bin |
||||
dh_install -X.svn -X.pyc py/csc usr/lib/$(PYTHON)/site-packages/ |
||||
dh_install -X.svn -X.pyc etc/csc etc/ |
||||
dh_install -X.svn -X.pyc sql/* usr/share/csc/ |
||||
|
||||
dh_install -X.svn -X.pyc bin/ceo usr/lib/csc/ |
||||
dh_install -X.svn -X.pyc build/ceo usr/bin/ |
||||
|
||||
|
||||
binary-indep: build install |
||||
dh_testdir |
||||
dh_testroot |
||||
dh_installchangelogs |
||||
dh_installdocs docs/* |
||||
dh_installexamples |
||||
dh_install |
||||
# dh_installlogrotate |
||||
# dh_installcron |
||||
dh_installman |
||||
dh_link |
||||
dh_strip |
||||
dh_compress |
||||
dh_fixperms |
||||
# dh_perl |
||||
# dh_python |
||||
# dh_makeshlibs |
||||
dh_installdeb |
||||
dh_shlibdeps |
||||
dh_gencontrol |
||||
dh_md5sums |
||||
dh_builddeb |
||||
|
||||
binary: binary-indep binary-arch |
||||
.PHONY: build clean binary-indep binary-arch binary install configure |
||||
|
||||
binary-arch: build install |
@ -0,0 +1,8 @@ |
||||
|
||||
Bugs and Caveats |
||||
================ |
||||
|
||||
CEO: |
||||
- curses does not draw borders/lines correctly in a screen session |
||||
- windows don't always clear properly |
||||
- the menu is not redrawn between windows and therefore a gap may grow there |
@ -0,0 +1,35 @@ |
||||
# $Id: accounts.cf 45 2007-01-02 01:39:10Z mspang $ |
||||
# CSC Accounts Configuration |
||||
|
||||
### Account Options ### |
||||
|
||||
minimum_id = 20000 |
||||
maximum_id = 40000 |
||||
|
||||
shell = "/bin/bash" |
||||
home = "/users" |
||||
gid = 100 |
||||
|
||||
|
||||
### LDAP Configuration ### |
||||
|
||||
server_url = "ldap:///" |
||||
|
||||
users_base = "ou=People,dc=csclub,dc=uwaterloo,dc=ca" |
||||
groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca" |
||||
|
||||
bind_dn = "cn=ceo,dc=csclub,dc=uwaterloo,dc=ca" |
||||
bind_password = "secret" |
||||
|
||||
|
||||
### Kerberos Configuration ### |
||||
|
||||
realm = "CSCLUB.UWATERLOO.CA" |
||||
principal = "ceo/admin@CSCLUB.UWATERLOO.CA" |
||||
keytab = "/etc/csc/ceo.keytab" |
||||
|
||||
|
||||
### Validation Tuning ### |
||||
|
||||
username_regex = "^[a-z][-a-z0-9]*$" |
||||
realname_regex = "^[^,:=]*$" |
@ -0,0 +1,15 @@ |
||||
# $Id: members.cf 45 2007-01-02 01:39:10Z mspang $ |
||||
# CSC Members Configuration |
||||
|
||||
### Database Configuration ### |
||||
|
||||
server = "localhost" |
||||
database = "ceo" |
||||
|
||||
user = "ceo" |
||||
password = "secret" |
||||
|
||||
### Validation Tuning ### |
||||
|
||||
studentid_regex = "^[0-9]{8}$" |
||||
realname_regex = "^[^,:=]*$" |
@ -0,0 +1,176 @@ |
||||
/*
|
||||
Template for a setuid program that calls a script. |
||||
|
||||
The script should be in an unwritable directory and should itself |
||||
be unwritable. In fact all parent directories up to the root |
||||
should be unwritable. The script must not be setuid, that's what |
||||
this program is for. |
||||
|
||||
This is a template program. You need to fill in the name of the |
||||
script that must be executed. This is done by changing the |
||||
definition of FULL_PATH below. |
||||
|
||||
There are also some rules that should be adhered to when writing |
||||
the script itself. |
||||
|
||||
The first and most important rule is to never, ever trust that the |
||||
user of the program will behave properly. Program defensively. |
||||
Check your arguments for reasonableness. If the user is allowed to |
||||
create files, check the names of the files. If the program depends |
||||
on argv[0] for the action it should perform, check it. |
||||
|
||||
Assuming the script is a Bourne shell script, the first line of the |
||||
script should be |
||||
#!/bin/sh - |
||||
The - is important, don't omit it. If you're using esh, the first |
||||
line should be |
||||
#!/usr/local/bin/esh -f |
||||
and for ksh, the first line should be |
||||
#!/usr/local/bin/ksh -p |
||||
The script should then set the variable IFS to the string |
||||
consisting of <space>, <tab>, and <newline>. After this (*not* |
||||
before!), the PATH variable should be set to a reasonable value and |
||||
exported. Do not expect the PATH to have a reasonable value, so do |
||||
not trust the old value of PATH. You should then set the umask of |
||||
the program by calling |
||||
umask 077 # or 022 if you want the files to be readable |
||||
If you plan to change directories, you should either unset CDPATH |
||||
or set it to a good value. Setting CDPATH to just ``.'' (dot) is a |
||||
good idea. |
||||
If, for some reason, you want to use csh, the first line should be |
||||
#!/bin/csh -fb |
||||
You should then set the path variable to something reasonable, |
||||
without trusting the inherited path. Here too, you should set the |
||||
umask using the command |
||||
umask 077 # or 022 if you want the files to be readable |
||||
*/ |
||||
|
||||
#include <unistd.h> |
||||
#include <stdlib.h> |
||||
#include <stdio.h> |
||||
#include <sys/types.h> |
||||
#include <sys/stat.h> |
||||
#include <string.h> |
||||
|
||||
/* CONFIGURATION SECTION */ |
||||
|
||||
#ifndef FULL_PATH /* so that this can be specified from the Makefile */ |
||||
/* Uncomment the following line:
|
||||
#define FULL_PATH "/full/path/of/script" |
||||
* Then comment out the #error line. */ |
||||
#error "You must define FULL_PATH somewhere" |
||||
#endif |
||||
#ifndef UMASK |
||||
#define UMASK 077 |
||||
#endif |
||||
|
||||
/* END OF CONFIGURATION SECTION */ |
||||
|
||||
#if defined(__STDC__) && defined(__sgi) |
||||
#define environ _environ |
||||
#endif |
||||
|
||||
/* don't change def_IFS */ |
||||
char def_IFS[] = "IFS= \t\n"; |
||||
/* you may want to change def_PATH, but you should really change it in */ |
||||
/* your script */ |
||||
#ifdef __sgi |
||||
char def_PATH[] = "PATH=/usr/bsd:/usr/bin:/bin:/usr/local/bin:/usr/sbin"; |
||||
#else |
||||
char def_PATH[] = "PATH=/usr/ucb:/usr/bin:/bin:/usr/local/bin"; |
||||
#endif |
||||
/* don't change def_CDPATH */ |
||||
char def_CDPATH[] = "CDPATH=."; |
||||
/* don't change def_ENV */ |
||||
char def_ENV[] = "ENV=:"; |
||||
|
||||
/*
|
||||
This function changes all environment variables that start with LD_ |
||||
into variables that start with XD_. This is important since we |
||||
don't want the script that is executed to use any funny shared |
||||
libraries. |
||||
|
||||
The other changes to the environment are, strictly speaking, not |
||||
needed here. They can safely be done in the script. They are done |
||||
here because we don't trust the script writer (just like the script |
||||
writer shouldn't trust the user of the script). |
||||
If IFS is set in the environment, set it to space,tab,newline. |
||||
If CDPATH is set in the environment, set it to ``.''. |
||||
Set PATH to a reasonable default. |
||||
*/ |
||||
void |
||||
clean_environ(void) |
||||
{ |
||||
char **p; |
||||
extern char **environ; |
||||
|
||||
for (p = environ; *p; p++) { |
||||
if (strncmp(*p, "LD_", 3) == 0) |
||||
**p = 'X'; |
||||
else if (strncmp(*p, "_RLD", 4) == 0) |
||||
**p = 'X'; |
||||
else if (strncmp(*p, "PYTHON", 6) == 0) |
||||
**p = 'X'; |
||||
else if (strncmp(*p, "IFS=", 4) == 0) |
||||
*p = def_IFS; |
||||
else if (strncmp(*p, "CDPATH=", 7) == 0) |
||||
*p = def_CDPATH; |
||||
else if (strncmp(*p, "ENV=", 4) == 0) |
||||
*p = def_ENV; |
||||
} |
||||
putenv(def_PATH); |
||||
} |
||||
|
||||
int |
||||
main(int argc, char **argv) |
||||
{ |
||||
struct stat statb; |
||||
gid_t egid = getegid(); |
||||
uid_t euid = geteuid(); |
||||
|
||||
/*
|
||||
Sanity check #1. |
||||
This check should be made compile-time, but that's not possible. |
||||
If you're sure that you specified a full path name for FULL_PATH, |
||||
you can omit this check. |
||||
*/ |
||||
if (FULL_PATH[0] != '/') { |
||||
fprintf(stderr, "%s: %s is not a full path name\n", argv[0], |
||||
FULL_PATH); |
||||
fprintf(stderr, "You can only use this wrapper if you\n"); |
||||
fprintf(stderr, "compile it with an absolute path.\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
/*
|
||||
Sanity check #2. |
||||
Check that the owner of the script is equal to either the |
||||
effective uid or the super user. |
||||
*/ |
||||
if (stat(FULL_PATH, &statb) < 0) { |
||||
perror("stat"); |
||||
exit(1); |
||||
} |
||||
if (statb.st_uid != 0 && statb.st_uid != euid) { |
||||
fprintf(stderr, "%s: %s has the wrong owner\n", argv[0], |
||||
FULL_PATH); |
||||
fprintf(stderr, "The script should be owned by root,\n"); |
||||
fprintf(stderr, "and shouldn't be writeable by anyone.\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
if (setregid(egid, egid) < 0) |
||||
perror("setregid"); |
||||
if (setreuid(euid, euid) < 0) |
||||
perror("setreuid"); |
||||
|
||||
clean_environ(); |
||||
|
||||
umask(UMASK); |
||||
|
||||
while (**argv == '-') /* don't let argv[0] start with '-' */ |
||||
(*argv)++; |
||||
execv(FULL_PATH, argv); |
||||
fprintf(stderr, "%s: could not execute the script\n", argv[0]); |
||||
exit(1); |
||||
} |
@ -0,0 +1,19 @@ |
||||
# $Id: __init__.py 24 2006-12-18 20:23:12Z mspang $ |
||||
""" |
||||
PyCSC - CSC Administrative Utilities |
||||
|
||||
Member Management: |
||||
|
||||
ceo - legacy ceo interface |
||||
|
||||
Account Management: |
||||
|
||||
ceo - legacy ceo interface |
||||
|
||||
Modules: |
||||
|
||||
admin - administrative code (member and account management) |
||||
backend - backend interface code |
||||
ui - user interface code |
||||
|
||||
""" |
@ -0,0 +1,232 @@ |
||||
# $Id: accounts.py 44 2006-12-31 07:09:27Z mspang $ |
||||
# UNIX Accounts Module |
||||
import re |
||||
from csc.backend import ldapi, krb |
||||
from csc.lib import read_config |
||||
|
||||
CONFIG_FILE = '/etc/csc/accounts.cf' |
||||
|
||||
cfg = {} |
||||
|
||||
# error constants |
||||
SUCCESS = 0 |
||||
LDAP_EXISTS = 1 |
||||
LDAP_NO_IDS = 2 |
||||
LDAP_NO_USER = 3 |
||||
KRB_EXISTS = 5 |
||||
KRB_NO_USER = 6 |
||||
BAD_USERNAME = 8 |
||||
BAD_REALNAME = 9 |
||||
|
||||
# error messages |
||||
errors = [ "Success", "LDAP: entry exists", |
||||
"LDAP: no user ids available", "LDAP: no such entry", |
||||
"KRB: principal exists", "KRB: no such principal", |
||||
"Invalid username", "Invalid real name"] |
||||
|
||||
|
||||
class AccountException(Exception): |
||||
"""Exception class for account-related errors.""" |
||||
|
||||
|
||||
def load_configuration(): |
||||
"""Load Accounts Configuration.""" |
||||
|
||||
# configuration already loaded? |
||||
if len(cfg) > 0: |
||||
return |
||||
|
||||
# read in the file |
||||
cfg_tmp = read_config(CONFIG_FILE) |
||||
|
||||
if not cfg_tmp: |
||||
raise AccountException("unable to read configuration file: %s" % CONFIG_FILE) |
||||
|
||||
# check that essential fields are completed |
||||
mandatory_fields = [ 'minimum_id', 'maximum_id', 'shell', 'home', |
||||
'gid', 'server_url', 'users_base', 'groups_base', 'bind_dn', |
||||
'bind_password', 'realm', 'principal', 'keytab', 'username_regex', |
||||
'realname_regex' |
||||
] |
||||
|
||||
for field in mandatory_fields: |
||||
if not field in cfg_tmp: |
||||
raise AccountException("missing configuration option: %s" % field) |
||||
if not cfg_tmp[field]: |
||||
raise AccountException("null configuration option: %s" % field) |
||||
|
||||
# check that numeric fields are ints |
||||
numeric_fields = [ 'minimum_id', 'maximum_id', 'gid' ] |
||||
|
||||
for field in numeric_fields: |
||||
if not type(cfg_tmp[field]) in (int, long): |
||||
raise AccountException("non-numeric value for configuration option: %s" % field) |
||||
|
||||
# update the current configuration with the loaded values |
||||
cfg.update(cfg_tmp) |
||||
|
||||
|
||||
def create_account(username, password, realname='', gecos_other=''): |
||||
""" |
||||
Creates a UNIX account for a member. This involves |
||||
first creating a directory entry, then creating |
||||
a Kerberos principal. |
||||
|
||||
Parameters: |
||||
username - UNIX username for the member |
||||
realname - real name of the member |
||||
password - password for the account |
||||
|
||||
Exceptions: |
||||
LDAPException - on LDAP failure |
||||
KrbException - on Kerberos failure |
||||
|
||||
Returns: |
||||
SUCCESS - on success |
||||
BAD_REALNAME - on badly formed real name |
||||
BAD_USERNAME - on badly formed user name |
||||
LDAP_EXISTS - when the user exists in LDAP |
||||
LDAP_NO_IDS - when no user ids are free |
||||
KRB_EXISTS - when the user exists in Kerberos |
||||
""" |
||||
|
||||
# Load Configuration |
||||
load_configuration() |
||||
|
||||
### Connect to the Backends ### |
||||
|
||||
ldap_connection = ldapi.LDAPConnection() |
||||
krb_connection = krb.KrbConnection() |
||||
|
||||
try: |
||||
|
||||
# connect to the LDAP server |
||||
ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base']) |
||||
|
||||
# connect to the Kerberos master server |
||||
krb_connection.connect(cfg['principal'], cfg['keytab']) |
||||
|
||||
### Sanity-checks ### |
||||
|
||||
# check the username and realame for validity |
||||
if not re.match(cfg['username_regex'], username): |
||||
return BAD_USERNAME |
||||
if not re.match(cfg['realname_regex'], realname): |
||||
return BAD_REALNAME |
||||
|
||||
# see if user exists in LDAP |
||||
if ldap_connection.user_lookup(username): |
||||
return LDAP_EXISTS |
||||
|
||||
# determine the first available userid |
||||
userid = ldap_connection.first_id(cfg['minimum_id'], cfg['maximum_id']) |
||||
if not userid: return LDAP_NO_IDS |
||||
|
||||
# build principal name from username |
||||
principal = username + '@' + cfg['realm'] |
||||
|
||||
# see if user exists in Kerberos |
||||
if krb_connection.get_principal(principal): |
||||
return KRB_EXISTS |
||||
|
||||
### User creation ### |
||||
|
||||
# process gecos_other (used to store memberid) |
||||
if gecos_other: |
||||
gecos_other = ',' + str(gecos_other) |
||||
|
||||
# account information defaults |
||||
shell = cfg['shell'] |
||||
home = cfg['home'] + '/' + username |
||||
gecos = realname + ',,,' + gecos_other |
||||
gid = cfg['gid'] |
||||
|
||||
# create the LDAP entry |
||||
ldap_connection.user_add(username, realname, shell, userid, gid, home, gecos) |
||||
|
||||
# create the Kerberos principal |
||||
krb_connection.add_principal(principal, password) |
||||
|
||||
finally: |
||||
ldap_connection.disconnect() |
||||
krb_connection.disconnect() |
||||
|
||||
return SUCCESS |
||||
|
||||
|
||||
def delete_account(username): |
||||
""" |
||||
Deletes the UNIX account of a member. |
||||
|
||||
Parameters: |
||||
username - UNIX username for the member |
||||
|
||||
Exceptions: |
||||
LDAPException - on LDAP failure |
||||
KrbException - on Kerberos failure |
||||
|
||||
Returns: |
||||
SUCCESS - on success |
||||
LDAP_NO_USER - when the user does not exist in LDAP |
||||
KRB_NO_USER - when the user does not exist in Kerberos |
||||
""" |
||||
|
||||
# Load Configuration |
||||
load_configuration() |
||||
|
||||
### Connect to the Backends ### |
||||
|
||||
ldap_connection = ldapi.LDAPConnection() |
||||
krb_connection = krb.KrbConnection() |
||||
|
||||
try: |
||||
|
||||
# connect to the LDAP server |
||||
ldap_connection.connect(cfg['server_url'], cfg['bind_dn'], cfg['bind_password'], cfg['users_base'], cfg['groups_base']) |
||||
|
||||
# connect to the Kerberos master server |
||||
krb_connection.connect(cfg['principal'], cfg['keytab']) |
||||
|
||||
### Sanity-checks ### |
||||
|
||||
# ensure user exists in LDAP |
||||
if not ldap_connection.user_lookup(username): |
||||
return LDAP_NO_USER |
||||
|
||||
# build principal name from username |
||||
principal = username + '@' + cfg['realm'] |
||||
|
||||
# see if user exists in Kerberos |
||||
if not krb_connection.get_principal(principal): |
||||
return KRB_NO_USER |
||||
|
||||
### User deletion ### |
||||
|
||||
# delete the LDAP entry |
||||
ldap_connection.user_delete(username) |
||||
|
||||
# delete the Kerberos principal |
||||
krb_connection.delete_principal(principal) |
||||
|
||||
finally: |
||||
ldap_connection.disconnect() |
||||
krb_connection.disconnect() |
||||
|
||||
return SUCCESS |
||||
|
||||
|
||||
|
||||
### Tests ### |
||||
|
||||
if __name__ == '__main__': |
||||
|
||||
# A word of notice: this test creates a _working_ account (and then deletes it). |
||||
# If deletion fails it must be cleaned up manually. |
||||
|
||||
# a bit of salt so the test account is reasonably tough to crack |
||||
import random |
||||
pw = str(random.randint(100000000000000000, 999999999999999999)) |
||||
|
||||
print "running create_account('testuser', ..., 'Test User', ...)", "->", errors[create_account('testuser', pw, 'Test User')] |
||||
print "running delete_account('testuser')", "->", errors[delete_account('testuser')] |
||||
|
@ -0,0 +1,426 @@ |
||||
# $Id: members.py 44 2006-12-31 07:09:27Z mspang $ |
||||
""" |
||||
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 re |
||||
from csc.admin import terms |
||||
from csc.backend import db |
||||
from csc.lib import read_config |
||||
|
||||
|
||||
|
||||
|
||||
### Configuration |
||||
|
||||
CONFIG_FILE = '/etc/csc/members.cf' |
||||
|
||||
cfg = {} |
||||
|
||||
|
||||
def load_configuration(): |
||||
"""Load Members Configuration""" |
||||
|
||||
# configuration already loaded? |
||||
if len(cfg) > 0: |
||||
return |
||||
|
||||
# read in the file |
||||
cfg_tmp = read_config(CONFIG_FILE) |
||||
|
||||
if not cfg_tmp: |
||||
raise MemberException("unable to read configuration file: %s" |
||||
% CONFIG_FILE) |
||||
|
||||
# check that essential fields are completed |
||||
mandatory_fields = [ 'server', 'database', 'user', 'password' ] |
||||
|
||||
for field in mandatory_fields: |
||||
if not field in cfg_tmp: |
||||
raise MemberException("missing configuratino option: %s" % field) |
||||
if not cfg_tmp[field]: |
||||
raise MemberException("null configuration option: %s" %field) |
||||
|
||||
# update the current configuration with the loaded values |
||||
cfg.update(cfg_tmp) |
||||
|
||||
|
||||
|
||||
### Exceptions ### |
||||
|
||||
class MemberException(Exception): |
||||
"""Exception class for member-related errors.""" |
||||
|
||||
class DuplicateStudentID(MemberException): |
||||
"""Exception class for student ID conflicts.""" |
||||
pass |
||||
|
||||
class InvalidStudentID(MemberException): |
||||
"""Exception class for malformed student IDs.""" |
||||
pass |
||||
|
||||
class InvalidTerm(MemberException): |
||||
"""Exception class for malformed terms.""" |
||||
pass |
||||
|
||||
class NoSuchMember(MemberException): |
||||
"""Exception class for nonexistent members.""" |
||||
pass |
||||
|
||||
|
||||
|
||||
### Connection Management ### |
||||
|
||||
# global database connection |
||||
connection = db.DBConnection() |
||||
|
||||
|
||||
def connect(): |
||||
"""Connect to PostgreSQL.""" |
||||
|
||||
load_configuration() |
||||
|
||||
connection.connect(cfg['server'], cfg['database']) |
||||
|
||||
|
||||
def disconnect(): |
||||
"""Disconnect from PostgreSQL.""" |
||||
|
||||
connection.disconnect() |
||||
|
||||
|
||||
def connected(): |
||||
"""Determine whether the connection has been established.""" |
||||
|
||||
return connection.connected() |
||||
|
||||
|
||||
### Member Table ### |
||||
|
||||
def new(realname, studentid=None, program=None): |
||||
""" |
||||
Registers a new CSC member. The member is added |
||||
to the members table and registered for the current |
||||
term. |
||||
|
||||
Parameters: |
||||
realname - the full real name of the member |
||||
studentid - the student id number of the member |
||||
program - the program of study of the member |
||||
|
||||
Returns: the memberid of the new member |
||||
|
||||
Exceptions: |
||||
DuplicateStudentID - if the student id already exists in the database |
||||
InvalidStudentID - if the student id is malformed |
||||
|
||||
Example: new("Michael Spang", program="CS") -> 3349 |
||||
""" |
||||
|
||||
# blank attributes should be NULL |
||||
if studentid == '': studentid = None |
||||
if program == '': program = None |
||||
|
||||
# check the student id format |
||||
regex = '^[0-9]{8}$' |
||||
if studentid != None and not re.match(regex, str(studentid)): |
||||
raise InvalidStudentID("student id is invalid: %s" % studentid) |
||||
|
||||
# check for duplicate student id |
||||
member = connection.select_member_by_studentid(studentid) |
||||
if member: |
||||
raise DuplicateStudentID("student id exists in database: %s" % studentid) |
||||
|
||||
# add the member |
||||
memberid = connection.insert_member(realname, studentid, program) |
||||
|
||||
# register them for this term |
||||
connection.insert_term(memberid, terms.current()) |
||||
|
||||
# commit the transaction |
||||
connection.commit() |
||||
|
||||
return memberid |
||||
|
||||
|
||||
def get(memberid): |
||||
""" |
||||
Look up attributes of a member by memberid. |
||||
|
||||
Parameters: |
||||
memberid - the member id number |
||||
|
||||
Returns: a dictionary of attributes |
||||
|
||||
Example: get(3349) -> { |
||||
'memberid': 3349, |
||||
'name': 'Michael Spang', |
||||
'program': 'Computer Science', |
||||
... |
||||
} |
||||
""" |
||||
|
||||
return connection.select_member_by_id(memberid) |
||||
|
||||
|
||||
def get_userid(userid): |
||||
""" |
||||
Look up attributes of a member by userid. |
||||
|
||||
Parameters: |
||||
userid - the UNIX user id |
||||
|
||||
Returns: a dictionary of attributes |
||||
|
||||
Example: get('mspang') -> { |
||||
'memberid': 3349, |
||||
'name': 'Michael Spang', |
||||
'program': 'Computer Science', |
||||
... |
||||
} |
||||
""" |
||||
|
||||
return connection.select_member_by_account(userid) |
||||
|
||||
|
||||
def get_studentid(studentid): |
||||
""" |
||||
Look up attributes of a member by studnetid. |
||||
|
||||
Parameters: |
||||
studentid - the student ID number |
||||
|
||||
Returns: a dictionary of attributes |
||||
|
||||
Example: get(...) -> { |
||||
'memberid': 3349, |
||||
'name': 'Michael Spang', |
||||
'program': 'Computer Science', |
||||
... |
||||
} |
||||
""" |
||||
|
||||
return connection.select_member_by_studentid(studentid) |
||||
|
||||
|
||||
def list_term(term): |
||||
""" |
||||
Build a list of members in a term. |
||||
|
||||
Parameters: |
||||
term - the term to match members against |
||||
|
||||
Returns: a list of member dictionaries |
||||
|
||||
Example: list_term('f2006'): -> [ |
||||
{ 'memberid': 3349, ... }, |
||||
{ 'memberid': ... }. |
||||
... |
||||
] |
||||
""" |
||||
|
||||
# retrieve a list of memberids in term |
||||
memberlist = connection.select_members_by_term(term) |
||||
|
||||
# convert the list of memberids to a list of dictionaries |
||||
memberlist = map(connection.select_member_by_id, memberlist) |
||||
|
||||
return memberlist |
||||
|
||||
|
||||
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'): -> [ |
||||
{ 'memberid': 3349, ... }, |
||||
{ 'memberid': ... }, |
||||
... |
||||
] |
||||
""" |
||||
|
||||
# retrieve a list of memberids matching name |
||||
memberlist = connection.select_members_by_name(name) |
||||
|
||||
# convert the list of memberids to a list of dictionaries |
||||
memberlist = map(connection.select_member_by_id, memberlist) |
||||
|
||||
return memberlist |
||||
|
||||
|
||||
def delete(memberid): |
||||
""" |
||||
Erase all records of a member. |
||||
|
||||
Note: real members are never removed |
||||
from the database |
||||
|
||||
Parameters: |
||||
memberid - the member id number |
||||
|
||||
Returns: attributes and terms of the |
||||
member in a tuple |
||||
|
||||
Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993']) |
||||
""" |
||||
|
||||
# save member data |
||||
member = connection.select_member_by_id(memberid) |
||||
term_list = connection.select_terms(memberid) |
||||
|
||||
# remove data from the db |
||||
connection.delete_term_all(memberid) |
||||
connection.delete_member(memberid) |
||||
connection.commit() |
||||
|
||||
return (member, term_list) |
||||
|
||||
|
||||
def update(member): |
||||
""" |
||||
Update CSC member attributes. None is NULL. |
||||
|
||||
Parameters: |
||||
member - a dictionary with member attributes as |
||||
returned by get, possibly omitting some |
||||
attributes. member['memberid'] must exist |
||||
and be valid. |
||||
|
||||
Exceptions: |
||||
NoSuchMember - if the member id does not exist |
||||
InvalidStudentID - if the student id number is malformed |
||||
DuplicateStudentID - if the student id number exists |
||||
|
||||
Example: update( {'memberid': 3349, userid: 'mspang'} ) |
||||
""" |
||||
|
||||
if member.has_key('studentid') and member['studentid'] != None: |
||||
|
||||
studentid = member['studentid'] |
||||
|
||||
# check the student id format |
||||
regex = '^[0-9]{8}$' |
||||
if studentid != None and not re.match(regex, str(studentid)): |
||||
raise InvalidStudentID("student id is invalid: %s" % studentid) |
||||
|
||||
# check for duplicate student id |
||||
member = connection.select_member_by_studentid(studentid) |
||||
if member: |
||||
raise DuplicateStudentID("student id exists in database: %s" % |
||||
studentid) |
||||
|
||||
# not specifying memberid is a bug |
||||
if not member.has_key('memberid'): |
||||
raise Exception("no member specified in call to update") |
||||
memberid = member['memberid'] |
||||
|
||||
# see if member exists |
||||
old_member = connection.select_member_by_id(memberid) |
||||
if not old_member: |
||||
raise NoSuchMember("memberid does not exist in database: %d" % |
||||
memberid) |
||||
|
||||
# do the update |
||||
connection.update_member(member) |
||||
|
||||
# commit the transaction |
||||
connection.commit() |
||||
|
||||
|
||||
|
||||
### Term Table ### |
||||
|
||||
def register(memberid, term_list): |
||||
""" |
||||
Registers a member for one or more terms. |
||||
|
||||
Parameters: |
||||
memberid - the member id number |
||||
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"]) |
||||
""" |
||||
|
||||
if not type(term_list) in (list, tuple): |
||||
term_list = [ term_list ] |
||||
|
||||
for term in term_list: |
||||
|
||||
# check term syntax |
||||
if not re.match('^[wsf][0-9]{4}$', term): |
||||
raise InvalidTerm("term is invalid: %s" % term) |
||||
|
||||
# add term to database |
||||
connection.insert_term(memberid, term) |
||||
|
||||
connection.commit() |
||||
|
||||
|
||||
def registered(memberid, term): |
||||
""" |
||||
Determines whether a member is registered |
||||
for a term. |
||||
|
||||
Parameters: |
||||
memberid - the member id number |
||||
term - the term to check |
||||
|
||||
Returns: whether the member is registered |
||||
|
||||
Example: registered(3349, "f2006") -> True |
||||
""" |
||||
|
||||
return connection.select_term(memberid, term) != None |
||||
|
||||
|
||||
def terms_list(memberid): |
||||
""" |
||||
Retrieves a list of terms a member is |
||||
registered for. |
||||
|
||||
Parameters: |
||||
memberid - the member id number |
||||
|
||||
Returns: list of term strings |
||||
|
||||
Example: registered(0) -> 's1993' |
||||
""" |
||||
|
||||
return connection.select_terms(memberid) |
||||
|
||||
|
||||
|
||||
### Tests ### |
||||
|
||||
if __name__ == '__main__': |
||||
|
||||
connect() |
||||
|
||||
|
||||
sid = new("Test User", "99999999", "CS") |
||||
|
||||
assert registered(id, terms.current()) |
||||
print get(sid) |
||||
register(sid, terms.next(terms.current())) |
||||
assert registered(sid, terms.next(terms.current())) |
||||
print terms_list(sid) |
||||
print get(sid) |
||||
print delete(sid) |
@ -0,0 +1,252 @@ |
||||
# $Id: terms.py 44 2006-12-31 07:09:27Z mspang $ |
||||
""" |
||||
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 valid(term): |
||||
""" |
||||
Determines whether a term is well-formed: |
||||
|
||||
Parameters: |
||||
term - the term string |
||||
|
||||
Returns: whether the term is valid (boolean) |
||||
|
||||
Example: valid("f2006") -> True |
||||
""" |
||||
|
||||
regex = '^[wsf][0-9]{4}$' |
||||
return re.match(regex, term) != 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 valid(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__': |
||||
|
||||
assert parse('f2006') == 110 |
||||
assert generate(110) == 'f2006' |
||||
assert next('f2006') == 'w2007' |
||||
assert previous('f2006') == 's2006' |
||||
assert delta('f2006', 'w2007') == 1 |
||||
assert add('f2006', delta('f2006', 'w2010')) == 'w2010' |
||||
assert interval('f2006', 3) == ['f2006', 'w2007', 's2007'] |
||||
assert from_timestamp(1166135779) == 'f2006' |
||||
assert parse( current() ) >= 110 |
||||
assert next_unregistered( [current()] ) == next( current() ) |
||||
assert next_unregistered( [] ) == current() |
||||
assert next_unregistered( [previous(current())] ) == current() |
||||
assert next_unregistered( [add(current(), -2)] ) == current() |
||||
|
||||
print "All tests passed." "\n" |
@ -0,0 +1,9 @@ |
||||
# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $ |
||||
""" |
||||
User Interfaces |
||||
|
||||
This module contains frontends and related modules. |
||||
CEO's primary frontends are: |
||||
|
||||
legacy - aims to reproduce the curses UI of the previous CEO |
||||
""" |
@ -0,0 +1,10 @@ |
||||
# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $ |
||||
""" |
||||
Legacy User Interface |
||||
|
||||
This module contains the legacy CEO user interface and related modules. |
||||
Important modules are: |
||||
|
||||
main.py - all of the main UI logic |
||||
helpers.py - user interface library routines |
||||
""" |
@ -0,0 +1,412 @@ |
||||
# $Id: helpers.py 35 2006-12-28 05:14:05Z mspang $ |
||||
""" |
||||
Helpers for legacy User Interface |
||||
|
||||
This module contains numerous functions that are designed to immitate |
||||
the look and behavior of the previous CEO. Included is code for various |
||||
curses-based UI widgets that were provided by Perl 5's Curses and |
||||
Curses::Widgets libraries. |
||||
|
||||
Though attempts have been made to keep the UI bug-compatible with |
||||
the previous system, some compromises have been made. For example, |
||||
the input and textboxes draw 'OK' and 'Cancel' buttons where the old |
||||
CEO had them, but they are fake. That is, the buttons in the old |
||||
CEO were selectable but non-operational, but in the new CEO they are |
||||
not even selectable. |
||||
""" |
||||
import curses.ascii |
||||
|
||||
# key constants not defined in CURSES |
||||
KEY_RETURN = ord('\n') |
||||
KEY_ESCAPE = 27 |
||||
KEY_EOT = 4 |
||||
|
||||
|
||||
def center(parent_dim, child_dim): |
||||
"""Helper for centering a length in a larget length.""" |
||||
return (parent_dim-child_dim)/2 |
||||
|
||||
|
||||
def read_input(wnd, offy, offx, width, maxlen, echo=True): |
||||
""" |
||||
Read user input within a confined region of a window. |
||||
|
||||
Basic line-editing is supported: |
||||
LEFT, RIGHT, HOME, and END move around. |
||||
BACKSPACE and DEL remove characters. |
||||
INSERT switches between insert and overwrite mode. |
||||
ESC and C-d abort input. |
||||
RETURN completes input. |
||||
|
||||
Parameters: |
||||
wnd - parent window for region |
||||
offy - the vertical offset for the beginning of the input region |
||||
offx - the horizontal offset for the beginning of the input region |
||||
width - the width of the region |
||||
maxlen - greatest number of characters to read (0 for no limit) |
||||
echo - boolean: whether to display typed characters |
||||
|
||||
Returns: the string, or None when the user aborts. |
||||
""" |
||||
|
||||
# turn on cursor |
||||
try: |
||||
curses.curs_set(1) |
||||
except: |
||||
pass |
||||
|
||||
# set keypad mode to allow UP, DOWN, etc |
||||
wnd.keypad(1) |
||||
|
||||
# the input string |
||||
input = "" |
||||
|
||||
# offset of cursor in input |
||||
# i.e. the next operation is applied at input[inputoff] |
||||
inputoff = 0 |
||||
|
||||
# display offset (for scrolling) |
||||
# i.e. the first character in the region is input[displayoff] |
||||
displayoff = 0 |
||||
|
||||
# insert mode (True) or overwrite mode (False) |
||||
insert = True |
||||
|
||||
while True: |
||||
|
||||
# echo mode, display the string |
||||
if echo: |
||||
# discard characters before displayoff, |
||||
# as the window may be scrolled to the right |
||||
substring = input[displayoff:] |
||||
|
||||
# pad the string with zeroes to overwrite stale characters |
||||
substring = substring + " " * (width - len(substring)) |
||||
|
||||
# display the substring |
||||
wnd.addnstr(offy, offx, substring, width) |
||||
|
||||
# await input |
||||