diff --git a/bin/ceo b/bin/ceo index 8550f79..1034117 100755 --- a/bin/ceo +++ b/bin/ceo @@ -15,7 +15,7 @@ os.environ['LESSSECURE'] = '1' os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin' for pathent in sys.path[:]: - if not pathent.find('/usr') == 0: + if not pathent.find('/usr') == 0 and not pathent.find('/var') == 0: sys.path.remove(pathent) euid = os.geteuid() @@ -27,5 +27,5 @@ except OSError, e: print str(e) sys.exit(1) -import csc.apps.legacy.main -csc.apps.legacy.main.run() +import csc.apps.urwid.main +csc.apps.urwid.main.start() diff --git a/bin/ceo-old b/bin/ceo-old new file mode 100755 index 0000000..8550f79 --- /dev/null +++ b/bin/ceo-old @@ -0,0 +1,31 @@ +#!/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_MESSAGES', 'LC_MONETARY', + 'LC_NUMERIC', 'LC_TIME', 'UID', 'GID', 'SSH_CONNECTION', 'SSH_AUTH_SOCK', + 'SSH_CLIENT'] + +for key in os.environ.keys(): + if key not in safe_environment: + del os.environ[key] + +os.environ['LESSSECURE'] = '1' +os.environ['PATH'] = '/usr/sbin:/usr/bin:/sbin:/bin' + +for pathent in sys.path[:]: + if not pathent.find('/usr') == 0: + sys.path.remove(pathent) + +euid = os.geteuid() +egid = os.getegid() +try: + os.setreuid(euid, euid) + os.setregid(egid, egid) +except OSError, e: + print str(e) + sys.exit(1) + +import csc.apps.legacy.main +csc.apps.legacy.main.run() diff --git a/debian/rules b/debian/rules index 332259d..f4af3e4 100755 --- a/debian/rules +++ b/debian/rules @@ -7,6 +7,7 @@ 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-old"' -o build/ceo-old misc/setuid-prog.c $(CC) -DFULL_PATH='"/usr/lib/csc/addhomedir"' -o build/addhomedir misc/setuid-prog.c $(CC) -DFULL_PATH='"/usr/lib/csc/ceoquery"' -o build/ceoquery misc/setuid-prog.c $(CC) -DFULL_PATH='"/usr/lib/csc/csc-chfn"' -o build/csc-chfn misc/setuid-prog.c @@ -31,8 +32,8 @@ install: build dh_install etc/* etc/csc/ dh_install sql/* usr/share/csc/ - dh_install bin/ceo bin/addhomedir bin/ceoquery bin/csc-chsh bin/csc-chfn usr/lib/csc/ - dh_install build/ceo build/addhomedir build/ceoquery build/csc-chsh build/csc-chfn usr/bin/ + dh_install bin/ceo bin/ceo-old bin/addhomedir bin/ceoquery bin/csc-chsh bin/csc-chfn usr/lib/csc/ + dh_install build/ceo build/ceo-old build/addhomedir build/ceoquery build/csc-chsh build/csc-chfn usr/bin/ dh_install misc/csc.schema etc/ldap/schema/ binary-arch: build install diff --git a/docs/TODO b/docs/TODO index f634051..e146ba5 100644 --- a/docs/TODO +++ b/docs/TODO @@ -2,7 +2,6 @@ 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 diff --git a/pylib/csc/apps/urwid/__init__.py b/pylib/csc/apps/urwid/__init__.py new file mode 100644 index 0000000..67bb77a --- /dev/null +++ b/pylib/csc/apps/urwid/__init__.py @@ -0,0 +1,3 @@ +""" +Urwid User Interface +""" diff --git a/pylib/csc/apps/urwid/info.py b/pylib/csc/apps/urwid/info.py new file mode 100644 index 0000000..8e0bd76 --- /dev/null +++ b/pylib/csc/apps/urwid/info.py @@ -0,0 +1,39 @@ +import urwid + +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * + +from csc.adm import accounts, members +from csc.common.excep import InvalidArgument + +class InfoPage(WizardPanel): + def init_widgets(self): + self.userid = urwid.Text("") + self.name = urwid.Text("") + self.terms = urwid.Text("") + self.program = urwid.Text("") + + self.widgets = [ + urwid.Text( "Member Details" ), + urwid.Divider(), + self.name, + self.userid, + self.program, + urwid.Divider(), + self.terms, + ] + def focusable(self): + return False + def activate(self): + member = self.state.get('member', {}) + name = member.get('cn', [''])[0] + userid = self.state['userid'] + program = member.get('program', [''])[0] + terms = member.get('term', []) + + self.name.set_text("Name: %s" % name) + self.userid.set_text("User: %s" % userid) + self.program.set_text("Program: %s" % program) + self.terms.set_text("Terms: %s" % ", ".join(terms)) + def check(self): + pop_window() diff --git a/pylib/csc/apps/urwid/main.py b/pylib/csc/apps/urwid/main.py new file mode 100644 index 0000000..a05f9f6 --- /dev/null +++ b/pylib/csc/apps/urwid/main.py @@ -0,0 +1,122 @@ +import random, time +import urwid, urwid.curses_display + +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * +import csc.apps.urwid.newmember as newmember +import csc.apps.urwid.renew as renew +import csc.apps.urwid.info as info +import csc.apps.urwid.search as search + +from csc.adm import accounts, members, terms +from csc.common.excep import InvalidArgument + +ui = urwid.curses_display.Screen() + +ui.register_palette([ + # name, foreground, background, mono + ('banner', 'light gray', 'default', None), + ('menu', 'light gray', 'default', 'bold'), + ('selected', 'black', 'light gray', 'bold'), +]) + + +def program_name(): + cwords = [ "CSC" ] * 20 + [ "Club" ] * 10 + [ "Campus" ] * 5 + \ + [ "Communist", "Canadian", "Celestial", "Cryptographic", "Calum's", + "Canonical", "Capitalist", "Catastrophic", "Ceremonial", "Chaotic", "Civic", + "City", "County", "Caffeinated" ] + ewords = [ "Embellished", "Ergonomic", "Electric", "Eccentric", "European", "Economic", + "Evil", "Egotistical", "Elliptic", "Emasculating", "Embalming", + "Embryonic", "Emigrant", "Emissary's", "Emoting", "Employment", "Emulated", + "Enabling", "Enamoring", "Encapsulated", "Enchanted", "Encoded", "Encrypted", + "Encumbered", "Endemic", "Enhanced", "Enigmatic", "Enlightened", "Enormous", + "Enrollment", "Enshrouded", "Ephermal", "Epidemic", "Episodic", "Epsilon", + "Equitable", "Equestrian", "Equilateral", "Erroneous", "Erratic", + "Espresso", "Essential", "Estate", "Esteemed", "Eternal", "Ethical", "Eucalyptus", + "Euphemistic", "Envangelist", "Evasive", "Everyday", "Evidence", "Eviction", "Evildoer's", + "Evolution", "Exacerbation", "Exalted", "Examiner's", "Excise", "Exciting", "Exclusion", + "Exec", "Executioner's", "Exile", "Existential", "Expedient", "Expert", "Expletive", + "Exploiter's", "Explosive", "Exponential", "Exposing", "Extortion", "Extraction", + "Extraneous", "Extravaganza", "Extreme", "Extraterrestrial", "Extremist", "Eerie" ] + owords = [ "Office" ] * 50 + [ "Outhouse", "Outpost" ] + + cword = random.choice(cwords) + eword = random.choice(ewords) + oword = random.choice(owords) + + return "%s %s %s" % (cword, eword, oword) + +def menu_items(items): + return [ urwid.AttrWrap( ButtonText( cb, txt ), 'menu', 'selected') for (txt, cb) in items ] + +def main_menu(): + menu = [ + ("New Member", new_member), + ("Renew Membership", renew_member), + ("Display Member", display_member), + ("Search", search_members), + ("Exit", raise_abort), + ] + + listbox = urwid.ListBox( menu_items( menu ) ) + return listbox + +def push_wizard(name, pages, dimensions=(50, 10)): + state = {} + wiz = Wizard() + for page in pages: + wiz.add_panel( page(state) ) + push_window( urwid.Filler( urwid.Padding( + urwid.LineBox(wiz), 'center', dimensions[0]), + 'middle', dimensions[1] ), name ) + +def new_member(*args, **kwargs): + push_wizard("New Member", [ + newmember.IntroPage, + newmember.InfoPage, + newmember.SignPage, + newmember.PassPage, + newmember.EndPage, + ]) + +def renew_member(*args, **kwargs): + push_wizard("Renew Membership", [ + renew.IntroPage, + renew.UserPage, + renew.TermPage, + renew.PayPage, + renew.EndPage, + ]) + +def display_member(a): + push_wizard("Display Member", [ + renew.UserPage, + info.InfoPage, + ], (60, 15)) + +def search_members(a): + menu = [ + ("Members by term", search_term), + ("Members by name", search_name), + ("Back", raise_back), + ] + + listbox = urwid.ListBox( menu_items( menu ) ) + push_window(listbox, "Search") + +def search_name(a): + push_wizard("By Name", [ search.NamePage ]) + +def search_term(a): + push_wizard("By Term", [ search.TermPage ]) + +def run(): + push_window( main_menu(), program_name() ) + event_loop( ui ) + +def start(): + ui.run_wrapper( run ) + +if __name__ == '__main__': + start() diff --git a/pylib/csc/apps/urwid/newmember.py b/pylib/csc/apps/urwid/newmember.py new file mode 100644 index 0000000..994de80 --- /dev/null +++ b/pylib/csc/apps/urwid/newmember.py @@ -0,0 +1,134 @@ +import urwid +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * + +from csc.adm import accounts, members +from csc.common.excep import InvalidArgument + +class IntroPage(WizardPanel): + def init_widgets(self): + self.widgets = [ + urwid.Text( "Joining the Computer Science Club" ), + urwid.Divider(), + urwid.Text( "CSC membership is $2.00 for one term. Please ensure " + "the fee is deposited into the safe before continuing." ), + ] + def focusable(self): + return False + +class InfoPage(WizardPanel): + def init_widgets(self): + self.userid = WordEdit("UWdir ID: ") + self.name = SingleEdit("Full name: ") + self.program = SingleEdit("Program of Study: ") + self.widgets = [ + urwid.Text( "Member Information - Please Check ID" ), + urwid.Divider(), + self.userid, + self.name, + self.program, + ] + def check(self): + self.state['userid'] = self.userid.get_edit_text() + self.state['name'] = self.name.get_edit_text() + self.state['program'] = self.program.get_edit_text() + + if len( self.state['userid'] ) < 4: + self.focus_widget( self.userid ) + set_status("Username is too short") + return True + elif len( self.state['name'] ) < 4: + self.focus_widget( self.name ) + set_status("Name is too short") + return True + clear_status() + +class SignPage(WizardPanel): + def init_widgets(self): + self.widgets = [ + urwid.Text( "Machine Usage Policy" ), + urwid.Divider(), + urwid.Text( "Ensure the new member has signed the " + "Machine Usage Policy. Accounts of users who have not " + "signed will be suspended if discovered." ), + ] + def focusable(self): + return False + +class PassPage(WizardPanel): + def init_widgets(self): + self.password = PassEdit("Password: ") + self.pwcheck = PassEdit("Re-enter: ") + self.widgets = [ + urwid.Text( "Member Password" ), + urwid.Divider(), + self.password, + self.pwcheck, + ] + def focus_widget(self, widget): + self.box.set_focus( self.widgets.index( widget ) ) + def clear_password(self): + self.focus_widget( self.password ) + self.password.set_edit_text("") + self.pwcheck.set_edit_text("") + def check(self): + self.state['password'] = self.password.get_edit_text() + pwcheck = self.pwcheck.get_edit_text() + + if self.state['password'] != pwcheck: + self.clear_password() + set_status("Passwords do not match") + return True + elif len(self.state['password']) < 5: + self.clear_password() + set_status("Password is too short") + return True + clear_status() + +class EndPage(WizardPanel): + def init_widgets(self): + self.headtext = urwid.Text("") + self.midtext = urwid.Text("") + self.widgets = [ + self.headtext, + urwid.Divider(), + self.midtext, + ] + def focusable(self): + return False + def check(self): + pop_window() + def activate(self): + try: + if not members.connected(): members.connect() + members.new( self.state['userid'], self.state['name'], self.state['program'] ) + problem = None + except members.InvalidRealName: + problem = "Invalid real name" + except InvalidArgument, e: + if e.argname == 'uid' and e.explanation == 'duplicate uid': + problem = 'Duplicate userid' + else: + raise + if not problem: + try: + if not accounts.connected(): accounts.connect() + accounts.create_member( self.state['userid'], self.state['password'], self.state['name'] ) + except accounts.NameConflict, e: + problem = str(e) + except accounts.NoAvailableIDs, e: + problem = str(e) + except accounts.InvalidArgument, e: + problem = str(e) + except accounts.LDAPException, e: + problem = str(e) + except accounts.KrbException, e: + problem = str(e) + if problem: + self.headtext.set_text("Failed to add member") + self.midtext.set_text("The error was: '%s'" % problem) + else: + self.headtext.set_text("Member Added") + self.midtext.set_text("Congratulations, %s has been added " + "successfully. Please run 'addhomedir %s'." + % (self.state['userid'], self.state['userid'])) diff --git a/pylib/csc/apps/urwid/renew.py b/pylib/csc/apps/urwid/renew.py new file mode 100644 index 0000000..b0932ba --- /dev/null +++ b/pylib/csc/apps/urwid/renew.py @@ -0,0 +1,118 @@ +import urwid + +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * + +from csc.adm import members, terms + +class IntroPage(WizardPanel): + def init_widgets(self): + self.widgets = [ + urwid.Text( "Renewing Membership" ), + urwid.Divider(), + urwid.Text( "CSC membership is $2.00 per term. You may pre-register " + "for future terms if desired." ) + ] + def focusable(self): + return False + +class UserPage(WizardPanel): + def init_widgets(self): + self.userid = WordEdit("Username: ") + + self.widgets = [ + urwid.Text( "Member Information" ), + urwid.Divider(), + self.userid, + ] + def check(self): + self.state['userid'] = self.userid.get_edit_text() + self.state['member'] = None + if self.state['userid']: + if not members.connected(): members.connect() + self.state['member'] = members.get(self.userid.get_edit_text()) + if not self.state['member']: + set_status("Member not found") + self.focus_widget(self.userid) + return True + +class TermPage(WizardPanel): + def init_widgets(self): + self.start = SingleEdit("Start: ") + self.count = SingleIntEdit("Count: ") + + self.widgets = [ + urwid.Text( "Terms to Register" ), + urwid.Divider(), + self.start, + self.count, + ] + def activate(self): + if not self.start.get_edit_text(): + old_terms = [] + if 'term' in self.state['member']: + old_terms = self.state['member']['term'] + self.start.set_edit_text( terms.next_unregistered( old_terms ) ) + self.count.set_edit_text( "1" ) + def check(self): + try: + self.state['terms'] = terms.interval( self.start.get_edit_text(), self.count.value() ) + except e: + self.focus_widget( self.start ) + set_status( "Invalid start term" ) + return True + for term in self.state['terms']: + if members.registered( self.state['userid'], term): + self.focus_widget( self.start ) + set_status( "Already registered for " + term ) + return True + if len(self.state['terms']) == 0: + self.focus_widget(self.count) + set_status( "Registering for zero terms?" ) + return True + +class PayPage(WizardPanel): + def init_widgets(self): + self.midtext = urwid.Text("") + + self.widgets = [ + urwid.Text("Membership Fee"), + urwid.Divider(), + self.midtext, + ] + def focusable(self): + return False + def activate(self): + regterms = self.state['terms'] + plural = "term" + if len(self.state['terms']) > 1: + plural = "terms" + self.midtext.set_text("You are registering for %d %s, and owe the " + "Computer Science Club $%d.00 in membership fees. " + "Please deposit the money in the safe before " + "continuing. " % ( len(regterms), plural, len(regterms * 2))) + +class EndPage(WizardPanel): + def init_widgets(self): + self.headtext = urwid.Text("") + self.midtext = urwid.Text("") + + self.widgets = [ + self.headtext, + urwid.Divider(), + self.midtext, + ] + def focusable(self): + return False + def activate(self): + try: + members.register( self.state['userid'], self.state['terms'] ) + self.headtext.set_text("Registration Succeeded") + self.midtext.set_text("The member has been registered for the following " + "terms: " + ", ".join(self.state['terms']) + ".") + except Exception, e: + self.headtext.set_text("Failed to Register") + self.midtext.set_text("You may refund any fees paid or retry." + "The error was: '%s'" % e) + def check(self): + pop_window() diff --git a/pylib/csc/apps/urwid/search.py b/pylib/csc/apps/urwid/search.py new file mode 100644 index 0000000..c6eb8f6 --- /dev/null +++ b/pylib/csc/apps/urwid/search.py @@ -0,0 +1,69 @@ +import urwid + +from csc.apps.urwid.widgets import * +from csc.apps.urwid.window import * + +from csc.adm import accounts, members, terms +from csc.common.excep import InvalidArgument + +class TermPage(WizardPanel): + def init_widgets(self): + self.term = SingleEdit("Term: ") + + self.widgets = [ + urwid.Text( "Terms Members" ), + urwid.Divider(), + self.term, + ] + def check(self): + if not members.connected(): members.connect() + try: + self.state['term'] = self.term.get_edit_text() + terms.parse( self.state['term'] ) + except: + self.focus_widget( self.term ) + set_status( "Invalid term" ) + return True + mlist = members.list_term( self.state['term'] ).values() + pop_window() + member_list( mlist ) + +class NamePage(WizardPanel): + def init_widgets(self): + self.name = SingleEdit("Name: ") + + self.widgets = [ + urwid.Text( "Members by Name" ), + urwid.Divider(), + self.name, + ] + def check(self): + if not members.connected(): members.connect() + self.state['name'] = self.name.get_edit_text() + if not self.state['name']: + self.focus_widget( self.term ) + set_status( "Invalid name" ) + return True + mlist = members.list_name( self.state['name'] ).values() + pop_window() + member_list( mlist ) + +def member_list(mlist): + mlist = list(mlist) + mlist.sort( lambda x, y: cmp(x['uid'], y['uid']) ) + buf = '' + for member in mlist: + if 'uid' in member: + uid = member['uid'][0] + else: + uid = None + if 'program' in member: + program = member['program'][0] + else: + program = None + attrs = ( uid, member['cn'][0], program ) + buf += "%10s %30s\n%41s\n\n" % attrs + set_status("Press escape to return to the menu") + push_window(urwid.ListBox([urwid.Text(buf)])) + + diff --git a/pylib/csc/apps/urwid/widgets.py b/pylib/csc/apps/urwid/widgets.py new file mode 100644 index 0000000..2216240 --- /dev/null +++ b/pylib/csc/apps/urwid/widgets.py @@ -0,0 +1,92 @@ +import urwid + +class ButtonText(urwid.Text): + def __init__(self, callback, *args, **kwargs): + self.callback = callback + urwid.Text.__init__(self, *args, **kwargs) + def selectable(self): + return True + def keypress(self, size, key): + if key == 'enter': + self.callback(self.get_text()) + else: + return key + +class SingleEdit(urwid.Edit): + def keypress(self, size, key): + if key == 'enter': + return urwid.Edit.keypress(self, size, 'down') + else: + return urwid.Edit.keypress(self, size, key) + +class SingleIntEdit(urwid.IntEdit): + def keypress(self, size, key): + if key == 'enter': + return urwid.Edit.keypress(self, size, 'down') + else: + return urwid.Edit.keypress(self, size, key) + +class WordEdit(SingleEdit): + def valid_char(self, ch): + return urwid.Edit.valid_char(self, ch) and ch != ' ' + +class PassEdit(SingleEdit): + def get_text(self): + text = urwid.Edit.get_text(self) + return (self.caption + " " * len(self.get_edit_text()), text[1]) + +class Wizard(urwid.WidgetWrap): + def __init__(self): + self.selected = None + self.panels = [] + + self.panelwrap = urwid.WidgetWrap( urwid.SolidFill() ) + self.back = urwid.Button("Back", self.back) + self.next = urwid.Button("Next", self.next) + self.buttons = urwid.Columns( [ self.back, self.next ], dividechars=3, focus_column=1 ) + pad = urwid.Padding( self.buttons, ('fixed right', 2), 19 ) + self.pile = urwid.Pile( [self.panelwrap, ('flow', pad)], 0 ) + urwid.WidgetWrap.__init__(self, self.pile) + + def add_panel(self, panel): + self.panels.append( panel ) + if len(self.panels) == 1: + self.select(0) + + def select(self, panelno, set_focus=True): + if 0 <= panelno < len(self.panels): + self.selected = panelno + self.panelwrap.set_w( self.panels[panelno] ) + self.panels[panelno].activate() + + if set_focus: + if self.panels[panelno].focusable(): + self.pile.set_focus( 0 ) + else: + self.pile.set_focus( 1 ) + + def next(self, *args, **kwargs): + if self.panels[self.selected].check(): + self.select( self.selected ) + return + self.select(self.selected + 1) + + def back(self, *args, **kwargs): + self.select(self.selected - 1, False) + +class WizardPanel(urwid.WidgetWrap): + def __init__(self, state): + self.state = state + self.init_widgets() + self.box = urwid.ListBox( urwid.SimpleListWalker( self.widgets ) ) + urwid.WidgetWrap.__init__( self, self.box ) + def init_widgets(self): + self.widgets = [] + def focus_widget(self, widget): + self.box.set_focus( self.widgets.index( widget ) ) + def focusable(self): + return True + def check(self): + return + def activate(self): + return diff --git a/pylib/csc/apps/urwid/window.py b/pylib/csc/apps/urwid/window.py new file mode 100644 index 0000000..370b6c1 --- /dev/null +++ b/pylib/csc/apps/urwid/window.py @@ -0,0 +1,65 @@ +import urwid + +window_stack = [] +window_names = [] + +header = urwid.Text( "" ) +footer = urwid.Text( "" ) +top = urwid.Frame( urwid.SolidFill(), header, footer ) + +def push_window( frame, name=None ): + window_stack.append( frame ) + window_names.append( name ) + update_top() + +def pop_window(): + if len(window_stack) == 1: + return False + window_stack.pop() + window_names.pop() + update_top() + return True + +def update_top(): + names = [ n for n in window_names if n ] + header.set_text(" - ".join( names ) + "\n") + top.set_body( window_stack[-1] ) + +def set_status(message): + footer.set_text(message) + +def clear_status(): + footer.set_text("") + +class Abort(Exception): + pass + +class Back(Exception): + pass + +def raise_abort(*args, **kwargs): + raise Abort() + +def raise_back(*args, **kwarg): + raise Back() + +def event_loop(ui): + while True: + try: + cols, rows = ui.get_cols_rows() + canvas = top.render( (cols, rows), focus=True ) + ui.draw_screen( (cols, rows), canvas ) + + keys = ui.get_input() + for k in keys: + if k == "esc": + if not pop_window(): + break + elif k == "window resize": + (cols, rows) = ui.get_cols_rows() + else: + top.keypress( (cols, rows), k ) + except Back: + pop_window() + except (Abort, KeyboardInterrupt): + return