forked from public/pyceo
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:
parent
cb59e85c2e
commit
58bf72726a
16
bin/ceo
16
bin/ceo
|
@ -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',
|
||||
'SSH_CLIENT']
|
||||
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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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 ###
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# /etc/csc/pgsql.cf: PostgreSQL database configuration
|
||||
|
||||
### Database Configuration ###
|
||||
|
||||
# server = "localhost"
|
||||
server = ""
|
||||
database = "ceo"
|
||||
|
||||
# not used
|
||||
user = "ceo"
|
||||
password = "secret"
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
# read password
|
||||
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)
|
||||
|
||||
# display the top level menu
|
||||
menu(ceo_wnd, menu_y, menu_x, menu_width, top_menu, acquire_ceo_wnd)
|
||||
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()
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,8 +41,8 @@ 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:
|
||||
break
|
||||
if not block and len(select.select([self.fd], [], [], timeout)[0]) == 0:
|
||||
break
|
||||
|
||||
data = os.read(self.fd, 65536)
|
||||
self.buffer += data
|
||||
|
@ -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,8 +76,8 @@ 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:
|
||||
break
|
||||
if not block and len(select.select([self.fd], [], [], timeout)[0]) == 0:
|
||||
break
|
||||
|
||||
data = os.read(self.fd, 128)
|
||||
self.buffer += data
|
||||
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Generally Useful Common Modules
|
||||
|
||||
conf - simple configuration file reader
|
||||
excep - generally useful exceptions
|
||||
test - test suite utility routines
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
val = int(val)
|
||||
except:
|
||||
if "." in val:
|
||||
val = float(val)
|
||||
else:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# save key and value
|
||||
options[key] = val
|
||||
|
||||
# found only key, value = None
|
||||
elif len(pair[0]) > 1:
|
||||
key, = pair
|
||||
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))
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
-- $Id: structure.sql 36 2006-12-28 10:00:11Z mspang $
|
||||
-- Table structure for CEO's SQL database.
|
||||
|
||||
-- Usage:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
-- $Id$
|
||||
-- PL/Python trigger to verify terms for validity
|
||||
|
||||
-- To (re)install:
|
||||
|
|
Loading…
Reference in New Issue