forked from public/pyceo
Initial import (version 0.1).
This commit is contained in:
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
|
||||||
|
key = wnd.getch(offy, offx + inputoff - displayoff)
|
||||||
|
|
||||||
|
# not echo mode, don't display the string
|
||||||
|
else:
|
||||||
|
# await input at arbitrary location
|
||||||
|
key = wnd.getch(offy, offx)
|
||||||
|
|
||||||
|
# enter returns input
|
||||||
|
if key == KEY_RETURN:
|
||||||
|
return input
|
||||||
|
|
||||||
|
# escape aborts input
|
||||||
|
elif key == KEY_ESCAPE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# EOT (C-d) aborts if there is no input
|
||||||
|
elif key == KEY_EOT:
|
||||||
|
if len(input) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# backspace removes the previous character
|
||||||
|
elif key == curses.KEY_BACKSPACE:
|
||||||
|
if inputoff > 0:
|
||||||
|
|
||||||
|
# remove the character immediately before the input offset
|
||||||
|
input = input[0:inputoff-1] + input[inputoff:]
|
||||||
|
inputoff -= 1
|
||||||
|
|
||||||
|
# move either the cursor or entire line of text left
|
||||||
|
if displayoff > 0:
|
||||||
|
displayoff -= 1
|
||||||
|
|
||||||
|
# delete removes the current character
|
||||||
|
elif key == curses.KEY_DC:
|
||||||
|
if inputoff < len(input):
|
||||||
|
|
||||||
|
# remove the character at the input offset
|
||||||
|
input = input[0:inputoff] + input[inputoff+1:]
|
||||||
|
|
||||||
|
# left moves the cursor one character left
|
||||||
|
elif key == curses.KEY_LEFT:
|
||||||
|
if inputoff > 0:
|
||||||
|
|
||||||
|
# move the cursor to the left
|
||||||
|
inputoff -= 1
|
||||||
|
|
||||||
|
# scroll left if necessary
|
||||||
|
if inputoff < displayoff:
|
||||||
|
displayoff -= 1
|
||||||
|
|
||||||
|
# right moves the cursor one character right
|
||||||
|
elif key == curses.KEY_RIGHT:
|
||||||
|
if inputoff < len(input):
|
||||||
|
|
||||||
|
# move the cursor to the right
|
||||||
|
inputoff += 1
|
||||||
|
|
||||||
|
# scroll right if necessary
|
||||||
|
if displayoff - inputoff == width:
|
||||||
|
displayoff += 1
|
||||||
|
|
||||||
|
# home moves the cursor to the first character
|
||||||
|
elif key == curses.KEY_HOME:
|
||||||
|
inputoff = 0
|
||||||
|
displayoff = 0
|
||||||
|
|
||||||
|
# end moves the cursor past the last character
|
||||||
|
elif key == curses.KEY_END:
|
||||||
|
inputoff = len(input)
|
||||||
|
displayoff = len(input) - width + 1
|
||||||
|
|
||||||
|
# insert toggles insert/overwrite mode
|
||||||
|
elif key == curses.KEY_IC:
|
||||||
|
insert = not insert
|
||||||
|
|
||||||
|
# other (printable) characters are added to the input string
|
||||||
|
elif curses.ascii.isprint(key):
|
||||||
|
if len(input) < maxlen or maxlen == 0:
|
||||||
|
|
||||||
|
# insert mode: insert before current offset
|
||||||
|
if insert:
|
||||||
|
input = input[0:inputoff] + chr(key) + input[inputoff:]
|
||||||
|
|
||||||
|
# overwrite mode: replace current offset
|
||||||
|
else:
|
||||||
|
input = input[0:inputoff] + chr(key) + input[inputoff+1:]
|
||||||
|
|
||||||
|
# increment the input offset
|
||||||
|
inputoff += 1
|
||||||
|
|
||||||
|
# scroll right if necessary
|
||||||
|
if inputoff - displayoff == width:
|
||||||
|
displayoff += 1
|
||||||
|
|
||||||
|
|
||||||
|
def inputbox(wnd, prompt, field_width, echo=True):
|
||||||
|
"""Display a window for user input."""
|
||||||
|
|
||||||
|
wnd_height, wnd_width = wnd.getmaxyx()
|
||||||
|
height, width = 12, field_width + 7
|
||||||
|
|
||||||
|
# draw a window for the dialog
|
||||||
|
childy, childx = center(wnd_height-1, height)+1, center(wnd_width, width)
|
||||||
|
child_wnd = wnd.subwin(height, width, childy, childx)
|
||||||
|
child_wnd.clear()
|
||||||
|
child_wnd.border()
|
||||||
|
|
||||||
|
# draw another window for the text box
|
||||||
|
texty, textx = center(height-1, 3)+1, center(width-1, width-5)+1
|
||||||
|
textheight, textwidth = 3, width-5
|
||||||
|
text_wnd = child_wnd.derwin(textheight, textwidth, texty, textx)
|
||||||
|
text_wnd.clear()
|
||||||
|
text_wnd.border()
|
||||||
|
|
||||||
|
# draw the prompt
|
||||||
|
prompty, promptx = 2, 3
|
||||||
|
child_wnd.addnstr(prompty, promptx, prompt, width-2)
|
||||||
|
|
||||||
|
# draw the fake buttons
|
||||||
|
fakey, fakex = 9, width - 19
|
||||||
|
child_wnd.addstr(fakey, fakex, "< OK > < Cancel >")
|
||||||
|
child_wnd.addch(fakey, fakex+2, "O", curses.A_UNDERLINE)
|
||||||
|
child_wnd.addch(fakey, fakex+9, "C", curses.A_UNDERLINE)
|
||||||
|
|
||||||
|
# update the screen
|
||||||
|
child_wnd.noutrefresh()
|
||||||
|
text_wnd.noutrefresh()
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
# read an input string within the field region of text_wnd
|
||||||
|
inputy, inputx, inputwidth = 1, 1, textwidth - 2
|
||||||
|
input = read_input(text_wnd, inputy, inputx, inputwidth, 0, echo)
|
||||||
|
|
||||||
|
# erase the window
|
||||||
|
child_wnd.erase()
|
||||||
|
child_wnd.refresh()
|
||||||
|
|
||||||
|
return input
|
||||||
|
|
||||||
|
|
||||||
|
def line_wrap(line, width):
|
||||||
|
"""Wrap a string to a certain width (returns a list of strings)."""
|
||||||
|
|
||||||
|
wrapped_lines = []
|
||||||
|
tokens = line.split(" ")
|
||||||
|
tokens.reverse()
|
||||||
|
tmp = tokens.pop()
|
||||||
|
if len(tmp) > width:
|
||||||
|
wrapped_lines.append(tmp[0:width])
|
||||||
|
tmp = tmp[width:]
|
||||||
|
while len(tokens) > 0:
|
||||||
|
token = tokens.pop()
|
||||||
|
if len(tmp) + len(token) + 1 <= width:
|
||||||
|
tmp += " " + token
|
||||||
|
elif len(token) > width:
|
||||||
|
tmp += " " + token[0:width-len(tmp)-1]
|
||||||
|
tokens.push(token[width-len(tmp)-1:])
|
||||||
|
else:
|
||||||
|
wrapped_lines.append(tmp)
|
||||||
|
tmp = token
|
||||||
|
wrapped_lines.append(tmp)
|
||||||
|
return wrapped_lines
|
||||||
|
|
||||||
|
|
||||||
|
def msgbox(wnd, msg, title="Message"):
|
||||||
|
"""Display a message in a window."""
|
||||||
|
|
||||||
|
# split message into a list of lines
|
||||||
|
lines = msg.split("\n")
|
||||||
|
|
||||||
|
# determine the dimensions of the method
|
||||||
|
message_height = len(lines)
|
||||||
|
message_width = 0
|
||||||
|
for line in lines:
|
||||||
|
if len(line) > message_width:
|
||||||
|
message_width = len(line)
|
||||||
|
|
||||||
|
# ensure the window fits the title
|
||||||
|
if len(title) > message_width:
|
||||||
|
message_width = len(title)
|
||||||
|
|
||||||
|
# maximum message width
|
||||||
|
parent_height, parent_width = wnd.getmaxyx()
|
||||||
|
max_message_width = parent_width - 8
|
||||||
|
|
||||||
|
# line-wrap if necessary
|
||||||
|
if message_width > max_message_width:
|
||||||
|
newlines = []
|
||||||
|
for line in lines:
|
||||||
|
for newline in line_wrap(line, max_message_width):
|
||||||
|
newlines.append(newline)
|
||||||
|
lines = newlines
|
||||||
|
message_width = max_message_width
|
||||||
|
message_height = len(lines)
|
||||||
|
|
||||||
|
# random padding that perl's curses adds
|
||||||
|
pad_width = 2
|
||||||
|
|
||||||
|
# create the outer window
|
||||||
|
outer_height, outer_width = message_height + 8, message_width + pad_width + 6
|
||||||
|
outer_y, outer_x = center(parent_height+1, outer_height)-1, center(parent_width, outer_width)
|
||||||
|
outer_wnd = wnd.derwin(outer_height, outer_width, outer_y, outer_x)
|
||||||
|
outer_wnd.erase()
|
||||||
|
outer_wnd.border()
|
||||||
|
|
||||||
|
# create the inner window
|
||||||
|
inner_height, inner_width = message_height + 2, message_width + pad_width + 2
|
||||||
|
inner_y, inner_x = 2, center(outer_width, inner_width)
|
||||||
|
inner_wnd = outer_wnd.derwin(inner_height, inner_width, inner_y, inner_x)
|
||||||
|
inner_wnd.border()
|
||||||
|
|
||||||
|
# display the title
|
||||||
|
outer_wnd.addstr(0, 1, " " + title + " ", curses.A_REVERSE | curses.A_BOLD)
|
||||||
|
|
||||||
|
# display the message
|
||||||
|
for i in xrange(len(lines)):
|
||||||
|
inner_wnd.addnstr(i+1, 1, lines[i], message_width)
|
||||||
|
|
||||||
|
# draw a solid block at the end of the first few lines
|
||||||
|
if i < len(lines) - 1:
|
||||||
|
inner_wnd.addch(i+1, inner_width-1, ' ', curses.A_REVERSE)
|
||||||
|
|
||||||
|
# display the fake OK button
|
||||||
|
fakey, fakex = outer_height - 3, outer_width - 8
|
||||||
|
outer_wnd.addstr(fakey, fakex, "< OK >", curses.A_REVERSE)
|
||||||
|
outer_wnd.addch(fakey, fakex+2, "O", curses.A_UNDERLINE | curses.A_REVERSE)
|
||||||
|
|
||||||
|
# update display
|
||||||
|
outer_wnd.noutrefresh()
|
||||||
|
inner_wnd.noutrefresh()
|
||||||
|
curses.doupdate()
|
||||||
|
|
||||||
|
# read a RETURN or ESC before returning
|
||||||
|
curses.curs_set(0)
|
||||||
|
outer_wnd.keypad(1)
|
||||||
|
while True:
|
||||||
|
key = outer_wnd.getch(0,0)
|
||||||
|
if key == KEY_RETURN or key == KEY_ESCAPE:
|
||||||
|
break
|
||||||
|
|
||||||
|
# clear the window
|
||||||
|
outer_wnd.erase()
|
||||||
|
outer_wnd.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
def menu(wnd, offy, offx, width, options, _acquire_wnd=None):
|
||||||
|
"""
|
||||||
|
Draw a menu and wait for a selection.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
wnd - parent window
|
||||||
|
offy - vertical offset for menu region
|
||||||
|
offx - horizontal offset for menu region
|
||||||
|
width - width of menu region
|
||||||
|
options - a list of selections
|
||||||
|
_acquire_wnd - hack to support resize: must be a function callback
|
||||||
|
that returns new values for wnd, offy, offx, height,
|
||||||
|
width. Unused if None.
|
||||||
|
|
||||||
|
Returns: index into options that was selected
|
||||||
|
"""
|
||||||
|
|
||||||
|
# the currently selected option
|
||||||
|
selected = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# disable cursor
|
||||||
|
curses.curs_set(0)
|
||||||
|
|
||||||
|
# hack to support resize: recreate the
|
||||||
|
# parent window every iteration
|
||||||
|
if _acquire_wnd:
|
||||||
|
wnd, offy, offx, height, width = _acquire_wnd()
|
||||||
|
|
||||||
|
# keypad mode so getch() works with up, down
|
||||||
|
wnd.keypad(1)
|
||||||
|
|
||||||
|
# display the menu
|
||||||
|
for i in xrange(len(options)):
|
||||||
|
text, callback = options[i]
|
||||||
|
text = text + " " * (width - len(text))
|
||||||
|
|
||||||
|
# the selected option is displayed in reverse video
|
||||||
|
if i == selected:
|
||||||
|
wnd.addstr(i+offy, offx, text, curses.A_REVERSE)
|
||||||
|
else:
|
||||||
|
wnd.addstr(i+offy, offx, text)
|
||||||
|
# display the member
|
||||||
|
|
||||||
|
wnd.refresh()
|
||||||
|
|
||||||
|
# read one keypress
|
||||||
|
input = wnd.getch()
|
||||||
|
|
||||||
|
# UP moves to the previous option
|
||||||
|
if input == curses.KEY_UP and selected > 0:
|
||||||
|
selected = (selected - 1)
|
||||||
|
|
||||||
|
# DOWN moves to the next option
|
||||||
|
elif input == curses.KEY_DOWN and selected < len(options) - 1:
|
||||||
|
selected = (selected + 1)
|
||||||
|
|
||||||
|
# RETURN runs the callback for the selected option
|
||||||
|
elif input == KEY_RETURN:
|
||||||
|
text, callback = options[selected]
|
||||||
|
|
||||||
|
# highlight the selected option
|
||||||
|
text = text + " " * (width - len(text))
|
||||||
|
wnd.addstr(selected+offy, offx, text, curses.A_REVERSE | curses.A_BOLD)
|
||||||
|
wnd.refresh()
|
||||||
|
|
||||||
|
# execute the selected option
|
||||||
|
if callback(wnd): # success
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def reset():
|
||||||
|
"""Reset the terminal and clear the screen."""
|
||||||
|
|
||||||
|
reset = curses.tigetstr('rs1')
|
||||||
|
if not reset: reset = '\x1bc'
|
||||||
|
curses.putp(reset)
|
||||||
|
|
|
@ -0,0 +1,555 @@
|
||||||
|
# $Id: main.py 44 2006-12-31 07:09:27Z mspang $
|
||||||
|
"""
|
||||||
|
CEO-like Frontend
|
||||||
|
|
||||||
|
This frontend aims to be compatible in both look and function with the
|
||||||
|
curses UI of CEO.
|
||||||
|
|
||||||
|
Some small improvements have been made, such as not echoing passwords and
|
||||||
|
aborting when nothing is typed into the first input box during an operation.
|
||||||
|
|
||||||
|
This frontend is poorly documented, deprecated, and undoubtedly full of bugs.
|
||||||
|
"""
|
||||||
|
import curses.ascii, re, os
|
||||||
|
from helpers import menu, inputbox, msgbox, reset
|
||||||
|
from csc.admin import accounts, members, terms
|
||||||
|
|
||||||
|
# color of the ceo border
|
||||||
|
BORDER_COLOR = curses.COLOR_RED
|
||||||
|
|
||||||
|
|
||||||
|
def action_new_member(wnd):
|
||||||
|
"""Interactively add a new member."""
|
||||||
|
|
||||||
|
username, studentid, program = '', None, ''
|
||||||
|
|
||||||
|
# read the name
|
||||||
|
prompt = " Name: "
|
||||||
|
realname = inputbox(wnd, prompt, 18)
|
||||||
|
|
||||||
|
# abort if no username is entered
|
||||||
|
if not realname or realname.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# read the student id
|
||||||
|
prompt = "Student id:"
|
||||||
|
while studentid == None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
|
||||||
|
studentid = inputbox(wnd, prompt, 18)
|
||||||
|
|
||||||
|
# abort if exit is entered
|
||||||
|
if studentid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
if studentid == '':
|
||||||
|
studentid = None
|
||||||
|
|
||||||
|
# read the program of study
|
||||||
|
prompt = " Program:"
|
||||||
|
program = inputbox(wnd, prompt, 18)
|
||||||
|
|
||||||
|
# abort if exit is entered
|
||||||
|
if program == None or program.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# connect the members module to its backend if necessary
|
||||||
|
if not members.connected(): members.connect()
|
||||||
|
|
||||||
|
# attempt to create the member
|
||||||
|
try:
|
||||||
|
memberid = members.new(realname, studentid, program)
|
||||||
|
|
||||||
|
msgbox(wnd, "Success! Your memberid is %s. You are now registered\n"
|
||||||
|
% memberid + "for the " + terms.current() + " term.");
|
||||||
|
|
||||||
|
except members.InvalidStudentID:
|
||||||
|
msgbox(wnd, "Invalid student ID.")
|
||||||
|
return False
|
||||||
|
except members.DuplicateStudentID:
|
||||||
|
msgbox(wnd, "A member with this student ID exists.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def action_term_register(wnd):
|
||||||
|
"""Interactively register a member for a term."""
|
||||||
|
|
||||||
|
memberid, term = '', ''
|
||||||
|
|
||||||
|
# read the member id
|
||||||
|
prompt = 'Enter memberid ("exit" to cancel):'
|
||||||
|
memberuserid = inputbox(wnd, prompt, 36)
|
||||||
|
|
||||||
|
if not memberuserid or memberuserid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
member = get_member_memberid_userid(wnd, memberuserid)
|
||||||
|
if not member: return False
|
||||||
|
|
||||||
|
memberid = member['memberid']
|
||||||
|
term_list = members.terms_list(memberid)
|
||||||
|
|
||||||
|
# display user
|
||||||
|
display_member_details(wnd, member, term_list)
|
||||||
|
|
||||||
|
# read the term
|
||||||
|
prompt = "Which term to register for ([fws]20nn):"
|
||||||
|
while not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit':
|
||||||
|
term = inputbox(wnd, prompt, 41)
|
||||||
|
|
||||||
|
# abort when exit is entered
|
||||||
|
if term.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# already registered?
|
||||||
|
if members.registered(memberid, term):
|
||||||
|
msgbox(wnd, "You are already registered for term " + term)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# attempt to register
|
||||||
|
members.register(memberid, term)
|
||||||
|
|
||||||
|
# display success message [sic]
|
||||||
|
msgbox(wnd, "Your are now registered for term " + term)
|
||||||
|
|
||||||
|
except members.InvalidTerm:
|
||||||
|
msgbox(wnd, "Term is not valid: %s" % term)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def action_term_register_multiple(wnd):
|
||||||
|
"""Interactively register a member for multiple terms."""
|
||||||
|
|
||||||
|
memberid, base, num = '', '', None
|
||||||
|
|
||||||
|
# read the member id
|
||||||
|
prompt = 'Enter memberid ("exit" to cancel):'
|
||||||
|
memberuserid = inputbox(wnd, prompt, 36)
|
||||||
|
|
||||||
|
if not memberuserid or memberuserid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
member = get_member_memberid_userid(wnd, memberuserid)
|
||||||
|
if not member: return False
|
||||||
|
|
||||||
|
memberid = member['memberid']
|
||||||
|
term_list = members.terms_list(memberid)
|
||||||
|
|
||||||
|
# display user
|
||||||
|
display_member_details(wnd, member, term_list)
|
||||||
|
|
||||||
|
# read the base
|
||||||
|
prompt = "Which term to start registering ([fws]20nn):"
|
||||||
|
while not re.match('^[wsf][0-9]{4}$', base) and not base == 'exit':
|
||||||
|
base = inputbox(wnd, prompt, 41)
|
||||||
|
|
||||||
|
# abort when exit is entered
|
||||||
|
if base.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# read number of terms
|
||||||
|
prompt = 'How many terms?'
|
||||||
|
while not num or not re.match('^[0-9]*$', num):
|
||||||
|
num = inputbox(wnd, prompt, 36)
|
||||||
|
num = int(num)
|
||||||
|
|
||||||
|
# any terms in the range?
|
||||||
|
if num < 1:
|
||||||
|
msgbox(wnd, "No terms to register.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# compile a list to register
|
||||||
|
term_list = terms.interval(base, num)
|
||||||
|
|
||||||
|
# already registered?
|
||||||
|
for term in term_list:
|
||||||
|
if members.registered(memberid, term):
|
||||||
|
msgbox(wnd, "You are already registered for term " + term)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# attempt to register all terms
|
||||||
|
members.register(memberid, term_list)
|
||||||
|
|
||||||
|
# display success message [sic]
|
||||||
|
msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
|
||||||
|
|
||||||
|
except members.InvalidTerm:
|
||||||
|
msgbox(wnd, "Term is not valid: %s" % term)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def action_create_account(wnd):
|
||||||
|
"""Interactively create an account for a member."""
|
||||||
|
|
||||||
|
memberid, userid = '', ''
|
||||||
|
|
||||||
|
# read the member id
|
||||||
|
prompt = 'Enter member ID (exit to cancel):'
|
||||||
|
memberid = inputbox(wnd, prompt, 35)
|
||||||
|
|
||||||
|
if not memberid or memberid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
member = get_member_memberid_userid(wnd, memberid)
|
||||||
|
if not member: return False
|
||||||
|
|
||||||
|
memberid = member['memberid']
|
||||||
|
term_list = members.terms_list(memberid)
|
||||||
|
|
||||||
|
# display the member
|
||||||
|
display_member_details(wnd, member, term_list)
|
||||||
|
|
||||||
|
# verify member
|
||||||
|
prompt = "Is this the correct member?"
|
||||||
|
answer = None
|
||||||
|
while answer != "yes" and answer != "y" and answer != "n" and answer != "no" and answer != "exit":
|
||||||
|
answer = inputbox(wnd, prompt, 28)
|
||||||
|
|
||||||
|
# user abort
|
||||||
|
if answer == "exit":
|
||||||
|
return False
|
||||||
|
|
||||||
|
# incorrect member; abort
|
||||||
|
if answer == "no" or answer == "n":
|
||||||
|
msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# read user id
|
||||||
|
prompt = "Userid:"
|
||||||
|
while userid == '':
|
||||||
|
userid = inputbox(wnd, prompt, 18)
|
||||||
|
|
||||||
|
# user abort
|
||||||
|
if userid == None or userid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# member already has an account?
|
||||||
|
#if member['userid'] != None:
|
||||||
|
# msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
|
||||||
|
# "Contact the sysadmin if there are still problems." )
|
||||||
|
# return False
|
||||||
|
|
||||||
|
# password input loop
|
||||||
|
password = "password"
|
||||||
|
check = "check"
|
||||||
|
while password != check:
|
||||||
|
|
||||||
|
# read password
|
||||||
|
prompt = "User password:"
|
||||||
|
password = None
|
||||||
|
while not password:
|
||||||
|
password = inputbox(wnd, prompt, 18, False)
|
||||||
|
|
||||||
|
# read another password
|
||||||
|
prompt = "Enter the password again:"
|
||||||
|
check = None
|
||||||
|
while not check:
|
||||||
|
check = inputbox(wnd, prompt, 27, False)
|
||||||
|
|
||||||
|
|
||||||
|
# create the UNIX account
|
||||||
|
result = accounts.create_account(userid, password, member['name'], memberid)
|
||||||
|
|
||||||
|
if result == accounts.LDAP_EXISTS:
|
||||||
|
msgbox(wnd, "Error: Could not do stuff , Already exists.")
|
||||||
|
return False
|
||||||
|
elif result == accounts.KRB_EXISTS:
|
||||||
|
msgbox(wnd, "This account already exists in Kerberos, but not in LDAP. Please contact the Systems Administrator.")
|
||||||
|
return False
|
||||||
|
elif result == accounts.LDAP_NO_IDS:
|
||||||
|
msgbox(wnd, "There are no available UNIX user ids. This is a fatal error. Contact the Systems Administrator.")
|
||||||
|
return False
|
||||||
|
elif result == accounts.BAD_REALNAME:
|
||||||
|
msgbox(wnd, "Invalid real name: %s. Contact the Systems Administrator." % member['name'])
|
||||||
|
return False
|
||||||
|
elif result == accounts.BAD_USERNAME:
|
||||||
|
msgbox(wnd, "Invalid username: %s. Enter a valid username." % userid)
|
||||||
|
return False
|
||||||
|
elif result != accounts.SUCCESS:
|
||||||
|
raise Exception("Unexpected return status of accounts.create_account(): %s" % result)
|
||||||
|
|
||||||
|
# now update the CEO database with the username
|
||||||
|
members.update( {'memberid':memberid, 'userid': userid} )
|
||||||
|
|
||||||
|
# success
|
||||||
|
msgbox(wnd, "Please run 'addhomedir " + userid + "'.")
|
||||||
|
msgbox(wnd, "Success! Your account has been added")
|
||||||
|
|
||||||
|
|
||||||
|
def display_member_details(wnd, member, term_list):
|
||||||
|
"""Display member attributes in a message box."""
|
||||||
|
|
||||||
|
# clone and sort term_list
|
||||||
|
term_list = list(term_list)
|
||||||
|
term_list.sort( terms.compare )
|
||||||
|
|
||||||
|
# labels for data
|
||||||
|
id_label, studentid_label, name_label = "ID:", "StudentID:", "Name:"
|
||||||
|
program_label, userid_label, terms_label = "Program:", "User ID:", "Terms:"
|
||||||
|
|
||||||
|
# format it all into a massive string
|
||||||
|
message = "%8s %-20s %10s %-10s (user)\n" % (name_label, member['name'], id_label, member['memberid']) + \
|
||||||
|
"%8s %-20s %10s %-10s\n" % (program_label, member['program'], studentid_label, member['studentid'])
|
||||||
|
|
||||||
|
if member['userid']:
|
||||||
|
message += "%8s %s\n" % (userid_label, member['userid'])
|
||||||
|
else:
|
||||||
|
message += 'No user ID.\n'
|
||||||
|
|
||||||
|
message += "%s %s" % (terms_label, " ".join(term_list))
|
||||||
|
|
||||||
|
# display the string in a message box
|
||||||
|
msgbox(wnd, message)
|
||||||
|
|
||||||
|
|
||||||
|
def get_member_memberid_userid(wnd, memberuserid):
|
||||||
|
"""Retrieve member attributes by member of user id."""
|
||||||
|
|
||||||
|
# connect the members module to its backends
|
||||||
|
if not members.connected(): members.connect()
|
||||||
|
|
||||||
|
# retrieve member data
|
||||||
|
|
||||||
|
if re.match('^[0-9]*$', memberuserid):
|
||||||
|
|
||||||
|
# numeric memberid, look it up
|
||||||
|
memberid = int(memberuserid)
|
||||||
|
member = members.get( memberid )
|
||||||
|
if not member:
|
||||||
|
msgbox(wnd, '%s is an invalid memberid' % memberuserid)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# non-numeric memberid: try userids
|
||||||
|
member = members.get_userid( memberuserid )
|
||||||
|
if not member:
|
||||||
|
msgbox(wnd, "%s is an invalid account userid" % memberuserid)
|
||||||
|
|
||||||
|
return member
|
||||||
|
|
||||||
|
|
||||||
|
def action_display_member(wnd):
|
||||||
|
"""Interactively display a member."""
|
||||||
|
|
||||||
|
# read the member id
|
||||||
|
prompt = 'Memberid: '
|
||||||
|
memberid = inputbox(wnd, prompt, 36)
|
||||||
|
|
||||||
|
if not memberid or memberid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
member = get_member_memberid_userid(wnd, memberid)
|
||||||
|
if not member: return
|
||||||
|
term_list = members.terms_list( member['memberid'] )
|
||||||
|
|
||||||
|
# display the details in a window
|
||||||
|
display_member_details(wnd, member, term_list)
|
||||||
|
|
||||||
|
|
||||||
|
def page(text):
|
||||||
|
|
||||||
|
try:
|
||||||
|
pipe = os.popen('/usr/bin/less', 'w')
|
||||||
|
pipe.write(text)
|
||||||
|
pipe.close()
|
||||||
|
except IOError:
|
||||||
|
# broken pipe (user didn't read the whole list)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def format_members(member_list):
|
||||||
|
"""Format a member list into a string."""
|
||||||
|
|
||||||
|
# clone and sort member_list
|
||||||
|
member_list = list(member_list)
|
||||||
|
member_list.sort( lambda x, y: x['memberid']-y['memberid'] )
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
|
||||||
|
for member in member_list:
|
||||||
|
attrs = ( member['memberid'], member['name'], member['studentid'],
|
||||||
|
member['type'], member['program'], member['userid'] )
|
||||||
|
buf += "%4d %50s %10s %10s \n%55s %10s\n\n" % attrs
|
||||||
|
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
def action_list_term(wnd):
|
||||||
|
"""Interactively list members registered in a term."""
|
||||||
|
|
||||||
|
term = None
|
||||||
|
|
||||||
|
# read the term
|
||||||
|
prompt = "Which term to list members for ([fws]20nn): "
|
||||||
|
while term == None or (not term == '' and not re.match('^[wsf][0-9]{4}$', term) and not term == 'exit'):
|
||||||
|
term = inputbox(wnd, prompt, 41)
|
||||||
|
|
||||||
|
# abort when exit is entered
|
||||||
|
if not term or term.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# connect the members module to its backends if necessary
|
||||||
|
if not members.connected(): members.connect()
|
||||||
|
|
||||||
|
# retrieve a list of members for term
|
||||||
|
member_list = members.list_term(term)
|
||||||
|
|
||||||
|
# format the data into a mess of text
|
||||||
|
buf = format_members(member_list)
|
||||||
|
|
||||||
|
# display the mass of text with a pager
|
||||||
|
page( buf )
|
||||||
|
|
||||||
|
|
||||||
|
def action_list_name(wnd):
|
||||||
|
|
||||||
|
name = None
|
||||||
|
|
||||||
|
# read the name
|
||||||
|
prompt = "Enter the member's name: "
|
||||||
|
name = inputbox(wnd, prompt, 41)
|
||||||
|
|
||||||
|
# abort when exit is entered
|
||||||
|
if not name or name.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# connect the members module to its backends if necessary
|
||||||
|
if not members.connected(): members.connect()
|
||||||
|
|
||||||
|
# retrieve a list of members for term
|
||||||
|
member_list = members.list_name(name)
|
||||||
|
|
||||||
|
# format the data into a mess of text
|
||||||
|
buf = format_members(member_list)
|
||||||
|
|
||||||
|
# display the mass of text with a pager
|
||||||
|
page( buf )
|
||||||
|
|
||||||
|
|
||||||
|
def action_list_studentid(wnd):
|
||||||
|
|
||||||
|
studentid = None
|
||||||
|
|
||||||
|
# read the studentid
|
||||||
|
prompt = "Enter the member's student id: "
|
||||||
|
studentid = inputbox(wnd, prompt, 41)
|
||||||
|
|
||||||
|
# abort when exit is entered
|
||||||
|
if not studentid or studentid.lower() == 'exit':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# connect the members module to its backends if necessary
|
||||||
|
if not members.connected(): members.connect()
|
||||||
|
|
||||||
|
# retrieve a list of members for term
|
||||||
|
member = members.get_studentid(studentid)
|
||||||
|
if member != None:
|
||||||
|
member_list = [ members.get_studentid(studentid) ]
|
||||||
|
else:
|
||||||
|
member_list = []
|
||||||
|
|
||||||
|
# format the data into a mess of text
|
||||||
|
buf = format_members(member_list)
|
||||||
|
|
||||||
|
# display the mass of text with a pager
|
||||||
|
page( buf )
|
||||||
|
|
||||||
|
|
||||||
|
def null_callback(wnd):
|
||||||
|
"""Callback for unimplemented menu options."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def exit_callback(wnd):
|
||||||
|
"""Callback for the exit option."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# the top level ceo menu
|
||||||
|
top_menu = [
|
||||||
|
( "New member", action_new_member ),
|
||||||
|
( "Register for a term", action_term_register ),
|
||||||
|
( "Register for multiple terms", action_term_register_multiple ),
|
||||||
|
( "Display a member", action_display_member ),
|
||||||
|
( "List members registered in a term", action_list_term ),
|
||||||
|
( "Search for a member by name", action_list_name ),
|
||||||
|
( "Search for a member by student id", action_list_studentid ),
|
||||||
|
( "Create an account", action_create_account ),
|
||||||
|
( "Re Create an account", null_callback ),
|
||||||
|
( "Library functions", null_callback ),
|
||||||
|
( "Exit", exit_callback ),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def acquire_ceo_wnd(screen=None):
|
||||||
|
"""Create the top level ceo window."""
|
||||||
|
|
||||||
|
# hack to get a reference to the entire screen
|
||||||
|
# even when the caller doesn't (shouldn't) have one
|
||||||
|
global _screen
|
||||||
|
if screen == None:
|
||||||
|
screen = _screen
|
||||||
|
else:
|
||||||
|
_screen = screen
|
||||||
|
|
||||||
|
# if the screen changes size, a mess may be left
|
||||||
|
screen.erase()
|
||||||
|
|
||||||
|
# for some reason, the legacy ceo system
|
||||||
|
# excluded the top line from its window
|
||||||
|
height, width = screen.getmaxyx()
|
||||||
|
ceo_wnd = screen.subwin(height-1, width, 1, 0)
|
||||||
|
|
||||||
|
# draw the border around the ceo window
|
||||||
|
curses.init_pair(1, BORDER_COLOR, -1)
|
||||||
|
color_attr = curses.color_pair(1) | curses.A_BOLD
|
||||||
|
ceo_wnd.attron(color_attr)
|
||||||
|
ceo_wnd.border()
|
||||||
|
ceo_wnd.attroff(color_attr)
|
||||||
|
|
||||||
|
# return window and dimensions of inner area
|
||||||
|
return ceo_wnd, 1, 1, height-2, width-2
|
||||||
|
|
||||||
|
|
||||||
|
def ceo_main_curses(screen):
|
||||||
|
"""Wrapped main for curses."""
|
||||||
|
|
||||||
|
curses.use_default_colors()
|
||||||
|
|
||||||
|
# workaround for SSH sessions on virtual consoles (reset terminal)
|
||||||
|
reset()
|
||||||
|
|
||||||
|
# create ceo window
|
||||||
|
ceo_wnd, menu_y, menu_x, menu_height, menu_width = acquire_ceo_wnd(screen)
|
||||||
|
|
||||||
|
# display the top level menu
|
||||||
|
menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
"""Main function for legacy UI."""
|
||||||
|
|
||||||
|
# wrap the entire program using curses.wrapper
|
||||||
|
# so that the terminal is restored to a sane state
|
||||||
|
# when the program exits
|
||||||
|
try:
|
||||||
|
curses.wrapper(ceo_main_curses)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
except curses.error:
|
||||||
|
print "Your screen is too small!"
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
reset()
|
||||||
|
raise
|
||||||
|
|
||||||
|
# clean up screen before exit
|
||||||
|
reset()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# $Id$
|
||||||
|
"""
|
||||||
|
Backends
|
||||||
|
|
||||||
|
This module contains backend interfaces and related modules.
|
||||||
|
CEO's primary backends are:
|
||||||
|
|
||||||
|
db.py - CEO's database for member and term registrations
|
||||||
|
ldapi.py - LDAP, for UNIX account metadata administration
|
||||||
|
krb.py - Kerberos, for UNIX account password administration
|
||||||
|
|
||||||
|
"""
|
|
@ -0,0 +1,548 @@
|
||||||
|
# $Id: db.py 37 2006-12-28 10:00:50Z mspang $
|
||||||
|
"""
|
||||||
|
Database Backend Interface
|
||||||
|
|
||||||
|
This module is intended to be a thin wrapper around CEO database operations.
|
||||||
|
Methods on the connection class correspond in a straightforward way to SQL
|
||||||
|
queries. These methods may restructure and clean up query output but may make
|
||||||
|
no other assumptions about its content or purpose.
|
||||||
|
|
||||||
|
This module makes use of the PygreSQL Python bindings to libpq,
|
||||||
|
PostgreSQL's native C client library.
|
||||||
|
"""
|
||||||
|
import pgdb
|
||||||
|
|
||||||
|
|
||||||
|
class DBException(Exception):
|
||||||
|
"""Exception class for database-related errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DBConnection(object):
|
||||||
|
"""
|
||||||
|
Connection to CEO's backend database. All database queries
|
||||||
|
and updates are made via this class.
|
||||||
|
|
||||||
|
Exceptions: (all methods)
|
||||||
|
DBException - on database query failure
|
||||||
|
|
||||||
|
Note: Updates will never take place until commit() is called.
|
||||||
|
|
||||||
|
Note: In the event that any method of this class raises a
|
||||||
|
DBException and that exception is caught, rollback()
|
||||||
|
must be called before further queries will be successful.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
connection = DBConnection()
|
||||||
|
connection.connect("localhost", "ceo")
|
||||||
|
|
||||||
|
# make queries and updates, i.e.
|
||||||
|
connection.insert_member("Calum T. Dalek")
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
connection.disconnect()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.cnx = None
|
||||||
|
self.cursor = None
|
||||||
|
|
||||||
|
|
||||||
|
def connect(self, hostname=None, database=None, username=None, password=None):
|
||||||
|
"""
|
||||||
|
Establishes the connection to CEO's PostgreSQL database.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
hostname - hostname:port to connect to
|
||||||
|
database - name of database
|
||||||
|
username - user to authenticate as
|
||||||
|
password - password of username
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.cnx: raise DBException("unable to connect: already connected")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cnx = pgdb.connect(host=hostname, database=database,
|
||||||
|
user=username, password=password)
|
||||||
|
self.cursor = self.cnx.cursor()
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("unable to connect: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Closes the connection to CEO's PostgreSQL database."""
|
||||||
|
|
||||||
|
if self.cursor:
|
||||||
|
self.cursor.close()
|
||||||
|
self.cursor = None
|
||||||
|
|
||||||
|
if self.cnx:
|
||||||
|
self.cnx.close()
|
||||||
|
self.cnx = None
|
||||||
|
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
"""Determine whether the connection has been established."""
|
||||||
|
|
||||||
|
return self.cnx != None
|
||||||
|
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
"""Commits the current transaction and starts a new one."""
|
||||||
|
|
||||||
|
self.cnx.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
"""Aborts the current transaction."""
|
||||||
|
|
||||||
|
self.cnx.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Member-related methods ###
|
||||||
|
|
||||||
|
def select_members(self, sql, params=None):
|
||||||
|
"""
|
||||||
|
Retrieves a list CSC members selected by given SQL statement.
|
||||||
|
|
||||||
|
This is a helper function that should generally not be called directly.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
sql - the SELECT sql statement
|
||||||
|
params - parameters for the SQL statement
|
||||||
|
|
||||||
|
The sql statement must select the six columns
|
||||||
|
(memberid, name, studentid, program, type, userid)
|
||||||
|
from the members table in that order.
|
||||||
|
|
||||||
|
Returns: a memberid-keyed dictionary whose values are
|
||||||
|
column-keyed dictionaries with member attributes
|
||||||
|
"""
|
||||||
|
|
||||||
|
# retrieve a list of all members
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
members_list = self.cursor.fetchall()
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("SELECT statement failed: %s" % e)
|
||||||
|
|
||||||
|
# build a dictionary of dictionaries from the result (a list of lists)
|
||||||
|
members_dict = {}
|
||||||
|
for member in members_list:
|
||||||
|
memberid, name, studentid, program, type, userid = member
|
||||||
|
members_dict[memberid] = {
|
||||||
|
'memberid': member[0],
|
||||||
|
'name': member[1],
|
||||||
|
'studentid': member[2],
|
||||||
|
'program': member[3],
|
||||||
|
'type': member[4],
|
||||||
|
'userid': member[5],
|
||||||
|
}
|
||||||
|
|
||||||
|
return members_dict
|
||||||
|
|
||||||
|
|
||||||
|
def select_single_member(self, sql, params=None):
|
||||||
|
"""
|
||||||
|
Retrieves a single member by memberid.
|
||||||
|
|
||||||
|
This is a helper function that should generally not be called directly.
|
||||||
|
|
||||||
|
See: self.select_members()
|
||||||
|
|
||||||
|
Returns: a column-keyed dictionary with member attributes, or
|
||||||
|
None if no member matching member exists
|
||||||
|
"""
|
||||||
|
|
||||||
|
# retrieve the member
|
||||||
|
results = self.select_members(sql, params)
|
||||||
|
|
||||||
|
# too many members returned
|
||||||
|
if len(results) > 1:
|
||||||
|
raise DBException("multiple members selected: sql='%s' params=%s" % (sql, repr(params)))
|
||||||
|
|
||||||
|
# no such member
|
||||||
|
elif len(results) < 1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# return the single match
|
||||||
|
memberid = results.keys()[0]
|
||||||
|
return results[memberid]
|
||||||
|
|
||||||
|
|
||||||
|
def select_all_members(self):
|
||||||
|
"""
|
||||||
|
Retrieves a list of all CSC members (past and present).
|
||||||
|
|
||||||
|
See: self.select_members()
|
||||||
|
|
||||||
|
Example: connection.select_all_members() -> {
|
||||||
|
0: { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
|
||||||
|
3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
sql = "SELECT memberid, name, studentid, program, type, userid FROM members"
|
||||||
|
return self.select_members(sql)
|
||||||
|
|
||||||
|
|
||||||
|
def select_members_by_name(self, name_re):
|
||||||
|
"""
|
||||||
|
Retrieves a list of all CSC members whose name matches name_re.
|
||||||
|
|
||||||
|
See: self.select_members()
|
||||||
|
|
||||||
|
Example: connection.select_members_by_name('Michael') -> {
|
||||||
|
3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE name ~* %s"
|
||||||
|
params = [ str(name_re) ]
|
||||||
|
|
||||||
|
return self.select_members(sql, params)
|
||||||
|
|
||||||
|
|
||||||
|
def select_members_by_term(self, term):
|
||||||
|
"""
|
||||||
|
Retrieves a list of all CSC members who were members in the specified term.
|
||||||
|
|
||||||
|
See: self.select_members()
|
||||||
|
|
||||||
|
Example: connection.select_members_by_term('f2006') -> {
|
||||||
|
3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
sql = "SELECT members.memberid, name, studentid, program, type, userid FROM members JOIN terms ON members.memberid=terms.memberid WHERE term=%s"
|
||||||
|
params = [ str(term) ]
|
||||||
|
|
||||||
|
return self.select_members(sql, params)
|
||||||
|
|
||||||
|
|
||||||
|
def select_member_by_id(self, memberid):
|
||||||
|
"""
|
||||||
|
Retrieves a single member by memberid.
|
||||||
|
|
||||||
|
See: self.select_single_member()
|
||||||
|
|
||||||
|
Example: connection.select_member_by_id(0) ->
|
||||||
|
{ 'memberid': 0, 'name': 'Calum T. Dalek' ...}
|
||||||
|
"""
|
||||||
|
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE memberid=%d"
|
||||||
|
params = [ int(memberid) ]
|
||||||
|
|
||||||
|
return self.select_single_member(sql, params)
|
||||||
|
|
||||||
|
|
||||||
|
def select_member_by_account(self, username):
|
||||||
|
"""
|
||||||
|
Retrieves a single member by UNIX account username.
|
||||||
|
|
||||||
|
See: self.select_single_member()
|
||||||
|
|
||||||
|
Example: connection.select_member_by_account('ctdalek') ->
|
||||||
|
{ 'memberid': 0, 'name': 'Calum T. Dalek' ...}
|
||||||
|
"""
|
||||||
|
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE userid=%s"
|
||||||
|
params = [ username ]
|
||||||
|
|
||||||
|
return self.select_single_member(sql, params)
|
||||||
|
|
||||||
|
|
||||||
|
def select_member_by_studentid(self, studentid):
|
||||||
|
"""
|
||||||
|
Retrieves a single member by student id number.
|
||||||
|
|
||||||
|
See: self.select_single_member()
|
||||||
|
|
||||||
|
Example: connection.select_member_by_studentid('nnnnnnnn') ->
|
||||||
|
{ 'memberid': 3349, 'name': 'Michael Spang' ...}
|
||||||
|
"""
|
||||||
|
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE studentid=%s"
|
||||||
|
params = [ studentid ]
|
||||||
|
|
||||||
|
return self.select_single_member(sql, params)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_member(self, name, studentid=None, program=None):
|
||||||
|
"""
|
||||||
|
Creates a member with the specified attributes.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
name - full name of member
|
||||||
|
studentid - student id number
|
||||||
|
program - program of study
|
||||||
|
|
||||||
|
Example: connection.insert_member('Michael Spang', '99999999', 'Math/CS') -> 3349
|
||||||
|
|
||||||
|
Returns: a memberid of created user
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# retrieve the next memberid
|
||||||
|
sql = "SELECT nextval('memberid_seq')"
|
||||||
|
self.cursor.execute(sql)
|
||||||
|
result = self.cursor.fetchone()
|
||||||
|
memberid = result[0]
|
||||||
|
|
||||||
|
# insert the member
|
||||||
|
sql = "INSERT INTO members (memberid, name, studentid, program, type) VALUES (%d, %s, %s, %s, %s)"
|
||||||
|
params = [ memberid, name, studentid, program, 'user' ]
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
|
||||||
|
return memberid
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("failed to create member: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_member(self, memberid):
|
||||||
|
"""
|
||||||
|
Deletes a member. Note that a member cannot
|
||||||
|
be deleted until it has been unregistered from
|
||||||
|
all terms.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
memberid - the member id number to delete
|
||||||
|
|
||||||
|
Example: connection.delete_member(3349)
|
||||||
|
"""
|
||||||
|
sql = "DELETE FROM members WHERE memberid=%d"
|
||||||
|
params = [ memberid ]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("DELETE statement failed: %s" %e)
|
||||||
|
|
||||||
|
|
||||||
|
def update_member(self, member):
|
||||||
|
"""
|
||||||
|
Modifies member attributes.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
member - a column-keyed dictionary with the new state of the member.
|
||||||
|
member['memberid'] must be present. ommitted columns
|
||||||
|
will not be changed. None is NULL.
|
||||||
|
|
||||||
|
Returns: the full new state of the member as a column-keyed dictionary
|
||||||
|
|
||||||
|
Example: connection.update_member({
|
||||||
|
'memberid': 3349,
|
||||||
|
'name': 'Michael C. Spang',
|
||||||
|
'program': 'CS!'
|
||||||
|
}) -> {
|
||||||
|
'memberid': 3349,
|
||||||
|
'name': 'Michael C. Spang',
|
||||||
|
'program': CS!',
|
||||||
|
'studentid': '99999999' # unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
Equivalent Example:
|
||||||
|
member = connection.select_member_by_id(3349)
|
||||||
|
member['name'] = 'Michael C. Spang'
|
||||||
|
member['program'] = 'CS!'
|
||||||
|
connection.update_member(member) -> { see above }
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
# memberid to update
|
||||||
|
memberid = member['memberid']
|
||||||
|
|
||||||
|
# retrieve current state of member
|
||||||
|
member_state = self.select_member_by_id(memberid)
|
||||||
|
|
||||||
|
# build a list of changes to make
|
||||||
|
changes = []
|
||||||
|
for column in member.keys():
|
||||||
|
if member[column] != member_state[column]:
|
||||||
|
|
||||||
|
# column's value must be updated
|
||||||
|
changes.append( (column, member[column]) )
|
||||||
|
member_state[column] = member[column]
|
||||||
|
|
||||||
|
# no changes?
|
||||||
|
if len(changes) < 1:
|
||||||
|
return member_state
|
||||||
|
|
||||||
|
# make the necessary changes in an update statement
|
||||||
|
changes = zip(*changes)
|
||||||
|
sql = "UPDATE members SET " + ", ".join(["%s=%%s"] * len(changes[0])) % changes[0] + " WHERE memberid=%d"
|
||||||
|
params = changes[1] + ( memberid, )
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
|
||||||
|
return member_state
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("member update failed: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Term-related methods ###
|
||||||
|
|
||||||
|
def select_term(self, memberid, term):
|
||||||
|
"""
|
||||||
|
Determines whether a member is registered for a term.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
memberid - the member id number
|
||||||
|
term - the term to check
|
||||||
|
|
||||||
|
Returns: a matching term, or None
|
||||||
|
|
||||||
|
Example: connection.select_term(3349, 'f2006') -> 'f2006'
|
||||||
|
"""
|
||||||
|
sql = "SELECT term FROM terms WHERE memberid=%d AND term=%s"
|
||||||
|
params = [ memberid, term ]
|
||||||
|
|
||||||
|
# retrieve matches
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
result = self.cursor.fetchall()
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("SELECT statement failed: %s" % e)
|
||||||
|
|
||||||
|
if len(result) > 1:
|
||||||
|
raise DBException("multiple rows in terms with memberid=%d term=%s" % (memberid, term))
|
||||||
|
elif len(result) == 0:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return result[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def select_terms(self, memberid):
|
||||||
|
"""
|
||||||
|
Retrieves a list of terms a member is registered for.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
memberid - the member id number
|
||||||
|
|
||||||
|
Returns: a sorted list of terms
|
||||||
|
|
||||||
|
Example: connection.select_terms(3349) -> ['f2006']
|
||||||
|
"""
|
||||||
|
sql = "SELECT term FROM terms WHERE memberid=%d"
|
||||||
|
params = [ memberid ]
|
||||||
|
|
||||||
|
# retrieve the list of terms
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
result = self.cursor.fetchall()
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("SELECT statement failed: %s" % e)
|
||||||
|
|
||||||
|
result = [ row[0] for row in result ]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def insert_term(self, memberid, term):
|
||||||
|
"""
|
||||||
|
Registers a member for a term.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
memberid - the member id number to register
|
||||||
|
term - string representation of the term
|
||||||
|
|
||||||
|
Example: connection.insert_term(3349, 'f2006')
|
||||||
|
"""
|
||||||
|
sql = "INSERT INTO terms (memberid, term) VALUES (%d, %s)"
|
||||||
|
params = [ memberid, term ]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("INSERT statement failed: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_term(self, memberid, term):
|
||||||
|
"""
|
||||||
|
Unregisters a member for a term.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
memberid - the member id number to register
|
||||||
|
term - string representation of the term
|
||||||
|
|
||||||
|
Example: connection.delete_term(3349, 'f2006')
|
||||||
|
"""
|
||||||
|
sql = "DELETE FROM terms WHERE memberid=%d and term=%s"
|
||||||
|
params = [ memberid, term ]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("DELETE statement failed: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_term_all(self, memberid):
|
||||||
|
"""
|
||||||
|
Unregisters a member for all registered terms.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
memberid - the member id number to unregister
|
||||||
|
|
||||||
|
Example: connection.delete_term_all(3349)
|
||||||
|
"""
|
||||||
|
sql = "DELETE FROM terms WHERE memberid=%d"
|
||||||
|
params = [ memberid ]
|
||||||
|
|
||||||
|
# retrieve a list of terms
|
||||||
|
try:
|
||||||
|
self.cursor.execute(sql, params)
|
||||||
|
except pgdb.Error, e:
|
||||||
|
raise DBException("DELETE statement failed: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous methods ###
|
||||||
|
|
||||||
|
def trim_memberid_sequence(self):
|
||||||
|
"""
|
||||||
|
Sets the value of the member id sequence to the id of the newest
|
||||||
|
member. For use after extensive testing to prevent large
|
||||||
|
intervals of unused memberids.
|
||||||
|
|
||||||
|
Note: this does nothing unless the most recently added member(s) have been deleted
|
||||||
|
"""
|
||||||
|
self.cursor.execute("SELECT setval('memberid_seq', (SELECT max(memberid) FROM members))")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Tests ###
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
HOST = "localhost"
|
||||||
|
DATABASE = "ceo"
|
||||||
|
|
||||||
|
connection = DBConnection()
|
||||||
|
|
||||||
|
print "Running disconnect()"
|
||||||
|
connection.disconnect()
|
||||||
|
|
||||||
|
print "Running connect('%s', '%s')" % (HOST, DATABASE)
|
||||||
|
connection.connect(HOST, DATABASE)
|
||||||
|
|
||||||
|
print "Running select_all_members()", "->", len(connection.select_all_members()), "members"
|
||||||
|
print "Running select_member_by_id(0)", "->", connection.select_member_by_id(0)['userid']
|
||||||
|
print "Running select_members_by_name('Spang')", "->", connection.select_members_by_name('Spang').keys()
|
||||||
|
print "Running select_members_by_term('f2006')", "->", "[" + ", ".join(map(str, connection.select_members_by_term('f2006').keys()[0:10])) + " ...]"
|
||||||
|
|
||||||
|
print "Running insert_member('test_member', '99999999', 'program')",
|
||||||
|
memberid = connection.insert_member('test_member', '99999999', 'program')
|
||||||
|
print "->", memberid
|
||||||
|
|
||||||
|
print "Running select_member_by_id(%d)" % memberid, "->", connection.select_member_by_id(memberid)
|
||||||
|
print "Running insert_term(%d, 'f2006')" % memberid
|
||||||
|
connection.insert_term(memberid, 'f2006')
|
||||||
|
|
||||||
|
print "Running select_terms(%d)" % memberid, "->", connection.select_terms(memberid)
|
||||||
|
print "Running update_member({'memberid':%d,'name':'test_updated','studentid':-1})" % memberid
|
||||||
|
connection.update_member({'memberid':memberid,'name':'test_updated','studentid':99999999})
|
||||||
|
print "Running select_member_by_id(%d)" % memberid, "->", connection.select_member_by_id(memberid)
|
||||||
|
|
||||||
|
print "Running rollback()"
|
||||||
|
connection.rollback()
|
||||||
|
|
||||||
|
print "Resetting memberid sequence"
|
||||||
|
connection.trim_memberid_sequence()
|
||||||
|
|
||||||
|
print "Running disconnect()"
|
||||||
|
connection.disconnect()
|
|
@ -0,0 +1,222 @@
|
||||||
|
# $Id: ipc.py 26 2006-12-20 21:25:08Z mspang $
|
||||||
|
"""
|
||||||
|
IPC Library Functions
|
||||||
|
|
||||||
|
This module contains very UNIX-specific code to allow interactive
|
||||||
|
communication with another program. For CEO they are required to
|
||||||
|
talk to kadmin because there is no Kerberos administration Python
|
||||||
|
module. Real bindings to libkadm5 are doable and thus a TODO.
|
||||||
|
"""
|
||||||
|
import os, pty, select
|
||||||
|
|
||||||
|
|
||||||
|
class _pty_file(object):
|
||||||
|
"""
|
||||||
|
A 'file'-like wrapper class for pseudoterminal file descriptors.
|
||||||
|
|
||||||
|
This wrapper is necessary because Python has a nasty
|
||||||
|
habit of throwing OSError at pty EOF.
|
||||||
|
|
||||||
|
This class also implements timeouts for read operations
|
||||||
|
which are handy for avoiding deadlock when both
|
||||||
|
processes are blocked in a read().
|
||||||
|
|
||||||
|
See the Python documentation of the file class
|
||||||
|
for explanation of the methods.
|
||||||
|
"""
|
||||||
|
def __init__(self, fd):
|
||||||
|
self.fd = fd
|
||||||
|
self.buffer = ''
|
||||||
|
self.closed = False
|
||||||
|
def __repr__(self):
|
||||||
|
status='open'
|
||||||
|
if self.closed:
|
||||||
|
status = 'closed'
|
||||||
|
return "<" + status + " pty '" + os.ttyname(self.fd) + "'>"
|
||||||
|
def read(self, size=-1, block=True, timeout=0.1):
|
||||||
|
if self.closed: raise ValueError
|
||||||
|
if size < 0:
|
||||||
|
data = None
|
||||||
|
|
||||||
|
# read data, catching OSError as EOF
|
||||||
|
try:
|
||||||
|
while data != '':
|
||||||
|
|
||||||
|
# wait timeout for the pty to become ready, otherwise stop reading
|
||||||
|
if not block and len(select.select([self.fd],[],[], timeout)[0]) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
data = os.read(self.fd, 65536)
|
||||||
|
self.buffer += data
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = self.buffer
|
||||||
|
self.buffer = ''
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
if len(self.buffer) < size:
|
||||||
|
|
||||||
|
# read data, catching OSError as EOF
|
||||||
|
try:
|
||||||
|
|
||||||
|
# wait timeout for the pty to become ready, then read
|
||||||
|
if block or len(select.select([self.fd],[],[], timeout)[0]) != 0:
|
||||||
|
self.buffer += os.read(self.fd, size - len(self.buffer) )
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = self.buffer[:size]
|
||||||
|
self.buffer = self.buffer[size:]
|
||||||
|
return data
|
||||||
|
def readline(self, size=-1, block=True, timeout=0.1):
|
||||||
|
data = None
|
||||||
|
|
||||||
|
# read data, catching OSError as EOF
|
||||||
|
try:
|
||||||
|
while data != '' and self.buffer.find("\n") == -1 and (size < 0 or len(self.buffer) < size):
|
||||||
|
|
||||||
|
# wait timeout for the pty to become ready, otherwise stop reading
|
||||||
|
if not block and len(select.select([self.fd],[],[], timeout)[0]) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
data = os.read(self.fd, 128)
|
||||||
|
self.buffer += data
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
split_index = self.buffer.find("\n") + 1
|
||||||
|
if split_index < 0:
|
||||||
|
split_index = len(self.buffer)
|
||||||
|
if size >= 0 and split_index > size:
|
||||||
|
split_index = size
|
||||||
|
line = self.buffer[:split_index]
|
||||||
|
self.buffer = self.buffer[split_index:]
|
||||||
|
return line
|
||||||
|
def readlines(self, sizehint=None, block=True, timeout=0.1):
|
||||||
|
lines = []
|
||||||
|
line = None
|
||||||
|
while True:
|
||||||
|
line = self.readline(-1, False, timeout)
|
||||||
|
if line == '': break
|
||||||
|
lines.append(line)
|
||||||
|
return lines
|
||||||
|
def write(self, data):
|
||||||
|
if self.closed: raise ValueError
|
||||||
|
os.write(self.fd, data)
|
||||||
|
def writelines(self, lines):
|
||||||
|
for line in lines:
|
||||||
|
self.write(line)
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
def next(self):
|
||||||
|
line = self.readline()
|
||||||
|
if line == '':
|
||||||
|
raise StopIteration
|
||||||
|
return line
|
||||||
|
def isatty(self):
|
||||||
|
if self.closed: raise ValueError
|
||||||
|
return os.isatty(self.fd)
|
||||||
|
def fileno(self):
|
||||||
|
if self.closed: raise ValueError
|
||||||
|
return self.fd
|
||||||
|
def flush(self):
|
||||||
|
if self.closed: raise ValueError
|
||||||
|
os.fsync(self.fd)
|
||||||
|
def close(self):
|
||||||
|
if not self.closed: os.close(self.fd)
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
|
||||||
|
def popeni(command, args, env=None):
|
||||||
|
"""
|
||||||
|
Open an interactive session with another command.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command - the command to run (full path)
|
||||||
|
args - a list of arguments to pass to command
|
||||||
|
env - optional environment for command
|
||||||
|
|
||||||
|
Returns: (pid, stdout, stdIn)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# use a pipe to send data to the child
|
||||||
|
child_stdin, parent_stdin = os.pipe()
|
||||||
|
|
||||||
|
# a pipe for receiving data would cause buffering and
|
||||||
|
# is therefore not suitable for interactive communication
|
||||||
|
# i.e. parent_stdout, child_stdout = os.pipe()
|
||||||
|
|
||||||
|
# therefore a pty must be used instead
|
||||||
|
master, slave = pty.openpty()
|
||||||
|
|
||||||
|
# collect both stdout and stderr on the pty
|
||||||
|
parent_stdout, child_stdout = master, slave
|
||||||
|
parent_stderr, child_stderr = master, slave
|
||||||
|
|
||||||
|
# fork the child to communicate with
|
||||||
|
pid = os.fork()
|
||||||
|
|
||||||
|
# child process
|
||||||
|
if pid == 0:
|
||||||
|
|
||||||
|
# close all of the parent's fds
|
||||||
|
os.close(parent_stdin)
|
||||||
|
if parent_stdout != parent_stdin:
|
||||||
|
os.close(parent_stdout)
|
||||||
|
if parent_stderr != parent_stdin and parent_stderr != parent_stdout:
|
||||||
|
os.close(parent_stderr)
|
||||||
|
|
||||||
|
# if stdout is a terminal, set it to the controlling terminal
|
||||||
|
if os.isatty(child_stdout):
|
||||||
|
|
||||||
|
# determine the filename of the tty
|
||||||
|
tty = os.ttyname(child_stdout)
|
||||||
|
|
||||||
|
# create a new session to disconnect
|
||||||
|
# from the parent's controlling terminal
|
||||||
|
os.setsid()
|
||||||
|
|
||||||
|
# set the controlling terminal to the pty
|
||||||
|
# by opening it (and closing it again since
|
||||||
|
# it's already open as child_stdout)
|
||||||
|
fd = os.open(tty, os.O_RDWR);
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
|
# init stdin/out/err
|
||||||
|
os.dup2(child_stdin, 0)
|
||||||
|
os.dup2(child_stdout, 1)
|
||||||
|
if child_stderr >= 0:
|
||||||
|
os.dup2(child_stderr, 2)
|
||||||
|
|
||||||
|
# finally, execute the child
|
||||||
|
if env:
|
||||||
|
os.execv(command, args, env)
|
||||||
|
else:
|
||||||
|
os.execv(command, args)
|
||||||
|
|
||||||
|
# parent process
|
||||||
|
else:
|
||||||
|
|
||||||
|
# close all of the child's fds
|
||||||
|
os.close(child_stdin)
|
||||||
|
if child_stdout != child_stdin:
|
||||||
|
os.close(child_stdout)
|
||||||
|
if child_stderr >= 0 and child_stderr != child_stdin and child_stderr != child_stdout:
|
||||||
|
os.close(child_stderr)
|
||||||
|
|
||||||
|
return pid, _pty_file(parent_stdout), os.fdopen(parent_stdin, 'w')
|
||||||
|
|
||||||
|
|
||||||
|
### Tests ###
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
import sys
|
||||||
|
pid, recv, send = popeni('/usr/sbin/kadmin.local', ['kadmin'])
|
||||||
|
|
||||||
|
send.write("listprincs\n")
|
||||||
|
send.flush()
|
||||||
|
|
||||||
|
print recv.readlines()
|
|
@ -0,0 +1,448 @@
|
||||||
|
# $Id: krb.py 40 2006-12-29 00:40:31Z mspang $
|
||||||
|
"""
|
||||||
|
Kerberos Backend Interface
|
||||||
|
|
||||||
|
This module is intended to be a thin wrapper around Kerberos operations.
|
||||||
|
Methods on the connection object correspond in a straightforward way to
|
||||||
|
calls to the Kerberos Master server.
|
||||||
|
|
||||||
|
A Kerberos principal is the second half of a CSC UNIX account. The principal
|
||||||
|
stores the user's password and and is used for all authentication on CSC
|
||||||
|
systems. Accounts that do not authenticate (e.g. club accounts) do not need
|
||||||
|
a Kerberos principal.
|
||||||
|
|
||||||
|
Unfortunately, there are no Python bindings to libkadm at this time. As a
|
||||||
|
temporary workaround, This module communicates with the kadmin CLI interface
|
||||||
|
via a pseudoterminal and pipe.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import ipc
|
||||||
|
|
||||||
|
|
||||||
|
class KrbException(Exception):
|
||||||
|
"""Exception class for all Kerberos-related errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KrbConnection(object):
|
||||||
|
"""
|
||||||
|
Connection to the Kerberos master server (kadmind). All Kerberos
|
||||||
|
principal updates are made via this class.
|
||||||
|
|
||||||
|
Exceptions: (all methods)
|
||||||
|
KrbException - on query/update failure
|
||||||
|
|
||||||
|
Example:
|
||||||
|
connection = KrbConnection()
|
||||||
|
connection.connect(...)
|
||||||
|
|
||||||
|
# make queries and updates, e.g.
|
||||||
|
connection.delete_principal("mspang")
|
||||||
|
|
||||||
|
connection.disconnect()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.pid = None
|
||||||
|
|
||||||
|
|
||||||
|
def connect(self, principal, keytab):
|
||||||
|
"""
|
||||||
|
Establishes the connection to the Kerberos master server.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
principal - the Kerberos princiapl to authenticate as
|
||||||
|
keytab - keytab filename for authentication
|
||||||
|
|
||||||
|
Example: connection.connect('ceo/admin@CSCLUB.UWATERLOO.CA', '/etc/ceo.keytab')
|
||||||
|
"""
|
||||||
|
|
||||||
|
# check keytab
|
||||||
|
if not os.access(keytab, os.R_OK):
|
||||||
|
raise KrbException("cannot access Kerberos keytab: %s" % keytab)
|
||||||
|
|
||||||
|
# command to run
|
||||||
|
kadmin = '/usr/sbin/kadmin'
|
||||||
|
kadmin_args = ['kadmin', '-p', principal, '-kt', keytab]
|
||||||
|
|
||||||
|
# fork the kadmin command
|
||||||
|
self.pid, self.kadm_out, self.kadm_in = ipc.popeni(kadmin, kadmin_args)
|
||||||
|
|
||||||
|
# read welcome messages
|
||||||
|
welcome = self.read_result()
|
||||||
|
|
||||||
|
# sanity checks on welcome messages
|
||||||
|
for line in welcome:
|
||||||
|
|
||||||
|
# ignore auth message
|
||||||
|
if line.find("Authenticating") == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ignore log file message
|
||||||
|
elif line.find("kadmin.log") != -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# error message?
|
||||||
|
else:
|
||||||
|
raise KrbException("unexpected kadmin output: " + welcome[0])
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Close the connection to the master server."""
|
||||||
|
|
||||||
|
if self.pid:
|
||||||
|
|
||||||
|
# close the pipe connected to kadmin's standard input
|
||||||
|
self.kadm_in.close()
|
||||||
|
|
||||||
|
# close the master pty connected to kadmin's stdout
|
||||||
|
try:
|
||||||
|
self.kadm_out.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# wait for kadmin to terminate
|
||||||
|
os.waitpid(self.pid, 0)
|
||||||
|
self.pid = None
|
||||||
|
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
"""Determine whether the connection has been established."""
|
||||||
|
|
||||||
|
return self.pid != None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Helper Methods ###
|
||||||
|
|
||||||
|
def read_result(self):
|
||||||
|
"""
|
||||||
|
Helper function to read output of kadmin until it
|
||||||
|
prompts for input.
|
||||||
|
|
||||||
|
Returns: a list of lines returned by kadmin
|
||||||
|
"""
|
||||||
|
|
||||||
|
# list of lines output by kadmin
|
||||||
|
result = []
|
||||||
|
|
||||||
|
# the kadmin prompt that signals the end output
|
||||||
|
# note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ
|
||||||
|
prompt = "kadmin:"
|
||||||
|
|
||||||
|
# timeout variables. the timeout will start at timeout and
|
||||||
|
# increase up to max_timeout when read() returns nothing (i.e., times out)
|
||||||
|
timeout = 0.01
|
||||||
|
timeout_increment = 0.10
|
||||||
|
timeout_maximum = 1.00
|
||||||
|
|
||||||
|
# input loop: read from kadmin until the kadmin prompt
|
||||||
|
buffer = ''
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# attempt to read any available data
|
||||||
|
data = self.kadm_out.read(block=False, timeout=timeout)
|
||||||
|
buffer += data
|
||||||
|
|
||||||
|
# nothing was read
|
||||||
|
if data == '':
|
||||||
|
|
||||||
|
# so wait longer for data next time
|
||||||
|
if timeout < timeout_maximum:
|
||||||
|
timeout += timeout_increment
|
||||||
|
continue
|
||||||
|
|
||||||
|
# give up after too much waiting
|
||||||
|
else:
|
||||||
|
|
||||||
|
# check kadmin status
|
||||||
|
status = os.waitpid(self.pid, os.WNOHANG)
|
||||||
|
if status[0] == 0:
|
||||||
|
|
||||||
|
# kadmin still alive
|
||||||
|
raise KrbException("timeout while reading response from kadmin")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# kadmin died!
|
||||||
|
raise KrbException("kadmin died while reading response")
|
||||||
|
|
||||||
|
# break into lines and save all but the final
|
||||||
|
# line (which is incomplete) into result
|
||||||
|
lines = buffer.split("\n")
|
||||||
|
buffer = lines[-1]
|
||||||
|
lines = lines[:-1]
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
result.append(line)
|
||||||
|
|
||||||
|
# if the incomplete lines in the buffer is the kadmin prompt,
|
||||||
|
# then the result is complete and may be returned
|
||||||
|
if buffer.strip() == prompt:
|
||||||
|
break
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def execute(self, command):
|
||||||
|
"""
|
||||||
|
Helper function to execute a kadmin command.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
command - the command to execute
|
||||||
|
|
||||||
|
Returns: a list of lines output by the command
|
||||||
|
"""
|
||||||
|
|
||||||
|
# there should be no remaining output from the previous
|
||||||
|
# command. if there is then something is broken.
|
||||||
|
stale_output = self.kadm_out.read(block=False, timeout=0)
|
||||||
|
if stale_output != '':
|
||||||
|
raise KrbException("unexpected kadmin output: " + stale_output)
|
||||||
|
|
||||||
|
# send the command to kadmin
|
||||||
|
self.kadm_in.write(command + "\n")
|
||||||
|
self.kadm_in.flush()
|
||||||
|
|
||||||
|
# read the command output and return it
|
||||||
|
result = self.read_result()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Commands ###
|
||||||
|
|
||||||
|
def list_principals(self):
|
||||||
|
"""
|
||||||
|
Retrieve a list of Kerberos principals.
|
||||||
|
|
||||||
|
Returns: a list of principals
|
||||||
|
|
||||||
|
Example: connection.list_principals() -> [
|
||||||
|
"ceo/admin@CSCLUB.UWATERLOO.CA",
|
||||||
|
"sysadmin/admin@CSCLUB.UWATERLOO.CA",
|
||||||
|
"mspang@CSCLUB.UWATERLOO.CA",
|
||||||
|
]
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
principals = self.execute("list_principals")
|
||||||
|
|
||||||
|
# assuming that there at least some host principals
|
||||||
|
if len(principals) < 1:
|
||||||
|
raise KrbException("no kerberos principals")
|
||||||
|
|
||||||
|
# detect error message
|
||||||
|
if principals[0].find("kadmin:") == 0:
|
||||||
|
raise KrbException("list_principals returned error: " + principals[0])
|
||||||
|
|
||||||
|
# verify principals are well-formed
|
||||||
|
for principal in principals:
|
||||||
|
if principal.find("@") == -1:
|
||||||
|
raise KrbException('malformed pricipal: "' + principal + '"')
|
||||||
|
|
||||||
|
return principals
|
||||||
|
|
||||||
|
|
||||||
|
def get_principal(self, principal):
|
||||||
|
"""
|
||||||
|
Retrieve principal details.
|
||||||
|
|
||||||
|
Returns: a dictionary of principal attributes
|
||||||
|
|
||||||
|
Example: connection.get_principal("ceo/admin@CSCLUB.UWATERLOO.CA") -> {
|
||||||
|
"Principal": "ceo/admin@CSCLUB.UWATERLOO.CA",
|
||||||
|
"Policy": "[none]",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = self.execute('get_principal "' + principal + '"')
|
||||||
|
|
||||||
|
# detect error message
|
||||||
|
if output[0].find("kadmin:") == 0:
|
||||||
|
raise KrbException("get_principal returned error: " + output[0])
|
||||||
|
|
||||||
|
# detect more errors
|
||||||
|
if output[0].find("get_principal: ") == 0:
|
||||||
|
|
||||||
|
message = output[0][15:]
|
||||||
|
|
||||||
|
# principal does not exist => None
|
||||||
|
if message.find("Principal does not exist") == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# dictionary to store attributes
|
||||||
|
principal_attributes = {}
|
||||||
|
|
||||||
|
# attributes that will not be returned
|
||||||
|
ignore_attributes = ['Key']
|
||||||
|
|
||||||
|
# split output into a dictionary of attributes
|
||||||
|
for line in output:
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
value = value.strip()
|
||||||
|
if not key in ignore_attributes:
|
||||||
|
principal_attributes[key] = value
|
||||||
|
|
||||||
|
return principal_attributes
|
||||||
|
|
||||||
|
|
||||||
|
def get_privs(self):
|
||||||
|
"""
|
||||||
|
Retrieve privileges of the current principal.
|
||||||
|
|
||||||
|
Returns: a list of privileges
|
||||||
|
|
||||||
|
Example: connection.get_privs() ->
|
||||||
|
[ "GET", "ADD", "MODIFY", "DELETE" ]
|
||||||
|
"""
|
||||||
|
|
||||||
|
output = self.execute("get_privs")
|
||||||
|
|
||||||
|
# one line of output is expected
|
||||||
|
if len(output) > 1:
|
||||||
|
raise KrbException("unexpected output of get_privs: " + output[1])
|
||||||
|
|
||||||
|
# detect error message
|
||||||
|
if output[0].find("kadmin:") == 0:
|
||||||
|
raise KrbException("get_privs returned error: " + output[0])
|
||||||
|
|
||||||
|
# parse output by removing the prefix and splitting it around spaces
|
||||||
|
if output[0][:20] != "current privileges: ":
|
||||||
|
raise KrbException("malformed get_privs output: " + output[0])
|
||||||
|
privs = output[0][20:].split(" ")
|
||||||
|
|
||||||
|
return privs
|
||||||
|
|
||||||
|
|
||||||
|
def add_principal(self, principal, password):
|
||||||
|
"""
|
||||||
|
Create a new principal.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
principal - the name of the principal
|
||||||
|
password - the principal's initial password
|
||||||
|
|
||||||
|
Example: connection.add_principal("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# exec the add_principal command
|
||||||
|
if password.find('"') == -1:
|
||||||
|
self.kadm_in.write('add_principal -pw "' + password + '" "' + principal + '"\n')
|
||||||
|
|
||||||
|
# fools at MIT didn't bother implementing escaping, so passwords
|
||||||
|
# that contain double quotes must be treated specially
|
||||||
|
else:
|
||||||
|
self.kadm_in.write('add_principal "' + principal + '"\n')
|
||||||
|
self.kadm_in.write(password + "\n" + password + "\n")
|
||||||
|
|
||||||
|
# send request and read response
|
||||||
|
self.kadm_in.flush()
|
||||||
|
output = self.read_result()
|
||||||
|
|
||||||
|
# verify output
|
||||||
|
created = False
|
||||||
|
for line in output:
|
||||||
|
|
||||||
|
# ignore NOTICE lines
|
||||||
|
if line.find("NOTICE:") == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ignore prompts
|
||||||
|
elif line.find("Enter password") == 0 or line.find("Re-enter password") == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# record whether success message was encountered
|
||||||
|
elif line.find("Principal") == 0 and line.find("created.") != 0:
|
||||||
|
created = True
|
||||||
|
|
||||||
|
# error messages
|
||||||
|
elif line.find("add_principal:") == 0 or line.find("kadmin:") == 0:
|
||||||
|
|
||||||
|
# principal exists
|
||||||
|
if line.find("already exists") != -1:
|
||||||
|
raise KrbException("principal already exists")
|
||||||
|
|
||||||
|
# misc errors
|
||||||
|
else:
|
||||||
|
raise KrbException(line)
|
||||||
|
|
||||||
|
# unknown output
|
||||||
|
else:
|
||||||
|
raise KrbException("unexpected add_principal output: " + line)
|
||||||
|
|
||||||
|
# ensure success message was received
|
||||||
|
if not created:
|
||||||
|
raise KrbException("did not receive principal created in response")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_principal(self, principal):
|
||||||
|
"""
|
||||||
|
Delete a principal.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
principal - the principal name
|
||||||
|
|
||||||
|
Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# exec the delete_principal command and read response
|
||||||
|
self.kadm_in.write('delete_principal -force "' + principal + '"\n')
|
||||||
|
self.kadm_in.flush()
|
||||||
|
output = self.read_result()
|
||||||
|
|
||||||
|
# verify output
|
||||||
|
deleted = False
|
||||||
|
for line in output:
|
||||||
|
|
||||||
|
# ignore reminder
|
||||||
|
if line.find("Make sure that") == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# record whether success message was encountered
|
||||||
|
elif line.find("Principal") == 0 and line.find("deleted.") != -1:
|
||||||
|
deleted = True
|
||||||
|
|
||||||
|
# error messages
|
||||||
|
elif line.find("delete_principal:") == 0 or line.find("kadmin:") == 0:
|
||||||
|
|
||||||
|
# principal exists
|
||||||
|
if line.find("does not exist") != -1:
|
||||||
|
raise KrbException("principal does not exist")
|
||||||
|
|
||||||
|
# misc errors
|
||||||
|
else:
|
||||||
|
raise KrbException(line)
|
||||||
|
|
||||||
|
# unknown output
|
||||||
|
else:
|
||||||
|
raise KrbException("unexpected delete_principal output: " + line)
|
||||||
|
|
||||||
|
# ensure success message was received
|
||||||
|
if not deleted:
|
||||||
|
raise KrbException("did not receive principal deleted")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Tests ###
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
PRINCIPAL = 'ceo/admin@CSCLUB.UWATERLOO.CA'
|
||||||
|
KEYTAB = 'ceo.keytab'
|
||||||
|
|
||||||
|
connection = KrbConnection()
|
||||||
|
print "running disconnect()"
|
||||||
|
connection.disconnect()
|
||||||
|
print "running connect('%s', '%s')" % (PRINCIPAL, KEYTAB)
|
||||||
|
connection.connect(PRINCIPAL, KEYTAB)
|
||||||
|
print "running list_principals()", "->", "[" + ", ".join(map(repr,connection.list_principals()[0:3])) + " ...]"
|
||||||
|
print "running get_privs()", "->", str(connection.get_privs())
|
||||||
|
print "running add_principal('testtest', 'BLAH')"
|
||||||
|
connection.add_principal("testtest", "FJDSLDLFKJSF")
|
||||||
|
print "running get_principal('testtest')", "->", '(' + connection.get_principal("testtest")['Principal'] + ')'
|
||||||
|
print "running delete_principal('testtest')"
|
||||||
|
connection.delete_principal("testtest")
|
||||||
|
print "running disconnect()"
|
||||||
|
connection.disconnect()
|
||||||
|
|
|
@ -0,0 +1,577 @@
|
||||||
|
# $Id: ldapi.py 41 2006-12-29 04:22:31Z mspang $
|
||||||
|
"""
|
||||||
|
LDAP Backend Interface
|
||||||
|
|
||||||
|
This module is intended to be a thin wrapper around LDAP operations.
|
||||||
|
Methods on the connection object correspond in a straightforward way
|
||||||
|
to LDAP queries and updates.
|
||||||
|
|
||||||
|
A LDAP entry is the most important component of a CSC UNIX account.
|
||||||
|
The entry contains the username, user id number, real name, shell,
|
||||||
|
and other important information. All non-local UNIX accounts must
|
||||||
|
have an LDAP entry, even if the account does not log in directly.
|
||||||
|
|
||||||
|
This module makes use of python-ldap, a Python module with bindings
|
||||||
|
to libldap, OpenLDAP's native C client library.
|
||||||
|
"""
|
||||||
|
import ldap.modlist
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPException(Exception):
|
||||||
|
"""Exception class for LDAP-related errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPConnection(object):
|
||||||
|
"""
|
||||||
|
Connection to the LDAP directory. All directory
|
||||||
|
queries and updates are made via this class.
|
||||||
|
|
||||||
|
Exceptions: (all methods)
|
||||||
|
LDAPException - on directory query failure
|
||||||
|
|
||||||
|
Example:
|
||||||
|
connection = LDAPConnection()
|
||||||
|
connection.connect(...)
|
||||||
|
|
||||||
|
# make queries and updates, e.g.
|
||||||
|
connection.user_delete('mspang')
|
||||||
|
|
||||||
|
connection.disconnect()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.ldap = None
|
||||||
|
|
||||||
|
|
||||||
|
def connect(self, server, bind_dn, bind_pw, user_base, group_base):
|
||||||
|
"""
|
||||||
|
Establish a connection to the LDAP Server.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
server - connection string (e.g. ldap://foo.com, ldaps://bar.com)
|
||||||
|
bind_dn - distinguished name to bind to
|
||||||
|
bind_pw - password of bind_dn
|
||||||
|
user_base - base of the users subtree
|
||||||
|
group_base - baes of the group subtree
|
||||||
|
|
||||||
|
Example: connect('ldaps:///', 'cn=ceo,dc=csclub,dc=uwaterloo,dc=ca',
|
||||||
|
'secret', 'ou=People,dc=csclub,dc=uwaterloo,dc=ca',
|
||||||
|
'ou=Group,dc=csclub,dc=uwaterloo,dc=ca')
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if bind_pw == None: bind_pw = ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# open the connection
|
||||||
|
self.ldap = ldap.initialize(server)
|
||||||
|
|
||||||
|
# authenticate as ceo
|
||||||
|
self.ldap.simple_bind_s(bind_dn, bind_pw)
|
||||||
|
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to connect: %s" % e)
|
||||||
|
|
||||||
|
self.user_base = user_base
|
||||||
|
self.group_base = group_base
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Close the connection to the LDAP server."""
|
||||||
|
|
||||||
|
if self.ldap:
|
||||||
|
|
||||||
|
# close connection
|
||||||
|
try:
|
||||||
|
self.ldap.unbind_s()
|
||||||
|
self.ldap = None
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to disconnect: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def connected(self):
|
||||||
|
"""Determine whether the connection has been established."""
|
||||||
|
|
||||||
|
return self.ldap != None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Helper Methods ###
|
||||||
|
|
||||||
|
def lookup(self, dn):
|
||||||
|
"""
|
||||||
|
Helper method to retrieve the attributes of an entry.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
dn - the distinguished name of the directory entry
|
||||||
|
|
||||||
|
Returns: a dictionary of attributes of the matched dn, or
|
||||||
|
None of the dn does not exist in the directory
|
||||||
|
"""
|
||||||
|
|
||||||
|
# search for the specified dn
|
||||||
|
try:
|
||||||
|
matches = self.ldap.search_s(dn, ldap.SCOPE_BASE)
|
||||||
|
except ldap.NO_SUCH_OBJECT:
|
||||||
|
return None
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to lookup dn %s: %s" % (dn, e))
|
||||||
|
|
||||||
|
# this should never happen due to the nature of DNs
|
||||||
|
if len(matches) > 1:
|
||||||
|
raise LDAPException("duplicate dn in ldap: " + dn)
|
||||||
|
|
||||||
|
# return the attributes of the single successful match
|
||||||
|
else:
|
||||||
|
match = matches[0]
|
||||||
|
match_dn, match_attributes = match
|
||||||
|
return match_attributes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### User-related Methods ###
|
||||||
|
|
||||||
|
def user_lookup(self, uid):
|
||||||
|
"""
|
||||||
|
Retrieve the attributes of a user.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
uid - the UNIX user accound name of the user
|
||||||
|
|
||||||
|
Returns: attributes of user with uid
|
||||||
|
|
||||||
|
Example: connection.user_lookup('mspang') ->
|
||||||
|
{ 'uid': 'mspang', 'uidNumber': 21292 ...}
|
||||||
|
"""
|
||||||
|
|
||||||
|
dn = 'uid=' + uid + ',' + self.user_base
|
||||||
|
return self.lookup(dn)
|
||||||
|
|
||||||
|
|
||||||
|
def user_search(self, filter):
|
||||||
|
"""
|
||||||
|
Helper for user searches.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
filter - LDAP filter string to match users against
|
||||||
|
|
||||||
|
Returns: the list of uids matched
|
||||||
|
"""
|
||||||
|
|
||||||
|
# search for entries that match the filter
|
||||||
|
try:
|
||||||
|
matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, filter)
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("user search failed: %s" % e)
|
||||||
|
|
||||||
|
# list for uids found
|
||||||
|
uids = []
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
dn, attributes = match
|
||||||
|
|
||||||
|
# uid is a required attribute of posixAccount
|
||||||
|
if not attributes.has_key('uid'):
|
||||||
|
raise LDAPException(dn + ' (posixAccount) has no uid')
|
||||||
|
|
||||||
|
# do not handle the case of multiple usernames in one entry (yet)
|
||||||
|
elif len(attributes['uid']) > 1:
|
||||||
|
raise LDAPException(dn + ' (posixAccount) has multiple uids')
|
||||||
|
|
||||||
|
# append the sole uid of this match to the list
|
||||||
|
uids.append( attributes['uid'][0] )
|
||||||
|
|
||||||
|
return uids
|
||||||
|
|
||||||
|
|
||||||
|
def user_search_id(self, uidNumber):
|
||||||
|
"""
|
||||||
|
Retrieves a list of users with a certain UNIX uid number.
|
||||||
|
|
||||||
|
LDAP (or passwd for that matter) does not enforce any
|
||||||
|
restriction on the number of accounts that can have
|
||||||
|
a certain UID. Therefore this method returns a list of matches.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
uidNumber - the user id of the accounts desired
|
||||||
|
|
||||||
|
Returns: the list of uids matched
|
||||||
|
|
||||||
|
Example: connection.user_search_id(21292) -> ['mspang']
|
||||||
|
"""
|
||||||
|
|
||||||
|
# search for posixAccount entries with the specified uidNumber
|
||||||
|
filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
|
||||||
|
return self.user_search(filter)
|
||||||
|
|
||||||
|
|
||||||
|
def user_search_gid(self, gidNumber):
|
||||||
|
"""
|
||||||
|
Retrieves a list of users with a certain UNIX gid number.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
gidNumber - the group id of the accounts desired
|
||||||
|
|
||||||
|
Returns: the list of uids matched
|
||||||
|
"""
|
||||||
|
|
||||||
|
# search for posixAccount entries with the specified gidNumber
|
||||||
|
filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
|
||||||
|
return self.user_search(filter)
|
||||||
|
|
||||||
|
|
||||||
|
def user_add(self, uid, cn, loginShell, uidNumber, gidNumber, homeDirectory, gecos):
|
||||||
|
"""
|
||||||
|
Adds a user to the directory.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
uid - the UNIX username for the account
|
||||||
|
cn - the full name of the member
|
||||||
|
userPassword - password of the account (our setup does not use this)
|
||||||
|
loginShell - login shell for the user
|
||||||
|
uidNumber - the UNIX user id number
|
||||||
|
gidNumber - the UNIX group id number
|
||||||
|
homeDirectory - home directory for the user
|
||||||
|
gecos - comment field (usually stores miscellania)
|
||||||
|
|
||||||
|
Example: connection.user_add('mspang', 'Michael Spang',
|
||||||
|
'/bin/bash', 21292, 100, '/users/mspang',
|
||||||
|
'Michael Spang,,,')
|
||||||
|
"""
|
||||||
|
|
||||||
|
dn = 'uid=' + uid + ',' + self.user_base
|
||||||
|
attrs = {
|
||||||
|
'objectClass': [ 'top', 'account', 'posixAccount', 'shadowAccount' ],
|
||||||
|
'uid': [ uid ],
|
||||||
|
'cn': [ cn ],
|
||||||
|
'loginShell': [ loginShell ],
|
||||||
|
'uidNumber': [ str(uidNumber) ],
|
||||||
|
'gidNumber': [ str(gidNumber) ],
|
||||||
|
'homeDirectory': [ homeDirectory ],
|
||||||
|
'gecos': [ gecos ],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
modlist = ldap.modlist.addModlist(attrs)
|
||||||
|
self.ldap.add_s(dn, modlist)
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to add: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def user_modify(self, uid, attrs):
|
||||||
|
"""
|
||||||
|
Update user attributes in the directory.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
uid - username of the user to modify
|
||||||
|
entry - dictionary as returned by user_lookup() with changes to make.
|
||||||
|
omitted attributes are DELETED.
|
||||||
|
|
||||||
|
Example: user = user_lookup('mspang')
|
||||||
|
user['uidNumber'] = [ '0' ]
|
||||||
|
connection.user_modify('mspang', user)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# distinguished name of the entry to modify
|
||||||
|
dn = 'uid=' + uid + ',' + self.user_base
|
||||||
|
|
||||||
|
# retrieve current state of user
|
||||||
|
old_user = self.user_lookup(uid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# build list of modifications to make
|
||||||
|
changes = ldap.modlist.modifyModlist(old_user, attrs)
|
||||||
|
|
||||||
|
# apply changes
|
||||||
|
self.ldap.modify_s(dn, changes)
|
||||||
|
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to modify: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def user_delete(self, uid):
|
||||||
|
"""
|
||||||
|
Removes a user from the directory.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
uid - the UNIX username of the account
|
||||||
|
|
||||||
|
Example: connection.user_delete('mspang')
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
dn = 'uid=' + uid + ',' + self.user_base
|
||||||
|
self.ldap.delete_s(dn)
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to delete: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Group-related Methods ###
|
||||||
|
|
||||||
|
def group_lookup(self, cn):
|
||||||
|
"""
|
||||||
|
Retrieves the attributes of a group.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
cn - the UNIX group name to lookup
|
||||||
|
|
||||||
|
Returns: attributes of group with cn
|
||||||
|
|
||||||
|
Example: connection.group_lookup('office') -> {
|
||||||
|
'cn': 'office',
|
||||||
|
'gidNumber', '1001',
|
||||||
|
...
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
dn = 'cn=' + cn + ',' + self.group_base
|
||||||
|
return self.lookup(dn)
|
||||||
|
|
||||||
|
|
||||||
|
def group_search_id(self, gidNumber):
|
||||||
|
"""
|
||||||
|
Retrieves a list of groups with the specified UNIX group number.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
gidNumber - the group id of the groups desired
|
||||||
|
|
||||||
|
Returns: a list of groups with gid gidNumber
|
||||||
|
|
||||||
|
Example: connection.group_search_id(1001) -> ['office']
|
||||||
|
"""
|
||||||
|
|
||||||
|
# search for posixAccount entries with the specified uidNumber
|
||||||
|
try:
|
||||||
|
filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
|
||||||
|
matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, filter)
|
||||||
|
except ldap.LDAPError,e :
|
||||||
|
raise LDAPException("group search failed: %s" % e)
|
||||||
|
|
||||||
|
# list for groups found
|
||||||
|
group_cns = []
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
dn, attributes = match
|
||||||
|
|
||||||
|
# cn is a required attribute of posixGroup
|
||||||
|
if not attributes.has_key('cn'):
|
||||||
|
raise LDAPException(dn + ' (posixGroup) has no cn')
|
||||||
|
|
||||||
|
# do not handle the case of multiple cns for one group (yet)
|
||||||
|
elif len(attributes['cn']) > 1:
|
||||||
|
raise LDAPException(dn + ' (posixGroup) has multiple cns')
|
||||||
|
|
||||||
|
# append the sole uid of this match to the list
|
||||||
|
group_cns.append( attributes['cn'][0] )
|
||||||
|
|
||||||
|
return group_cns
|
||||||
|
|
||||||
|
|
||||||
|
def group_add(self, cn, gidNumber):
|
||||||
|
"""
|
||||||
|
Adds a group to the directory.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
cn - the name of the group
|
||||||
|
gidNumber - the number of the group
|
||||||
|
|
||||||
|
Example: connection.group_add('office', 1001)
|
||||||
|
"""
|
||||||
|
|
||||||
|
dn = 'cn=' + cn + ',' + self.group_base
|
||||||
|
attrs = {
|
||||||
|
'objectClass': [ 'top', 'posixGroup' ],
|
||||||
|
'cn': [ cn ],
|
||||||
|
'gidNumber': [ str(gidNumber) ],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
modlist = ldap.modlist.addModlist(attrs)
|
||||||
|
self.ldap.add_s(dn, modlist)
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to add group: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def group_modify(self, cn, attrs):
|
||||||
|
"""
|
||||||
|
Update group attributes in the directory.
|
||||||
|
|
||||||
|
The only available updates are fairly destructive
|
||||||
|
(rename or renumber) but this method is provided
|
||||||
|
for completeness.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
cn - name of the group to modify
|
||||||
|
entry - dictionary as returned by group_lookup() with changes to make.
|
||||||
|
omitted attributes are DELETED.
|
||||||
|
|
||||||
|
Example: group = group_lookup('office')
|
||||||
|
group['gidNumber'] = [ str(connection.first_id(20000, 40000)) ]
|
||||||
|
del group['memberUid']
|
||||||
|
connection.group_modify('office', group)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# distinguished name of the entry to modify
|
||||||
|
dn = 'cn=' + cn + ',' + self.group_base
|
||||||
|
|
||||||
|
# retrieve current state of group
|
||||||
|
old_group = self.group_lookup(cn)
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# build list of modifications to make
|
||||||
|
changes = ldap.modlist.modifyModlist(old_group, attrs)
|
||||||
|
|
||||||
|
# apply changes
|
||||||
|
self.ldap.modify_s(dn, changes)
|
||||||
|
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to modify: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def group_delete(self, cn):
|
||||||
|
"""
|
||||||
|
Removes a group from the directory."
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
cn - the name of the group
|
||||||
|
|
||||||
|
Example: connection.group_delete('office')
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
dn = 'cn=' + cn + ',' + self.group_base
|
||||||
|
self.ldap.delete_s(dn)
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("unable to delete group: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def group_members(self, cn):
|
||||||
|
"""
|
||||||
|
Retrieves a group's members.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
cn - the name of the group
|
||||||
|
|
||||||
|
Example: connection.group_members('office') ->
|
||||||
|
['sfflaw', 'jeperry', 'cschopf' ...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
group = self.group_lookup(cn)
|
||||||
|
return group.get('memberUid', None)
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Methods ###
|
||||||
|
|
||||||
|
def first_id(self, minimum, maximum):
|
||||||
|
"""
|
||||||
|
Determines the first available id within a range.
|
||||||
|
|
||||||
|
To be "available", there must be neither a user
|
||||||
|
with the id nor a group with the id.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
minimum - smallest uid that may be returned
|
||||||
|
maximum - largest uid that may be returned
|
||||||
|
|
||||||
|
Returns: the id, or None if there are none available
|
||||||
|
|
||||||
|
Example: connection.first_id(20000, 40000) -> 20018
|
||||||
|
"""
|
||||||
|
|
||||||
|
# compile a list of used uids
|
||||||
|
try:
|
||||||
|
users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['uidNumber'])
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("search for uids failed: %s" % e)
|
||||||
|
uids = []
|
||||||
|
for user in users:
|
||||||
|
dn, attrs = user
|
||||||
|
uid = int(attrs['uidNumber'][0])
|
||||||
|
if minimum <= uid <= maximum:
|
||||||
|
uids.append(uid)
|
||||||
|
|
||||||
|
# compile a list of used gids
|
||||||
|
try:
|
||||||
|
groups = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)', ['gidNumber'])
|
||||||
|
except ldap.LDAPError, e:
|
||||||
|
raise LDAPException("search for gids failed: %s" % e)
|
||||||
|
gids = []
|
||||||
|
for group in groups:
|
||||||
|
dn, attrs = group
|
||||||
|
gid = int(attrs['gidNumber'][0])
|
||||||
|
if minimum <= gid <= maximum:
|
||||||
|
gids.append(gid)
|
||||||
|
|
||||||
|
# iterate through ids and return the first available
|
||||||
|
for id in xrange(minimum, maximum+1):
|
||||||
|
if not id in uids and not id in gids:
|
||||||
|
return id
|
||||||
|
|
||||||
|
# no suitable id was found
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
### Tests ###
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
password_file = 'ldap.ceo'
|
||||||
|
server = 'ldaps:///'
|
||||||
|
base_dn = 'dc=csclub,dc=uwaterloo,dc=ca'
|
||||||
|
bind_dn = 'cn=ceo,' + base_dn
|
||||||
|
user_dn = 'ou=People,' + base_dn
|
||||||
|
group_dn = 'ou=Group,' + base_dn
|
||||||
|
bind_pw = open(password_file).readline().strip()
|
||||||
|
|
||||||
|
connection = LDAPConnection()
|
||||||
|
print "running disconnect()"
|
||||||
|
connection.disconnect()
|
||||||
|
print "running connect('%s', '%s', '%s', '%s', '%s')" % (server, bind_dn, '***', user_dn, group_dn)
|
||||||
|
connection.connect(server, bind_dn, bind_pw, user_dn, group_dn)
|
||||||
|
print "running user_lookup('mspang')", "->", "(%s)" % connection.user_lookup('mspang')['uidNumber'][0]
|
||||||
|
print "running user_search_id(21292)", "->", connection.user_search_id(21292)
|
||||||
|
print "running first_id(20000, 40000)", "->",
|
||||||
|
first_id = connection.first_id(20000, 40000)
|
||||||
|
print first_id
|
||||||
|
print "running group_add('testgroup', %d)" % first_id
|
||||||
|
try:
|
||||||
|
connection.group_add('testgroup', first_id)
|
||||||
|
except Exception, e:
|
||||||
|
print "FAILED: %s (continuing)" % e
|
||||||
|
print "running user_add('testuser', 'Test User', '/bin/false', %d, %d, '/home/null', 'Test User,,,')" % (first_id, first_id)
|
||||||
|
try:
|
||||||
|
connection.user_add('testuser', 'Test User', '/bin/false', first_id, first_id, '/home/null', 'Test User,,,')
|
||||||
|
except Exception, e:
|
||||||
|
print "FAILED: %s (continuing)" % e
|
||||||
|
print "running user_lookup('testuser')", "->",
|
||||||
|
user = connection.user_lookup('testuser')
|
||||||
|
print repr(connection.user_lookup('testuser')['cn'][0])
|
||||||
|
user['homeDirectory'] = ['/home/changed']
|
||||||
|
user['loginShell'] = ['/bin/true']
|
||||||
|
print "running user_modify(...)"
|
||||||
|
connection.user_modify('testuser', user)
|
||||||
|
print "running user_lookup('testuser')", "->",
|
||||||
|
user = connection.user_lookup('testuser')
|
||||||
|
print '(%s, %s)' % (user['homeDirectory'], user['loginShell'])
|
||||||
|
print "running group_lookup('testgroup')", "->",
|
||||||
|
group = connection.group_lookup('testgroup')
|
||||||
|
print group
|
||||||
|
print "running group_modify(...)"
|
||||||
|
group['gidNumber'] = [str(connection.first_id(20000, 40000))]
|
||||||
|
group['memberUid'] = [ str(first_id) ]
|
||||||
|
connection.group_modify('testgroup', group)
|
||||||
|
print "running group_lookup('testgroup')", "->",
|
||||||
|
group = connection.group_lookup('testgroup')
|
||||||
|
print group
|
||||||
|
print "running user_delete('testuser')"
|
||||||
|
connection.user_delete('testuser')
|
||||||
|
print "running group_delete('testgroup')"
|
||||||
|
connection.group_delete('testgroup')
|
||||||
|
print "running user_search_gid(100)", "->", "[" + ", ".join(map(repr,connection.user_search_gid(100)[:10])) + " ...]"
|
||||||
|
print "running group_members('office')", "->", "[" + ", ".join(map(repr,connection.group_members('office')[:10])) + " ...]"
|
||||||
|
print "running disconnect()"
|
||||||
|
connection.disconnect()
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""Library Routines"""
|
||||||
|
|
||||||
|
def read_config(config_file):
|
||||||
|
|
||||||
|
try:
|
||||||
|
conffile = open(config_file)
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
line = conffile.readline()
|
||||||
|
if line == '':
|
||||||
|
break
|
||||||
|
|
||||||
|
if '#' in line:
|
||||||
|
line = line[:line.find('#')]
|
||||||
|
|
||||||
|
while len(line) > 1 and line[-2] == '\\':
|
||||||
|
line = line[:-2] + line[-1]
|
||||||
|
next = conffile.readline()
|
||||||
|
line += next
|
||||||
|
if next == '':
|
||||||
|
break
|
||||||
|
|
||||||
|
pair = map(str.strip, line.split('=', 1))
|
||||||
|
|
||||||
|
if len(pair) == 2:
|
||||||
|
key, val = pair
|
||||||
|
|
||||||
|
if val[0] == val[-1] == '"':
|
||||||
|
val = val[1:-1]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
val = int(val)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
options[key] = val
|
||||||
|
elif len(pair[0]) > 1:
|
||||||
|
key, = pair
|
||||||
|
options[key] = None
|
||||||
|
|
||||||
|
return options
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# $Id: initialize.sh 13 2006-12-15 03:57:00Z mspang $
|
||||||
|
# Initializes a database for CEO.
|
||||||
|
|
||||||
|
# initialize the database
|
||||||
|
createdb $1
|
||||||
|
createlang plpythonu $1
|
||||||
|
|
||||||
|
# initialize the tables
|
||||||
|
psql $1 < structure.sql
|
||||||
|
|
||||||
|
# initialize check triggers
|
||||||
|
psql $1 < verify_studentid.sql
|
||||||
|
psql $1 < verify_term.sql
|
|
@ -0,0 +1,25 @@
|
||||||
|
-- $Id: structure.sql 36 2006-12-28 10:00:11Z mspang $
|
||||||
|
-- Table structure for CEO's SQL database.
|
||||||
|
|
||||||
|
-- Usage:
|
||||||
|
-- su postgres
|
||||||
|
-- createdb ceo
|
||||||
|
-- psql ceo < structure.sql
|
||||||
|
|
||||||
|
CREATE SEQUENCE memberid_seq;
|
||||||
|
|
||||||
|
CREATE TABLE members (
|
||||||
|
memberid integer PRIMARY KEY DEFAULT nextval('memberid_seq') NOT NULL,
|
||||||
|
name character varying(50) NOT NULL,
|
||||||
|
studentid character varying(10) UNIQUE,
|
||||||
|
program character varying(50),
|
||||||
|
"type" character varying(10),
|
||||||
|
userid character varying(32) UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE terms (
|
||||||
|
memberid integer NOT NULL,
|
||||||
|
term character(5) NOT NULL,
|
||||||
|
UNIQUE(memberid, term)
|
||||||
|
);
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- $Id: verify_studentid.sql 7 2006-12-11 06:27:22Z mspang $
|
||||||
|
-- PL/Python trigger to verify student ids for validity
|
||||||
|
-- Dedicated to office staff who can't type student ids.
|
||||||
|
|
||||||
|
-- To (re)install:
|
||||||
|
-- su postgres
|
||||||
|
-- psql ceo < verify_studentid.sql
|
||||||
|
|
||||||
|
-- To uninstall:
|
||||||
|
-- su postgres
|
||||||
|
-- echo 'DROP FUNCTION verify_studentid() CASCADE' | psql ceo
|
||||||
|
|
||||||
|
DROP FUNCTION verify_studentid() CASCADE;
|
||||||
|
|
||||||
|
CREATE FUNCTION verify_studentid() RETURNS trigger AS '
|
||||||
|
import re
|
||||||
|
|
||||||
|
# update this line if the student id format changes
|
||||||
|
STUDENTID_REGEX = "^[0-9]{8}$"
|
||||||
|
|
||||||
|
studentid = TD["new"]["studentid"]
|
||||||
|
if studentid and not re.match(STUDENTID_REGEX, studentid):
|
||||||
|
plpy.error("student id is invalid (%s)" % studentid)
|
||||||
|
|
||||||
|
' LANGUAGE plpythonu;
|
||||||
|
|
||||||
|
CREATE TRIGGER verify_studentid_insert BEFORE INSERT on members
|
||||||
|
FOR ROW EXECUTE PROCEDURE verify_studentid();
|
||||||
|
|
||||||
|
CREATE TRIGGER verify_studentid_update BEFORE UPDATE ON members
|
||||||
|
FOR ROW EXECUTE PROCEDURE verify_studentid();
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- $Id$
|
||||||
|
-- PL/Python trigger to verify terms for validity
|
||||||
|
|
||||||
|
-- To (re)install:
|
||||||
|
-- su postgres
|
||||||
|
-- psql ceo < verify_term.sql
|
||||||
|
|
||||||
|
-- To uninstall:
|
||||||
|
-- su postgres
|
||||||
|
-- echo 'DROP FUNCTION verify_term() CASCADE' | psql ceo
|
||||||
|
|
||||||
|
DROP FUNCTION verify_term() CASCADE;
|
||||||
|
|
||||||
|
CREATE FUNCTION verify_term() RETURNS trigger AS '
|
||||||
|
import re
|
||||||
|
|
||||||
|
# update this line if the term format changes
|
||||||
|
TERM_REGEX = "^[wsf][0-9]{4}$"
|
||||||
|
|
||||||
|
term = TD["new"]["term"]
|
||||||
|
if term and not re.match(TERM_REGEX, term):
|
||||||
|
plpy.error("term is invalid (%s)" % term)
|
||||||
|
|
||||||
|
' LANGUAGE plpythonu;
|
||||||
|
|
||||||
|
CREATE TRIGGER verify_term_insert BEFORE INSERT on terms
|
||||||
|
FOR ROW EXECUTE PROCEDURE verify_term();
|
||||||
|
|
||||||
|
CREATE TRIGGER verify_term_update BEFORE UPDATE ON terms
|
||||||
|
FOR ROW EXECUTE PROCEDURE verify_term();
|
Loading…
Reference in New Issue