New release (version 0.2).

Updates in this version:

  * Tests added to most Python modules.
  * Split configuration files.
  * Added maintainer scripts to manage permissions during install and purge.
  * Added functions for use by tools planned for next release (chfn, etc).

ceo:

  * Added support for account "repair", which will recreate LDAP entries
    and Kerberos principals if necessary.
  * The recreate account menu option is now active.

Miscellaneous:

  * Replaced instances of "== None" and "!= None" with "is None" and
    "is not None", respectively (thanks to: Nick Guenther).
  * Renamed terms.valid() to terms.validate() (thanks to: Nick Guenther).
This commit is contained in:
Michael Spang 2007-01-27 19:23:18 -05:00
parent cb59e85c2e
commit 58bf72726a
36 changed files with 2365 additions and 709 deletions

14
bin/ceo
View File

@ -1,22 +1,20 @@
#!/usr/bin/python2.4 --
"""CEO SUID Python Wrapper Script"""
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',
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:
if key not 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:
if not dir.find('/usr') == 0:
while dir in sys.path:
sys.path.remove(dir)

15
debian/changelog vendored
View File

@ -1,3 +1,18 @@
csc (0.2) unstable; urgency=low
* Tests added to most Python modules.
* Split configuration files.
* Added maintainer scripts to manage permissions during install and purge.
* Added functions for use by tools planned for next release (chfn, etc).
* Added support for account "repair", which will recreate LDAP entries
and principals if necessary.
* The recreate account menu option in CEO is now active.
* Replaced instances of "== None" and "!= None" with "is None" and
"is not None", respectively (thanks to: Nick Guenther).
* Renamed terms.valid() to terms.validate() (thanks to: Nick Guenther).
-- Michael Spang <mspang@uwaterloo.ca> Fri, 26 Jan 2007 20:10:14 -0500
csc (0.1) unstable; urgency=low
* Initial Release.

4
debian/control vendored
View File

@ -3,11 +3,11 @@ Section: admin
Priority: optional
Maintainer: Michael Spang <mspang@uwaterloo.ca>
Build-Depends: debhelper (>= 4.0.0)
Standards-Version: 3.6.1
Standards-Version: 3.7.2
Package: csc
Architecture: any
Depends: python, python2.4, python2.4-ldap, python2.4-pygresql, krb5-user, less
Depends: python, python2.4, python2.4-ldap, python2.4-pygresql, krb5-user, less, ${shlibs:Depends}
Description: Computer Science Club Administrative Utilities
This package contains the CSC Electronic Office
and other Computer Science Club administrative

4
debian/copyright vendored
View File

@ -1,7 +1,7 @@
This package was debianized by mspang <mspang@uwaterloo.ca> on
This package was debianized by Michael Spang <mspang@uwaterloo.ca> on
Thu, 28 Dec 2006 04:07:03 -0500.
Copyright (c) 2006, 2007 Michael Spang
Copyright (c) 2006-2007, Michael Spang
All rights reserved.
Redistribution and use in source and binary forms, with or without

58
debian/postinst vendored Normal file
View File

@ -0,0 +1,58 @@
#!/bin/bash -e
case "$1" in
configure|upgrade)
if getent passwd ceo > /dev/null; then
CEO=ceo
SUID=4750
else
CEO=root
SUID=755
fi
if getent group office > /dev/null; then
OFFICE=office
else
OFFICE=root
fi
if ! dpkg-statoverride --list /usr/bin/ceo > /dev/null; then
dpkg-statoverride --add --update $CEO $OFFICE $SUID /usr/bin/ceo
fi
if [ -f /etc/csc/ldap.cf ] && ! dpkg-statoverride --list /etc/csc/ldap.cf > /dev/null; then
dpkg-statoverride --add --update $CEO staff 640 /etc/csc/ldap.cf
fi
if [ ! -e /etc/csc/ceo.keytab ] && [ -x /usr/sbin/kadmin.local ]; then
if dpkg-statoverride --list /etc/csc/ceo.keytab > /dev/null; then
dpkg-statoverride --remove /etc/csc/ceo.keytab || true
fi
echo 'warning: re-creating ceo.keytab'
echo 'ktadd -k /etc/csc/ceo.keytab ceo/admin' | /usr/sbin/kadmin.local || true
if [ -e /etc/csc/ceo.keytab ]; then
echo -e "\nSuccess!"
else
echo -e "\nFailed!"
fi
fi
if [ -f /etc/csc/ceo.keytab ] && ! dpkg-statoverride --list /etc/csc/ceo.keytab > /dev/null; then
dpkg-statoverride --add --update $CEO staff 640 /etc/csc/ceo.keytab
fi
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \"$1\"" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

33
debian/postrm vendored Normal file
View File

@ -0,0 +1,33 @@
#!/bin/bash -e
case "$1" in
purge)
if dpkg-statoverride --list /usr/bin/ceo > /dev/null; then
dpkg-statoverride --remove /usr/bin/ceo || true
fi
if dpkg-statoverride --list /etc/csc/ldap.cf > /dev/null; then
dpkg-statoverride --remove /etc/csc/ldap.cf || true
fi
if dpkg-statoverride --list /etc/csc/ceo.keytab > /dev/null; then
dpkg-statoverride --remove /etc/csc/ceo.keytab || true
fi
rmdir --ignore-fail-on-non-empty /etc/csc
;;
remove|failed-upgrade|upgrade)
;;
*)
echo "postrm called with invalid argument \"$1\"" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

30
debian/rules vendored
View File

@ -2,13 +2,11 @@
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
$(CC) -DFULL_PATH='"/usr/lib/csc/ceo"' -o build/ceo misc/setuid-prog.c
touch build-stamp
clean:
@ -17,27 +15,22 @@ clean:
dh_clean
rm -f build-stamp
rm -rf build/
find pylib/ -name '*.pyc' -print0 | xargs -0 rm -f
find pylib/ -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/*
dh_installdirs etc/csc usr/lib/$(PYTHON)/site-packages usr/share/csc \
usr/lib/csc usr/bin
dh_install -X.svn -X.pyc pylib/csc usr/lib/$(PYTHON)/site-packages/
dh_install -X.svn -X.pyc etc/* etc/csc/
dh_install -X.svn -X.pyc sql/* usr/share/csc/
dh_install pylib/* usr/lib/$(PYTHON)/site-packages/
dh_install etc/* etc/csc/
dh_install 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/
dh_install bin/ceo usr/lib/csc/
dh_install build/ceo usr/bin/
binary-indep: build install
binary-arch: build install
dh_testdir
dh_testroot
dh_installchangelogs
@ -60,7 +53,8 @@ binary-indep: build install
dh_md5sums
dh_builddeb
binary: binary-indep binary-arch
.PHONY: build clean binary-indep binary-arch binary install configure
binary-indep:
binary-arch: build install
binary: binary-indep binary-arch
.PHONY: build clean binary-indep binary-arch binary install

View File

@ -3,6 +3,6 @@ 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
- curses does not draw borders/lines correctly in a screen session. screen apparently ignores
some font-changing characters. workaround should be possible (other progs work).
- the menu is not redrawn between windows and therefore a gap tends to grow there

8
docs/TODO Normal file
View File

@ -0,0 +1,8 @@
TODO:
* Python bindings for libkadm5
* Python bindings for quota?
* New UI: urwid-based?
* Logging via syslog
* Try to recover and roll-back on error during account creation
* Write manpages

View File

@ -1,35 +1,44 @@
# $Id: accounts.cf 45 2007-01-02 01:39:10Z mspang $
# CSC Accounts Configuration
# /etc/csc/accounts.cf: CSC Accounts Configuration
### Account Options ###
include /etc/csc/ldap.cf
include /etc/csc/kerberos.cf
minimum_id = 20000
maximum_id = 40000
### Member Account Options ###
shell = "/bin/bash"
home = "/users"
gid = 100
member_min_id = 20000
member_max_id = 39999
member_shell = "/bin/bash"
member_home = "/users"
member_desc = "CSC Member Account"
member_group = "users"
### Club Account Options ###
### LDAP Configuration ###
club_min_id = 15000
club_max_id = 19999
club_shell = "/bin/bash"
club_home = "/users"
club_desc = "CSC Club Account"
club_group = "users"
server_url = "ldap:///"
### Administrative Account Options
users_base = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
admin_min_id = 10000
admin_max_id = 14999
admin_shell = "/bin/bash"
admin_home = "/users"
admin_desc = "CSC Administrative Account"
admin_group = "users"
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"
### Account Group Options ###
group_min_id = 10000
group_max_id = 14999
group_desc = "CSC Group"
### Validation Tuning ###
username_regex = "^[a-z][-a-z0-9]*$"
realname_regex = "^[^,:=]*$"
groupname_regex = "^[a-z][-a-z0-9]*$"
min_password_length = 4
shells_file = "/etc/shells"

5
etc/kerberos.cf Normal file
View File

@ -0,0 +1,5 @@
# /etc/csc/kerberos.cf: CSC Kerberos Administration Configuration
realm = "CSCLUB.UWATERLOO.CA"
admin_principal = "ceo/admin@CSCLUB.UWATERLOO.CA"
admin_keytab = "/etc/csc/ceo.keytab"

9
etc/ldap.cf Normal file
View File

@ -0,0 +1,9 @@
# /etc/csc/ldap.cf: CSC LDAP Configuration
server_url = "ldaps:///"
users_base = "ou=People,dc=csclub,dc=uwaterloo,dc=ca"
groups_base = "ou=Group,dc=csclub,dc=uwaterloo,dc=ca"
admin_bind_dn = "cn=ceo,dc=csclub,dc=uwaterloo,dc=ca"
admin_bind_pw = "secret"

View File

@ -1,13 +1,6 @@
# $Id: members.cf 45 2007-01-02 01:39:10Z mspang $
# CSC Members Configuration
# /etc/csc/members.cf: CSC Members Configuration
### Database Configuration ###
server = "localhost"
database = "ceo"
user = "ceo"
password = "secret"
include /etc/csc/pgsql.cf
### Validation Tuning ###

11
etc/pgsql.cf Normal file
View File

@ -0,0 +1,11 @@
# /etc/csc/pgsql.cf: PostgreSQL database configuration
### Database Configuration ###
# server = "localhost"
server = ""
database = "ceo"
# not used
user = "ceo"
password = "secret"

View File

@ -1,19 +1,5 @@
# $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
Computer Science Club Python Modules
The csc module is a container for all CSC-specific Python modules.
"""

View File

@ -0,0 +1,9 @@
"""
CSC Administrative Modules
This module provides member and account management modules.
members - member registration management functions
accounts - account administration functions
terms - helper routines for manipulating terms
"""

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
# $Id: members.py 44 2006-12-31 07:09:27Z mspang $
"""
CSC Member Management
@ -10,44 +9,29 @@ 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.adm import terms
from csc.backends import db
from csc.common.conf import read_config
from csc.common import conf
### Configuration
### Configuration ###
CONFIG_FILE = '/etc/csc/members.cf'
cfg = {}
def load_configuration():
"""Load Members Configuration"""
# configuration already loaded?
if len(cfg) > 0:
return
string_fields = [ 'studentid_regex', 'realname_regex', 'server',
'database', 'user', 'password' ]
# read in the file
cfg_tmp = read_config(CONFIG_FILE)
# read configuration file
cfg_tmp = conf.read(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)
# verify configuration
conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
# update the current configuration with the loaded values
cfg.update(cfg_tmp)
@ -56,24 +40,46 @@ def load_configuration():
### Exceptions ###
DBException = db.DBException
ConfigurationException = conf.ConfigurationException
class MemberException(Exception):
"""Exception class for member-related errors."""
"""Base exception class for member-related errors."""
class DuplicateStudentID(MemberException):
"""Exception class for student ID conflicts."""
pass
def __init__(self, studentid):
self.studentid = studentid
def __str__(self):
return "Student ID already exists in the database: %s" % self.studentid
class InvalidStudentID(MemberException):
"""Exception class for malformed student IDs."""
pass
def __init__(self, studentid):
self.studentid = studentid
def __str__(self):
return "Student ID is invalid: %s" % self.studentid
class InvalidTerm(MemberException):
"""Exception class for malformed terms."""
pass
def __init__(self, term):
self.term = term
def __str__(self):
return "Term is invalid: %s" % self.term
class InvalidRealName(MemberException):
"""Exception class for invalid real names."""
def __init__(self, name):
self.name = name
def __str__(self):
return "Name is invalid: %s" % self.name
class NoSuchMember(MemberException):
"""Exception class for nonexistent members."""
pass
def __init__(self, memberid):
self.memberid = memberid
def __str__(self):
return "Member not found: %d" % self.memberid
@ -82,12 +88,10 @@ class NoSuchMember(MemberException):
# global database connection
connection = db.DBConnection()
def connect():
"""Connect to PostgreSQL."""
load_configuration()
connection.connect(cfg['server'], cfg['database'])
@ -103,24 +107,27 @@ def connected():
return connection.connected()
### Member Table ###
def new(realname, studentid=None, program=None):
def new(realname, studentid=None, program=None, mtype='user', userid=None):
"""
Registers a new CSC member. The member is added
to the members table and registered for the current
term.
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
mtype - a string describing the type of member ('user', 'club')
userid - the initial user id
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
InvalidRealName - if the real name is malformed
Example: new("Michael Spang", program="CS") -> 3349
"""
@ -128,16 +135,21 @@ def new(realname, studentid=None, program=None):
# blank attributes should be NULL
if studentid == '': studentid = None
if program == '': program = None
if userid == '': userid = None
if mtype == '': mtype = 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)
if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
raise InvalidStudentID(studentid)
# check real name format (UNIX account real names must not contain [,:=])
if not re.match(cfg['realname_regex'], realname):
raise InvalidRealName(realname)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID("student id exists in database: %s" % studentid)
raise DuplicateStudentID(studentid)
# add the member
memberid = connection.insert_member(realname, studentid, program)
@ -155,9 +167,6 @@ 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) -> {
@ -188,7 +197,7 @@ def get_userid(userid):
}
"""
return connection.select_member_by_account(userid)
return connection.select_member_by_userid(userid)
def get_studentid(studentid):
@ -265,20 +274,23 @@ def delete(memberid):
"""
Erase all records of a member.
Note: real members are never removed
from the database
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
Returns: attributes and terms of the
member in a tuple
Exceptions:
NoSuchMember - if the member id does not exist
Example: delete(0) -> ({ 'memberid': 0, name: 'Calum T. Dalek' ...}, ['s1993'])
"""
# save member data
member = connection.select_member_by_id(memberid)
# bail if not found
if not member:
raise NoSuchMember(memberid)
term_list = connection.select_terms(memberid)
# remove data from the db
@ -291,13 +303,12 @@ def delete(memberid):
def update(member):
"""
Update CSC member attributes. None is NULL.
Update CSC member attributes.
Parameters:
member - a dictionary with member attributes as
returned by get, possibly omitting some
attributes. member['memberid'] must exist
and be valid.
member - a dictionary with member attributes as returned by get,
possibly omitting some attributes. member['memberid']
must exist and be valid. None is NULL.
Exceptions:
NoSuchMember - if the member id does not exist
@ -307,20 +318,18 @@ def update(member):
Example: update( {'memberid': 3349, userid: 'mspang'} )
"""
if member.has_key('studentid') and member['studentid'] != None:
if member.has_key('studentid') and member['studentid'] is not 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)
if studentid is not None and not re.match(cfg['studentid_regex'], str(studentid)):
raise InvalidStudentID(studentid)
# check for duplicate student id
member = connection.select_member_by_studentid(studentid)
if member:
raise DuplicateStudentID("student id exists in database: %s" %
studentid)
dupmember = connection.select_member_by_studentid(studentid)
if dupmember:
raise DuplicateStudentID(studentid)
# not specifying memberid is a bug
if not member.has_key('memberid'):
@ -328,10 +337,8 @@ def update(member):
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)
if not get(memberid):
raise NoSuchMember(memberid)
# do the update
connection.update_member(member)
@ -359,14 +366,14 @@ def register(memberid, term_list):
Example: register(3349, ["w2007", "s2007"])
"""
if not type(term_list) in (list, tuple):
if type(term_list) in (str, unicode):
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)
raise InvalidTerm(term)
# add term to database
connection.insert_term(memberid, term)
@ -388,10 +395,10 @@ def registered(memberid, term):
Example: registered(3349, "f2006") -> True
"""
return connection.select_term(memberid, term) != None
return connection.select_term(memberid, term) is not None
def terms_list(memberid):
def member_terms(memberid):
"""
Retrieves a list of terms a member is
registered for.
@ -404,7 +411,9 @@ def terms_list(memberid):
Example: registered(0) -> 's1993'
"""
return connection.select_terms(memberid)
terms_list = connection.select_terms(memberid)
terms_list.sort(terms.compare)
return terms_list
@ -412,15 +421,104 @@ def terms_list(memberid):
if __name__ == '__main__':
from csc.common.test import *
# t=test m=member s=student u=updated
tmname = 'Test Member'
tmprogram = 'Metaphysics'
tmsid = '00000000'
tm2name = 'Test Member 2'
tm2sid = '00000001'
tm2uname = 'Test Member II'
tm2usid = '00000002'
tm2uprogram = 'Pseudoscience'
tm2uuserid = 'testmember'
tmdict = {'name': tmname, 'userid': None, 'program': tmprogram, 'type': 'user', 'studentid': tmsid }
tm2dict = {'name': tm2name, 'userid': None, 'program': None, 'type': 'user', 'studentid': tm2sid }
tm2udict = {'name': tm2uname, 'userid': tm2uuserid, 'program': tm2uprogram, 'type': 'user', 'studentid': tm2usid }
thisterm = terms.current()
nextterm = terms.next(thisterm)
test(connect)
connect()
success()
test(connected)
assert_equal(True, connected())
success()
sid = new("Test User", "99999999", "CS")
dmid = get_studentid(tmsid)
if dmid: delete(dmid['memberid'])
dmid = get_studentid(tm2sid)
if dmid: delete(dmid['memberid'])
dmid = get_studentid(tm2usid)
if dmid: delete(dmid['memberid'])
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)
test(new)
tmid = new(tmname, tmsid, tmprogram)
tm2id = new(tm2name, tm2sid)
success()
tmdict['memberid'] = tmid
tm2dict['memberid'] = tm2id
tm2udict['memberid'] = tm2id
test(registered)
assert_equal(True, registered(tmid, thisterm))
assert_equal(True, registered(tm2id, thisterm))
assert_equal(False, registered(tmid, nextterm))
success()
test(get)
assert_equal(tmdict, get(tmid))
assert_equal(tm2dict, get(tm2id))
success()
test(list_name)
assert_equal(True, tmid in [ x['memberid'] for x in list_name(tmname) ])
assert_equal(True, tm2id in [ x['memberid'] for x in list_name(tm2name) ])
success()
test(register)
register(tmid, terms.next(terms.current()))
assert_equal(True, registered(tmid, nextterm))
success()
test(member_terms)
assert_equal([thisterm, nextterm], member_terms(tmid))
assert_equal([thisterm], member_terms(tm2id))
success()
test(list_term)
assert_equal(True, tmid in [ x['memberid'] for x in list_term(thisterm) ])
assert_equal(True, tmid in [ x['memberid'] for x in list_term(nextterm) ])
assert_equal(True, tm2id in [ x['memberid'] for x in list_term(thisterm) ])
assert_equal(False, tm2id in [ x['memberid'] for x in list_term(nextterm) ])
success()
test(update)
update(tm2udict)
assert_equal(tm2udict, get(tm2id))
success()
test(get_userid)
assert_equal(tm2udict, get_userid(tm2uuserid))
success()
test(get_studentid)
assert_equal(tm2udict, get_studentid(tm2usid))
assert_equal(tmdict, get_studentid(tmsid))
success()
test(delete)
delete(tmid)
delete(tm2id)
success()
test(disconnect)
disconnect()
assert_equal(False, connected())
disconnect()
success()

View File

@ -1,11 +1,9 @@
# $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.
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
@ -16,27 +14,27 @@ EPOCH = 1970
SEASONS = [ 'w', 's', 'f' ]
def valid(term):
def validate(term):
"""
Determines whether a term is well-formed:
Determines whether a term is well-formed.
Parameters:
term - the term string
Returns: whether the term is valid (boolean)
Example: valid("f2006") -> True
Example: validate("f2006") -> True
"""
regex = '^[wsf][0-9]{4}$'
return re.match(regex, term) != None
return re.match(regex, term) is not None
def parse(term):
"""Helper function to convert a term string to the number of terms
since the epoch. Such numbers are intended for internal use only."""
if not valid(term):
if not validate(term):
raise Exception("malformed term: %s" % term)
year = int( term[1:] )
@ -176,8 +174,8 @@ def from_timestamp(timestamp):
This function notes that:
WINTER = JANUARY to APRIL
SPRING = MAY TO AUGUST
FALL = SEPTEMBER TO DECEMBER
SPRING = MAY to AUGUST
FALL = SEPTEMBER to DECEMBER
Parameters:
timestamp - number of seconds since the epoch
@ -235,18 +233,22 @@ def next_unregistered(registered):
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()
from csc.common.test import *
print "All tests passed." "\n"
test(parse); assert_equal(110, parse('f2006')); success()
test(generate); assert_equal('f2006', generate(110)); success()
test(next); assert_equal('w2007', next('f2006')); success()
test(previous); assert_equal('s2006', previous('f2006')); success()
test(delta); assert_equal(1, delta('f2006', 'w2007')); success()
test(compare); assert_equal(-1, compare('f2006', 'w2007')); success()
test(add); assert_equal('w2010', add('f2006', delta('f2006', 'w2010'))); success()
test(interval); assert_equal(['f2006', 'w2007', 's2007'], interval('f2006', 3)); success()
test(from_timestamp); assert_equal('f2006', from_timestamp(1166135779)); success()
test(current); assert_equal(True, parse( current() ) >= 110 ); success()
test(next_unregistered)
assert_equal( next(current()), next_unregistered([ current() ]))
assert_equal( current(), next_unregistered([]))
assert_equal( current(), next_unregistered([ previous(current()) ]))
assert_equal( current(), next_unregistered([ add(current(), -2) ]))
success()

View File

@ -1,9 +1,8 @@
# $Id: __init__.py 23 2006-12-18 20:14:51Z mspang $
"""
User Interfaces
Application-style User Interfaces
This module contains frontends and related modules.
CEO's primary frontends are:
This module contains large frontends with many functions
and fancy graphical user interfaces.
legacy - aims to reproduce the curses UI of the previous CEO
"""

View File

@ -1,10 +1,8 @@
# $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
main - all of the main UI logic
helpers - user interface library routines
"""

View File

@ -1,4 +1,3 @@
# $Id: helpers.py 35 2006-12-28 05:14:05Z mspang $
"""
Helpers for legacy User Interface
@ -7,7 +6,7 @@ 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
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
@ -52,14 +51,14 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# turn on cursor
try:
curses.curs_set(1)
except:
except curses.error:
pass
# set keypad mode to allow UP, DOWN, etc
wnd.keypad(1)
# the input string
input = ""
inputbuf = ""
# offset of cursor in input
# i.e. the next operation is applied at input[inputoff]
@ -78,7 +77,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
if echo:
# discard characters before displayoff,
# as the window may be scrolled to the right
substring = input[displayoff:]
substring = inputbuf[displayoff:]
# pad the string with zeroes to overwrite stale characters
substring = substring + " " * (width - len(substring))
@ -96,7 +95,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# enter returns input
if key == KEY_RETURN:
return input
return inputbuf
# escape aborts input
elif key == KEY_ESCAPE:
@ -104,7 +103,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# EOT (C-d) aborts if there is no input
elif key == KEY_EOT:
if len(input) == 0:
if len(inputbuf) == 0:
return None
# backspace removes the previous character
@ -112,7 +111,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
if inputoff > 0:
# remove the character immediately before the input offset
input = input[0:inputoff-1] + input[inputoff:]
inputbuf = inputbuf[0:inputoff-1] + inputbuf[inputoff:]
inputoff -= 1
# move either the cursor or entire line of text left
@ -124,7 +123,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
if inputoff < len(input):
# remove the character at the input offset
input = input[0:inputoff] + input[inputoff+1:]
inputbuf = inputbuf[0:inputoff] + inputbuf[inputoff+1:]
# left moves the cursor one character left
elif key == curses.KEY_LEFT:
@ -139,7 +138,7 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# right moves the cursor one character right
elif key == curses.KEY_RIGHT:
if inputoff < len(input):
if inputoff < len(inputbuf):
# move the cursor to the right
inputoff += 1
@ -155,8 +154,8 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# end moves the cursor past the last character
elif key == curses.KEY_END:
inputoff = len(input)
displayoff = len(input) - width + 1
inputoff = len(inputbuf)
displayoff = len(inputbuf) - width + 1
# insert toggles insert/overwrite mode
elif key == curses.KEY_IC:
@ -164,15 +163,15 @@ def read_input(wnd, offy, offx, width, maxlen, echo=True):
# other (printable) characters are added to the input string
elif curses.ascii.isprint(key):
if len(input) < maxlen or maxlen == 0:
if len(inputbuf) < maxlen or maxlen == 0:
# insert mode: insert before current offset
if insert:
input = input[0:inputoff] + chr(key) + input[inputoff:]
inputbuf = inputbuf[0:inputoff] + chr(key) + inputbuf[inputoff:]
# overwrite mode: replace current offset
else:
input = input[0:inputoff] + chr(key) + input[inputoff+1:]
inputbuf = inputbuf[0:inputoff] + chr(key) + inputbuf[inputoff+1:]
# increment the input offset
inputoff += 1
@ -218,13 +217,13 @@ def inputbox(wnd, prompt, field_width, echo=True):
# 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)
inputbuf = read_input(text_wnd, inputy, inputx, inputwidth, 0, echo)
# erase the window
child_wnd.erase()
child_wnd.refresh()
return input
return inputbuf
def line_wrap(line, width):
@ -323,7 +322,7 @@ def msgbox(wnd, msg, title="Message"):
curses.curs_set(0)
outer_wnd.keypad(1)
while True:
key = outer_wnd.getch(0,0)
key = outer_wnd.getch(0, 0)
if key == KEY_RETURN or key == KEY_ESCAPE:
break
@ -379,18 +378,18 @@ def menu(wnd, offy, offx, width, options, _acquire_wnd=None):
wnd.refresh()
# read one keypress
input = wnd.getch()
keypress = wnd.getch()
# UP moves to the previous option
if input == curses.KEY_UP and selected > 0:
if keypress == 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:
elif keypress == curses.KEY_DOWN and selected < len(options) - 1:
selected = (selected + 1)
# RETURN runs the callback for the selected option
elif input == KEY_RETURN:
elif keypress == KEY_RETURN:
text, callback = options[selected]
# highlight the selected option

View File

@ -1,4 +1,3 @@
# $Id: main.py 44 2006-12-31 07:09:27Z mspang $
"""
CEO-like Frontend
@ -21,7 +20,7 @@ BORDER_COLOR = curses.COLOR_RED
def action_new_member(wnd):
"""Interactively add a new member."""
username, studentid, program = '', None, ''
studentid, program = None, ''
# read the name
prompt = " Name: "
@ -33,7 +32,7 @@ def action_new_member(wnd):
# read the student id
prompt = "Student id:"
while studentid == None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
while studentid is None or (re.search("[^0-9]", studentid) and not studentid.lower() == 'exit'):
studentid = inputbox(wnd, prompt, 18)
# abort if exit is entered
@ -48,7 +47,7 @@ def action_new_member(wnd):
program = inputbox(wnd, prompt, 18)
# abort if exit is entered
if program == None or program.lower() == 'exit':
if program is None or program.lower() == 'exit':
return False
# connect the members module to its backend if necessary
@ -59,14 +58,17 @@ def action_new_member(wnd):
memberid = members.new(realname, studentid, program)
msgbox(wnd, "Success! Your memberid is %s. You are now registered\n"
% memberid + "for the " + terms.current() + " term.");
% memberid + "for the " + terms.current() + " term.")
except members.InvalidStudentID:
msgbox(wnd, "Invalid student ID.")
msgbox(wnd, "Invalid student ID: %s" % studentid)
return False
except members.DuplicateStudentID:
msgbox(wnd, "A member with this student ID exists.")
return False
except members.InvalidRealName:
msgbox(wnd, 'Invalid real name: "%s"' % realname)
return False
def action_term_register(wnd):
@ -85,7 +87,7 @@ def action_term_register(wnd):
if not member: return False
memberid = member['memberid']
term_list = members.terms_list(memberid)
term_list = members.member_terms(memberid)
# display user
display_member_details(wnd, member, term_list)
@ -134,7 +136,7 @@ def action_term_register_multiple(wnd):
if not member: return False
memberid = member['memberid']
term_list = members.terms_list(memberid)
term_list = members.member_terms(memberid)
# display user
display_member_details(wnd, member, term_list)
@ -177,11 +179,57 @@ def action_term_register_multiple(wnd):
msgbox(wnd, "Your are now registered for terms: " + ", ".join(term_list))
except members.InvalidTerm:
msgbox(wnd, "Term is not valid: %s" % term)
msgbox(wnd, "Invalid term entered.")
return False
def repair_account(wnd, memberid, userid):
"""Attemps to repair an account."""
if not accounts.connected(): accounts.connect()
member = members.get(memberid)
exists, haspw = accounts.status(userid)
if not exists:
password = input_password(wnd)
accounts.create_member(userid, password, member['name'], memberid)
msgbox(wnd, "Account created (where the hell did it go, anyway?)\n"
"If you're homedir still exists, it will not be inaccessible to you,\n"
"please contact systems-committee@csclub.uwaterloo.ca to get this resolved.\n")
elif not haspw:
password = input_password(wnd)
accounts.add_password(userid, password)
msgbox(wnd, "Password added to account.")
else:
msgbox(wnd, "No problems to repair.")
def input_password(wnd):
# 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)
return password
def action_create_account(wnd):
"""Interactively create an account for a member."""
@ -198,7 +246,7 @@ def action_create_account(wnd):
if not member: return False
memberid = member['memberid']
term_list = members.terms_list(memberid)
term_list = members.member_terms(memberid)
# display the member
display_member_details(wnd, member, term_list)
@ -218,67 +266,59 @@ def action_create_account(wnd):
msgbox(wnd, "I suggest searching for the member by userid or name from the main menu.")
return False
# member already has an account?
if member['userid']:
userid = member['userid']
msgbox(wnd, "Member " + str(memberid) + " already has an account: " + member['userid'] + "\n"
"Attempting to repair it. Contact the sysadmin if there are still problems." )
repair_account(wnd, memberid, userid)
return False
# read user id
prompt = "Userid:"
while userid == '':
userid = inputbox(wnd, prompt, 18)
# user abort
if userid == None or userid.lower() == 'exit':
if userid is 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)
password = input_password(wnd)
# 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.")
try:
if not accounts.connected(): accounts.connect()
accounts.create_member(userid, password, member['name'], memberid)
except accounts.AccountExists, e:
msgbox(wnd, str(e))
return False
elif result == accounts.KRB_EXISTS:
msgbox(wnd, "This account already exists in Kerberos, but not in LDAP. Please contact the Systems Administrator.")
except accounts.NoAvailableIDs, e:
msgbox(wnd, str(e))
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.")
except accounts.InvalidArgument, e:
msgbox(wnd, str(e))
return False
elif result == accounts.BAD_REALNAME:
msgbox(wnd, "Invalid real name: %s. Contact the Systems Administrator." % member['name'])
except accounts.LDAPException, e:
msgbox(wnd, "Error creating LDAP entry - Contact the Systems Administrator: %s" % e)
return False
elif result == accounts.BAD_USERNAME:
msgbox(wnd, "Invalid username: %s. Enter a valid username." % userid)
except accounts.KrbException, e:
msgbox(wnd, "Error creating Kerberos principal - Contact the Systems Administrator: %s" % e)
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} )
members.update( {'memberid': memberid, 'userid': userid} )
# success
msgbox(wnd, "Please run 'addhomedir " + userid + "'.")
msgbox(wnd, "Success! Your account has been added")
return False
def display_member_details(wnd, member, term_list):
"""Display member attributes in a message box."""
@ -343,17 +383,21 @@ def action_display_member(wnd):
return False
member = get_member_memberid_userid(wnd, memberid)
if not member: return
term_list = members.terms_list( member['memberid'] )
if not member: return False
term_list = members.member_terms( member['memberid'] )
# display the details in a window
display_member_details(wnd, member, term_list)
return False
def page(text):
"""Send a text buffer to an external pager for display."""
try:
pipe = os.popen('/usr/bin/less', 'w')
pager = '/usr/bin/less'
pipe = os.popen(pager, 'w')
pipe.write(text)
pipe.close()
except IOError:
@ -385,7 +429,7 @@ def action_list_term(wnd):
# 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'):
while term is 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
@ -404,8 +448,11 @@ def action_list_term(wnd):
# display the mass of text with a pager
page( buf )
return False
def action_list_name(wnd):
"""Interactively search for members by name."""
name = None
@ -420,7 +467,7 @@ def action_list_name(wnd):
# connect the members module to its backends if necessary
if not members.connected(): members.connect()
# retrieve a list of members for term
# retrieve a list of members with similar names
member_list = members.list_name(name)
# format the data into a mess of text
@ -429,8 +476,11 @@ def action_list_name(wnd):
# display the mass of text with a pager
page( buf )
return False
def action_list_studentid(wnd):
"""Interactively search for members by student id."""
studentid = None
@ -458,6 +508,8 @@ def action_list_studentid(wnd):
# display the mass of text with a pager
page( buf )
return False
def null_callback(wnd):
"""Callback for unimplemented menu options."""
@ -479,7 +531,7 @@ top_menu = [
( "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 ),
( "Re Create an account", action_create_account ),
( "Library functions", null_callback ),
( "Exit", exit_callback ),
]
@ -490,11 +542,10 @@ def acquire_ceo_wnd(screen=None):
# 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
if screen is None:
screen = globals()['screen']
else:
_screen = screen
globals()['screen'] = screen
# if the screen changes size, a mess may be left
screen.erase()
@ -526,13 +577,21 @@ def ceo_main_curses(screen):
# create ceo window
ceo_wnd, menu_y, menu_x, menu_height, menu_width = acquire_ceo_wnd(screen)
try:
# display the top level menu
menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
finally:
members.disconnect()
accounts.disconnect()
def run():
"""Main function for legacy UI."""
# workaround for xterm-color (bad terminfo? - curs_set(0) fails)
if "TERM" in os.environ and os.environ['TERM'] == "xterm-color":
os.environ['TERM'] = "xterm"
# wrap the entire program using curses.wrapper
# so that the terminal is restored to a sane state
# when the program exits
@ -541,7 +600,7 @@ def run():
except KeyboardInterrupt:
pass
except curses.error:
print "Your screen is too small!"
print "Is your screen too small?"
raise
except:
reset()

View File

@ -1,12 +1,9 @@
# $Id$
"""
Backends
Backend Modules
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
db - CEO database interface for member registrations
ldapi - LDAP interface for UNIX account attribute administration
krb - Kerberos interface for UNIX account password management
"""

View File

@ -1,4 +1,3 @@
# $Id: db.py 37 2006-12-28 10:00:50Z mspang $
"""
Database Backend Interface
@ -7,7 +6,7 @@ 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,
This module makes use of the PyGreSQL Python bindings to libpq,
PostgreSQL's native C client library.
"""
import pgdb
@ -20,7 +19,7 @@ class DBException(Exception):
class DBConnection(object):
"""
Connection to CEO's backend database. All database queries
A connection to CEO's backend database. All database queries
and updates are made via this class.
Exceptions: (all methods)
@ -84,7 +83,7 @@ class DBConnection(object):
def connected(self):
"""Determine whether the connection has been established."""
return self.cnx != None
return self.cnx is not None
def commit(self):
@ -130,8 +129,7 @@ class DBConnection(object):
# 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] = {
members_dict[member[0]] = {
'memberid': member[0],
'name': member[1],
'studentid': member[2],
@ -236,13 +234,13 @@ class DBConnection(object):
return self.select_single_member(sql, params)
def select_member_by_account(self, username):
def select_member_by_userid(self, username):
"""
Retrieves a single member by UNIX account username.
See: self.select_single_member()
Example: connection.select_member_by_account('ctdalek') ->
Example: connection.select_member_by_userid('ctdalek') ->
{ 'memberid': 0, 'name': 'Calum T. Dalek' ...}
"""
sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE userid=%s"
@ -266,7 +264,7 @@ class DBConnection(object):
return self.select_single_member(sql, params)
def insert_member(self, name, studentid=None, program=None):
def insert_member(self, name, studentid=None, program=None, mtype='user', userid=None):
"""
Creates a member with the specified attributes.
@ -274,6 +272,8 @@ class DBConnection(object):
name - full name of member
studentid - student id number
program - program of study
mtype - member type
userid - account id
Example: connection.insert_member('Michael Spang', '99999999', 'Math/CS') -> 3349
@ -287,8 +287,8 @@ class DBConnection(object):
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' ]
sql = "INSERT INTO members (memberid, name, studentid, program, type, userid) VALUES (%d, %s, %s, %s, %s, %s)"
params = [ memberid, name, studentid, program, mtype, userid ]
self.cursor.execute(sql, params)
return memberid
@ -497,8 +497,8 @@ class DBConnection(object):
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.
member. For use after testing to prevent large intervals of unused
memberids from developing.
Note: this does nothing unless the most recently added member(s) have been deleted
"""
@ -509,40 +509,163 @@ class DBConnection(object):
### Tests ###
if __name__ == '__main__':
HOST = "localhost"
DATABASE = "ceo"
from csc.common.test import *
conffile = "/etc/csc/pgsql.cf"
cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
hostnm = cfg['server'][1:-1]
dbase = cfg['database'][1:-1]
# t=test m=member s=student d=default e=expected u=updated
tmname = 'Test Member'
tmuname = 'Member Test'
tmsid = '00000004'
tmusid = '00000008'
tmprogram = 'Undecidable'
tmuprogram = 'Nondetermined'
tmtype = 'Untyped'
tmutype = 'Poly'
tmuserid = 'tmem'
tmuuserid = 'identifier'
tm2name = 'Test Member 2'
tm2sid = '00000005'
tm2program = 'Undeclared'
tm3name = 'T. M. 3'
dtype = 'user'
tmterm = 'w0000'
tm3term = 'f1112'
tm3term2 = 's1010'
emdict = { 'name': tmname, 'program': tmprogram, 'studentid': tmsid, 'type': tmtype, 'userid': tmuserid }
emudict = { 'name': tmuname, 'program': tmuprogram, 'studentid': tmusid, 'type': tmutype, 'userid': tmuuserid }
em2dict = { 'name': tm2name, 'program': tm2program, 'studentid': tm2sid, 'type': dtype, 'userid': None }
em3dict = { 'name': tm3name, 'program': None, 'studentid': None, 'type': dtype, 'userid': None }
test(DBConnection)
connection = DBConnection()
success()
print "Running disconnect()"
connection.disconnect()
test(connection.connect)
connection.connect(hostnm, dbase)
success()
print "Running connect('%s', '%s')" % (HOST, DATABASE)
connection.connect(HOST, DATABASE)
test(connection.connected)
assert_equal(True, connection.connected())
success()
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])) + " ...]"
test(connection.insert_member)
tmid = connection.insert_member(tmname, tmsid, tmprogram, tmtype, tmuserid)
tm2id = connection.insert_member(tm2name, tm2sid, tm2program)
tm3id = connection.insert_member(tm3name)
assert_equal(True, int(tmid) >= 0)
assert_equal(True, int(tmid) >= 0)
success()
print "Running insert_member('test_member', '99999999', 'program')",
memberid = connection.insert_member('test_member', '99999999', 'program')
print "->", memberid
emdict['memberid'] = tmid
emudict['memberid'] = tmid
em2dict['memberid'] = tm2id
em3dict['memberid'] = tm3id
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')
test(connection.select_member_by_id)
m1 = connection.select_member_by_id(tmid)
m2 = connection.select_member_by_id(tm2id)
m3 = connection.select_member_by_id(tm3id)
assert_equal(emdict, m1)
assert_equal(em2dict, m2)
assert_equal(em3dict, m3)
success()
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)
test(connection.select_all_members)
members = connection.select_all_members()
assert_equal(True, tmid in members)
assert_equal(True, tm2id in members)
assert_equal(True, tm3id in members)
assert_equal(emdict, members[tmid])
success()
print "Running rollback()"
test(connection.select_members_by_name)
members = connection.select_members_by_name(tmname)
assert_equal(True, tmid in members)
assert_equal(False, tm3id in members)
assert_equal(emdict, members[tmid])
success()
test(connection.select_member_by_userid)
assert_equal(emdict, connection.select_member_by_userid(tmuserid))
success()
test(connection.insert_term)
connection.insert_term(tmid, tmterm)
connection.insert_term(tm3id, tm3term)
connection.insert_term(tm3id, tm3term2)
success()
test(connection.select_members_by_term)
members = connection.select_members_by_term(tmterm)
assert_equal(True, tmid in members)
assert_equal(False, tm2id in members)
assert_equal(False, tm3id in members)
success()
test(connection.select_term)
assert_equal(tmterm, connection.select_term(tmid, tmterm))
assert_equal(None, connection.select_term(tm2id, tmterm))
assert_equal(tm3term, connection.select_term(tm3id, tm3term))
assert_equal(tm3term2, connection.select_term(tm3id, tm3term2))
success()
test(connection.select_terms)
trms = connection.select_terms(tmid)
trms2 = connection.select_terms(tm2id)
assert_equal([tmterm], trms)
assert_equal([], trms2)
success()
test(connection.delete_term)
assert_equal(tm3term, connection.select_term(tm3id, tm3term))
connection.delete_term(tm3id, tm3term)
assert_equal(None, connection.select_term(tm3id, tm3term))
success()
test(connection.update_member)
connection.update_member({'memberid': tmid, 'name': tmuname})
connection.update_member({'memberid': tmid, 'program': tmuprogram, 'studentid': tmusid })
connection.update_member({'memberid': tmid, 'userid': tmuuserid, 'type': tmutype })
assert_equal(emudict, connection.select_member_by_id(tmid))
connection.update_member(emdict)
assert_equal(emdict, connection.select_member_by_id(tmid))
success()
test(connection.delete_term_all)
connection.delete_term_all(tm2id)
connection.delete_term_all(tm3id)
assert_equal([], connection.select_terms(tm2id))
assert_equal([], connection.select_terms(tm3id))
success()
test(connection.delete_member)
connection.delete_member(tm3id)
assert_equal(None, connection.select_member_by_id(tm3id))
negative(connection.delete_member, (tmid,), DBException, "delete of term-registered member")
success()
test(connection.rollback)
connection.rollback()
assert_equal(None, connection.select_member_by_id(tm2id))
success()
print "Resetting memberid sequence"
test(connection.commit)
connection.commit()
success()
test(connection.trim_memberid_sequence)
connection.trim_memberid_sequence()
success()
print "Running disconnect()"
test(connection.disconnect)
connection.disconnect()
assert_equal(False, connection.connected())
connection.disconnect()
success()

View File

@ -1,4 +1,3 @@
# $Id: ipc.py 26 2006-12-20 21:25:08Z mspang $
"""
IPC Library Functions
@ -14,22 +13,21 @@ 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 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().
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.
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'
status = 'open'
if self.closed:
status = 'closed'
return "<" + status + " pty '" + os.ttyname(self.fd) + "'>"
@ -43,7 +41,7 @@ class _pty_file(object):
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:
if not block and len(select.select([self.fd], [], [], timeout)[0]) == 0:
break
data = os.read(self.fd, 65536)
@ -61,7 +59,7 @@ class _pty_file(object):
try:
# wait timeout for the pty to become ready, then read
if block or len(select.select([self.fd],[],[], timeout)[0]) != 0:
if block or len(select.select([self.fd], [], [], timeout)[0]) != 0:
self.buffer += os.read(self.fd, size - len(self.buffer) )
except OSError:
@ -78,7 +76,7 @@ class _pty_file(object):
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:
if not block and len(select.select([self.fd], [], [], timeout)[0]) == 0:
break
data = os.read(self.fd, 128)
@ -94,7 +92,7 @@ class _pty_file(object):
line = self.buffer[:split_index]
self.buffer = self.buffer[split_index:]
return line
def readlines(self, sizehint=None, block=True, timeout=0.1):
def readlines(self, sizehint=None, timeout=0.1):
lines = []
line = None
while True:
@ -138,7 +136,7 @@ def popeni(command, args, env=None):
args - a list of arguments to pass to command
env - optional environment for command
Returns: (pid, stdout, stdIn)
Returns: (pid, stdout, stdin)
"""
# use a pipe to send data to the child
@ -181,7 +179,7 @@ def popeni(command, args, env=None):
# 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);
fd = os.open(tty, os.O_RDWR)
os.close(fd)
# init stdin/out/err
@ -209,14 +207,21 @@ def popeni(command, args, env=None):
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'])
from csc.common.test import *
send.write("listprincs\n")
prog = '/bin/cat'
argv = [ prog ]
message = "test\n"
test(popeni)
proc, recv, send = popeni(prog, argv)
send.write(message)
send.flush()
print recv.readlines()
line = recv.readline()
assert_equal(message.strip(), line.strip())
success()

View File

@ -1,4 +1,3 @@
# $Id: krb.py 40 2006-12-29 00:40:31Z mspang $
"""
Kerberos Backend Interface
@ -12,8 +11,8 @@ 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.
temporary workaround, this module communicates with the kadmin CLI interface
via a pseudo-terminal and a pipe.
"""
import os
import ipc
@ -109,7 +108,7 @@ class KrbConnection(object):
def connected(self):
"""Determine whether the connection has been established."""
return self.pid != None
return self.pid is not None
@ -125,6 +124,7 @@ class KrbConnection(object):
# list of lines output by kadmin
result = []
lines = []
# the kadmin prompt that signals the end output
# note: KADMIN_ARGS[0] must be "kadmin" or the actual prompt will differ
@ -137,12 +137,12 @@ class KrbConnection(object):
timeout_maximum = 1.00
# input loop: read from kadmin until the kadmin prompt
buffer = ''
buf = ''
while True:
# attempt to read any available data
data = self.kadm_out.read(block=False, timeout=timeout)
buffer += data
buf += data
# nothing was read
if data == '':
@ -165,20 +165,20 @@ class KrbConnection(object):
else:
# kadmin died!
raise KrbException("kadmin died while reading response")
raise KrbException("kadmin died while reading response:\n%s\n%s" % ("\n".join(lines), buf))
# break into lines and save all but the final
# line (which is incomplete) into result
lines = buffer.split("\n")
buffer = lines[-1]
lines = buf.split("\n")
buf = 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,
# if the incomplete line in the buffer is the kadmin prompt,
# then the result is complete and may be returned
if buffer.strip() == prompt:
if buf.strip() == prompt:
break
return result
@ -189,7 +189,7 @@ class KrbConnection(object):
Helper function to execute a kadmin command.
Parameters:
command - the command to execute
command - command string to pass on to kadmin
Returns: a list of lines output by the command
"""
@ -222,8 +222,8 @@ class KrbConnection(object):
"ceo/admin@CSCLUB.UWATERLOO.CA",
"sysadmin/admin@CSCLUB.UWATERLOO.CA",
"mspang@CSCLUB.UWATERLOO.CA",
...
]
"""
principals = self.execute("list_principals")
@ -374,16 +374,13 @@ class KrbConnection(object):
# ensure success message was received
if not created:
raise KrbException("did not receive principal created in response")
raise KrbException("kadmin did not acknowledge principal creation")
def delete_principal(self, principal):
"""
Delete a principal.
Parameters:
principal - the principal name
Example: connection.delete_principal("mspang@CSCLUB.UWATERLOO.CA")
"""
@ -424,25 +421,116 @@ class KrbConnection(object):
raise KrbException("did not receive principal deleted")
def change_password(self, principal, password):
"""
Changes a principal's password.
Example: connection.change_password("mspang@CSCLUB.UWATERLOO.CA", "opensesame")
"""
# exec the add_principal command
if password.find('"') == -1:
self.kadm_in.write('change_password -pw "' + password + '" "' + principal + '"\n')
else:
self.kadm_in.write('change_password "' + 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
changed = 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("Password") == 0 and line.find("changed.") != 0:
changed = True
# error messages
elif line.find("change_password:") == 0 or line.find("kadmin:") == 0:
raise KrbException(line)
# unknown output
else:
raise KrbException("unexpected change_password output: " + line)
# ensure success message was received
if not changed:
raise KrbException("kadmin did not acknowledge password change")
### Tests ###
if __name__ == '__main__':
PRINCIPAL = 'ceo/admin@CSCLUB.UWATERLOO.CA'
KEYTAB = 'ceo.keytab'
from csc.common.test import *
import random
conffile = '/etc/csc/kerberos.cf'
cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
principal = cfg['admin_principal'][1:-1]
keytab = cfg['admin_keytab'][1:-1]
realm = cfg['realm'][1:-1]
# t=test p=principal e=expected
tpname = 'testpirate' + '@' + realm
tpw = str(random.randint(10**30, 10**31-1)) + 'YAR!'
eprivs = ['GET', 'ADD', 'MODIFY', 'DELETE']
test(KrbConnection)
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()
success()
test(connection.connect)
connection.connect(principal, keytab)
success()
try:
connection.delete_principal(tpname)
except KrbException:
pass
test(connection.connected)
assert_equal(True, connection.connected())
success()
test(connection.add_principal)
connection.add_principal(tpname, tpw)
success()
test(connection.list_principals)
pals = connection.list_principals()
assert_equal(True, tpname in pals)
success()
test(connection.get_privs)
privs = connection.get_privs()
assert_equal(eprivs, privs)
success()
test(connection.get_principal)
princ = connection.get_principal(tpname)
assert_equal(tpname, princ['Principal'])
success()
test(connection.delete_principal)
connection.delete_principal(tpname)
assert_equal(None, connection.get_principal(tpname))
success()
test(connection.disconnect)
connection.disconnect()
assert_equal(False, connection.connected())
success()

View File

@ -1,4 +1,3 @@
# $Id: ldapi.py 41 2006-12-29 04:22:31Z mspang $
"""
LDAP Backend Interface
@ -60,7 +59,7 @@ class LDAPConnection(object):
"""
if bind_pw == None: bind_pw = ''
if bind_pw is None: bind_pw = ''
try:
@ -93,7 +92,7 @@ class LDAPConnection(object):
def connected(self):
"""Determine whether the connection has been established."""
return self.ldap != None
return self.ldap is not None
@ -137,7 +136,7 @@ class LDAPConnection(object):
Retrieve the attributes of a user.
Parameters:
uid - the UNIX user accound name of the user
uid - the UNIX username to look up
Returns: attributes of user with uid
@ -145,23 +144,25 @@ class LDAPConnection(object):
{ 'uid': 'mspang', 'uidNumber': 21292 ...}
"""
if not self.connected(): raise LDAPException("Not connected!")
dn = 'uid=' + uid + ',' + self.user_base
return self.lookup(dn)
def user_search(self, filter):
def user_search(self, search_filter):
"""
Helper for user searches.
Parameters:
filter - LDAP filter string to match users against
search_filter - LDAP filter string to match users against
Returns: the list of uids matched
Returns: the list of uids matched (usernames)
"""
# search for entries that match the filter
try:
matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, filter)
matches = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, search_filter)
except ldap.LDAPError, e:
raise LDAPException("user search failed: %s" % e)
@ -196,47 +197,45 @@ class LDAPConnection(object):
Parameters:
uidNumber - the user id of the accounts desired
Returns: the list of uids matched
Returns: the list of uids matched (usernames)
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)
search_filter = '(&(objectClass=posixAccount)(uidNumber=%d))' % uidNumber
return self.user_search(search_filter)
def user_search_gid(self, gidNumber):
"""
Retrieves a list of users with a certain UNIX gid number.
Retrieves a list of users with a certain UNIX gid
number (search by default group).
Parameters:
gidNumber - the group id of the accounts desired
Returns: the list of uids matched
Returns: the list of uids matched (usernames)
"""
# search for posixAccount entries with the specified gidNumber
filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
return self.user_search(filter)
search_filter = '(&(objectClass=posixAccount)(gidNumber=%d))' % gidNumber
return self.user_search(search_filter)
def user_add(self, uid, cn, loginShell, uidNumber, gidNumber, homeDirectory, gecos):
def user_add(self, uid, cn, uidNumber, gidNumber, homeDirectory, loginShell=None, gecos=None, description=None):
"""
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
cn - the real name of the member
uidNumber - the UNIX user id number
gidNumber - the UNIX group id number
gidNumber - the UNIX group id number (default group)
homeDirectory - home directory for the user
gecos - comment field (usually stores miscellania)
loginShell - login shell for the user
gecos - comment field (usually stores name etc)
description - description field (optional and unimportant)
Example: connection.user_add('mspang', 'Michael Spang',
'/bin/bash', 21292, 100, '/users/mspang',
21292, 100, '/users/mspang', '/bin/bash',
'Michael Spang,,,')
"""
@ -252,6 +251,11 @@ class LDAPConnection(object):
'gecos': [ gecos ],
}
if loginShell:
attrs['loginShell'] = loginShell
if description:
attrs['description'] = [ description ]
try:
modlist = ldap.modlist.addModlist(attrs)
self.ldap.add_s(dn, modlist)
@ -265,7 +269,7 @@ class LDAPConnection(object):
Parameters:
uid - username of the user to modify
entry - dictionary as returned by user_lookup() with changes to make.
attrs - dictionary as returned by user_lookup() with changes to make.
omitted attributes are DELETED.
Example: user = user_lookup('mspang')
@ -295,9 +299,6 @@ class LDAPConnection(object):
"""
Removes a user from the directory.
Parameters:
uid - the UNIX username of the account
Example: connection.user_delete('mspang')
"""
@ -318,7 +319,7 @@ class LDAPConnection(object):
Parameters:
cn - the UNIX group name to lookup
Returns: attributes of group with cn
Returns: attributes of the group's LDAP entry
Example: connection.group_lookup('office') -> {
'cn': 'office',
@ -335,9 +336,6 @@ class LDAPConnection(object):
"""
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']
@ -345,8 +343,8 @@ class LDAPConnection(object):
# 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)
search_filter = '(&(objectClass=posixGroup)(gidNumber=%d))' % gidNumber
matches = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, search_filter)
except ldap.LDAPError,e :
raise LDAPException("group search failed: %s" % e)
@ -370,15 +368,11 @@ class LDAPConnection(object):
return group_cns
def group_add(self, cn, gidNumber):
def group_add(self, cn, gidNumber, description=None):
"""
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)
Example: connection.group_add('office', 1001, 'Office Staff')
"""
dn = 'cn=' + cn + ',' + self.group_base
@ -387,6 +381,8 @@ class LDAPConnection(object):
'cn': [ cn ],
'gidNumber': [ str(gidNumber) ],
}
if description:
attrs['description'] = description
try:
modlist = ldap.modlist.addModlist(attrs)
@ -399,9 +395,8 @@ class LDAPConnection(object):
"""
Update group attributes in the directory.
The only available updates are fairly destructive
(rename or renumber) but this method is provided
for completeness.
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
@ -436,9 +431,6 @@ class LDAPConnection(object):
"""
Removes a group from the directory."
Parameters:
cn - the name of the group
Example: connection.group_delete('office')
"""
@ -449,129 +441,203 @@ class LDAPConnection(object):
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):
def used_uids(self, minimum=None, maximum=None):
"""
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.
Compiles a list of used UIDs in a range.
Parameters:
minimum - smallest uid that may be returned
maximum - largest uid that may be returned
minimum - smallest uid to return in the list
maximum - largest uid to return in the list
Returns: the id, or None if there are none available
Returns: list of integer uids
Example: connection.first_id(20000, 40000) -> 20018
Example: connection.used_uids(20000, 40000) -> [20000, 20001, ...]
"""
# 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:
if (not minimum or uid >= minimum) and (not maximum or uid <= maximum):
uids.append(uid)
# compile a list of used gids
return uids
def used_gids(self, minimum=None, maximum=None):
"""
Compiles a list of used GIDs in a range.
Parameters:
minimum - smallest gid to return in the list
maximum - largest gid to return in the list
Returns: list of integer gids
Example: connection.used_gids(20000, 40000) -> [20000, 20001, ...]
"""
try:
groups = self.ldap.search_s(self.group_base, ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)', ['gidNumber'])
users = self.ldap.search_s(self.user_base, ldap.SCOPE_SUBTREE, '(objectClass=posixAccount)', ['gidNumber'])
except ldap.LDAPError, e:
raise LDAPException("search for gids failed: %s" % e)
gids = []
for group in groups:
dn, attrs = group
for user in users:
dn, attrs = user
gid = int(attrs['gidNumber'][0])
if minimum <= gid <= maximum:
if (not minimum or gid >= minimum) and (not maximum or 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
return gids
# 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()
from csc.common.test import *
conffile = '/etc/csc/ldap.cf'
cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
srvurl = cfg['server_url'][1:-1]
binddn = cfg['admin_bind_dn'][1:-1]
bindpw = cfg['admin_bind_pw'][1:-1]
ubase = cfg['users_base'][1:-1]
gbase = cfg['groups_base'][1:-1]
minid = 99999000
maxid = 100000000
# t=test u=user g=group c=changed r=real e=expected
tuname = 'testuser'
turname = 'Test User'
tuhome = '/home/testuser'
tushell = '/bin/false'
tugecos = 'Test User,,,'
tgname = 'testgroup'
cushell = '/bin/true'
cuhome = '/home/changed'
curname = 'Test Modified User'
test("LDAPConnection()")
connection = LDAPConnection()
print "running disconnect()"
success()
test("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
success()
test("connect()")
connection.connect(srvurl, binddn, bindpw, ubase, gbase)
if not connection.connected():
fail("not connected")
success()
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.user_delete(tuname)
connection.group_delete(tgname)
except LDAPException:
pass
test("used_uids()")
uids = connection.used_uids(minid, maxid)
if type(uids) is not list:
fail("list not returned")
success()
test("used_gids()")
gids = connection.used_gids(minid, maxid)
if type(gids) is not list:
fail("list not returned")
success()
unusedids = []
for idnum in xrange(minid, maxid):
if not idnum in uids and not idnum in gids:
unusedids.append(idnum)
tuuid = unusedids.pop()
tugid = unusedids.pop()
eudata = {
'uid': [ tuname ],
'loginShell': [ tushell ],
'uidNumber': [ str(tuuid) ],
'gidNumber': [ str(tugid) ],
'gecos': [ tugecos ],
'homeDirectory': [ tuhome ],
'cn': [ turname ]
}
test("user_add()")
connection.user_add(tuname, turname, tuuid, tugid, tuhome, tushell, tugecos)
success()
tggid = unusedids.pop()
egdata = {
'cn': [ tgname ],
'gidNumber': [ str(tggid) ]
}
test("group_add()")
connection.group_add(tgname, tggid)
success()
test("user_lookup()")
udata = connection.user_lookup(tuname)
del udata['objectClass']
assert_equal(eudata, udata)
success()
test("group_lookup()")
gdata = connection.group_lookup(tgname)
del gdata['objectClass']
assert_equal(egdata, gdata)
success()
test("user_search_id()")
eulist = [ tuname ]
ulist = connection.user_search_id(tuuid)
assert_equal(eulist, ulist)
success()
test("user_search_gid()")
ulist = connection.user_search_gid(tugid)
if tuname not in ulist:
fail("(%s) not in (%s)" % (tuname, ulist))
success()
ecudata = connection.user_lookup(tuname)
ecudata['loginShell'] = [ cushell ]
ecudata['homeDirectory'] = [ cuhome ]
ecudata['cn'] = [ curname ]
test("user_modify")
connection.user_modify(tuname, ecudata)
cudata = connection.user_lookup(tuname)
assert_equal(ecudata, cudata)
success()
ecgdata = connection.group_lookup(tgname)
ecgdata['memberUid'] = [ tuname ]
test("group_modify()")
connection.group_modify(tgname, ecgdata)
cgdata = connection.group_lookup(tgname)
assert_equal(ecgdata, cgdata)
success()
test("user_delete()")
connection.group_delete(tgname)
success()
test("disconnect()")
connection.disconnect()
success()

View File

@ -1,3 +1,7 @@
"""
Generally Useful Common Modules
conf - simple configuration file reader
excep - generally useful exceptions
test - test suite utility routines
"""

View File

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

12
pylib/csc/common/excep.py Normal file
View File

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

42
pylib/csc/common/test.py Normal file
View File

@ -0,0 +1,42 @@
"""
Common Test Routines
This module contains helpful functions called by each module's test suite.
"""
from types import FunctionType, MethodType, ClassType, TypeType
class TestException(Exception):
"""Exception class for test failures."""
def test(subject):
"""Print a test message."""
if type(subject) in (MethodType, FunctionType, ClassType, TypeType):
print "testing %s()..." % subject.__name__,
else:
print "testing %s..." % subject,
def success():
"""Print a success message."""
print "pass."
def assert_equal(expected, actual):
if expected != actual:
message = "Expected (%s)\nWas (%s)" % (repr(expected), repr(actual))
fail(message)
def fail(message):
print "failed!"
raise TestException("Test failed:\n%s" % message)
def negative(call, args, excep, message):
try:
call(*args)
fail(message)
except excep:
pass

View File

@ -1,5 +1,4 @@
#!/bin/sh
# $Id: initialize.sh 13 2006-12-15 03:57:00Z mspang $
# Initializes a database for CEO.
# initialize the database

View File

@ -1,4 +1,3 @@
-- $Id: structure.sql 36 2006-12-28 10:00:11Z mspang $
-- Table structure for CEO's SQL database.
-- Usage:

View File

@ -1,4 +1,3 @@
-- $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.

View File

@ -1,4 +1,3 @@
-- $Id$
-- PL/Python trigger to verify terms for validity
-- To (re)install: