From 9470a429984a3533006f9f3e782c1d3f51db6ab5 Mon Sep 17 00:00:00 2001 From: Michael Spang Date: Sun, 16 Dec 2007 01:16:21 -0500 Subject: [PATCH] Remove chfn and chsh and allow shell changes in the gui The chsh and chfn programs were broken anyway. --- bin/csc-chfn | 140 ------------------------------------------- bin/csc-chsh | 105 -------------------------------- ceo/members.py | 28 ++++++++- ceo/urwid/info.py | 2 + ceo/urwid/main.py | 14 +++-- ceo/urwid/shell.py | 94 +++++++++++++++++++++++++++++ ceo/urwid/widgets.py | 3 + 7 files changed, 136 insertions(+), 250 deletions(-) delete mode 100755 bin/csc-chfn delete mode 100755 bin/csc-chsh create mode 100644 ceo/urwid/shell.py diff --git a/bin/csc-chfn b/bin/csc-chfn deleted file mode 100755 index 1f2e2d6..0000000 --- a/bin/csc-chfn +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python -""" -chfn - change real user name and information - -This utility imitates chfn(1) from the shadow password suite, but makes its -changes in the LDAP directory rather than in the passwd file. - -When run from an unprivileged account, authentication will be performed -before the account information is changed. -""" -import os, sys, pwd, getopt, PAM -from ceo import accounts -from ceo.excep import InvalidArgument - -progname = os.path.basename(sys.argv[0]) - -OPTION_MAP = { - '-f': 'fullname', - '-r': 'roomnumber', - '-w': 'workphone', - '-h': 'homephone', - '-o': 'other' -} -LONG_NAMES = [ - ('fullname', 'Full Name'), - ('roomnumber', 'Room Number'), - ('workphone', 'Work Phone'), - ('homephone', 'Home Phone'), - ('other', 'Other') -] -READONLY_FIELDS = [ 'fullname', 'other' ] - -def usage(): - umesg = "Usage: %s [-f full name] [-r room no] [-w work ph] " + \ - "[-h home ph] [-o other] [user]" - print umesg % progname - sys.exit(2) - - -def whoami(): - uid = os.getuid() - username = os.getlogin() - if pwd.getpwnam(username).pw_uid != uid: - username = pwd.getpwuid(uid).pw_name - return (uid, username) - -def authenticate(username): - auth = PAM.pam() - auth.start('chsh', username) - try: - auth.authenticate() - auth.acct_mgmt() - except PAM.error, resp: - print "%s: %s" % (progname, resp.args[0]) - sys.exit(1) - -def main(): - - pwuid, pwnam = whoami() - - euid = os.geteuid() - os.setreuid(euid, euid) - - gecos_params = {} - - try: - options, arguments = getopt.gnu_getopt(sys.argv[1:], 'f:r:w:h:o:') - for opt, val in options: - gecos_params[OPTION_MAP[opt]] = val - if len(arguments) > 1: - usage() - elif len(arguments) == 1: - username = arguments[0] - else: - username = pwnam - except getopt.GetoptError, e: - usage() - - for field in READONLY_FIELDS: - if field in gecos_params and pwuid: - print "%s: Permission denied." % progname - sys.exit(1) - - try: - if pwuid and pwd.getpwnam(username).pw_uid != pwuid: - print "%s: Permission denied." % progname - sys.exit(1) - except KeyError: - print "%s: unknown user %s" % (progname, username) - sys.exit(1) - - try: - accounts.connect() - gecos_raw = accounts.get_gecos(username) - gecos = accounts.parse_gecos(gecos_raw) - - if pwuid: - authenticate(username) - - if not gecos_params: - print "Changing the user information for %s" % username - print "Enter the new value, or press ENTER for the default" - for field, longname in LONG_NAMES: - if pwuid and field == 'other' and 'other' in READONLY_FIELDS: - continue - if gecos[field] is None: - gecos[field] = "" - if field in READONLY_FIELDS and pwuid: - print " %s: %s" % (longname, gecos[field]) - else: - print " %s: [%s]:" % (longname, gecos[field]), - new_value = raw_input() - if new_value: - gecos[field] = new_value.strip() - else: - gecos.update(gecos_params) - - gecos_raw_new = accounts.build_gecos(**gecos) - if gecos_raw != gecos_raw_new: - accounts.update_gecos(username, gecos_raw_new) - - except InvalidArgument, e: - longnames = dict(LONG_NAMES) - longname = longnames.get(e.argname, e.argname).lower() - print "%s: invalid %s: %s" % (progname, longname, e.argval) - sys.exit(1) - -if __name__ == '__main__': - exceps = ( accounts.ConfigurationException, accounts.LDAPException, - accounts.KrbException, accounts.AccountException ) - try: - main() - except KeyboardInterrupt: - sys.exit(130) - except IOError, e: - print "%s: %s: %s" % (progname, e.filename, e.strerror) - sys.exit(1) - except exceps, e: - print "%s: %s" % (progname, e) - sys.exit(1) diff --git a/bin/csc-chsh b/bin/csc-chsh deleted file mode 100755 index 4ec7790..0000000 --- a/bin/csc-chsh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/python -""" -chsh - change login shell - -This utility imitates chsh(1) from the shadow password suite, but makes its -changes in the LDAP directory rather than in the passwd file. - -When run from an unprivileged account, authentication will be performed -before the shell is changed, and the new shell must be listed in /etc/shells. -""" -import os, sys, pwd, getopt, PAM -from ceo import accounts -from ceo.excep import InvalidArgument - -progname = os.path.basename(sys.argv[0]) - -def usage(): - print "Usage: %s [-s shell] [username]" % progname - sys.exit(2) - -def whoami(): - uid = os.getuid() - username = os.getlogin() - if pwd.getpwnam(username).pw_uid != uid: - username = pwd.getpwuid(uid).pw_name - return (uid, username) - -def authenticate(username): - auth = PAM.pam() - auth.start('chsh', username) - try: - auth.authenticate() - auth.acct_mgmt() - except PAM.error, resp: - print "%s: %s" % (progname, resp.args[0]) - sys.exit(1) - -def main(): - - pwuid, pwnam = whoami() - - euid = os.geteuid() - os.setreuid(euid, euid) - - try: - options, arguments = getopt.gnu_getopt(sys.argv[1:], 's:') - new_shell = None - for opt, val in options: - if opt == '-s': - new_shell = val - if len(arguments) > 1: - usage() - elif len(arguments) == 1: - username = arguments[0] - else: - username = pwnam - except getopt.GetoptError, e: - usage() - - try: - if pwuid and pwd.getpwnam(username).pw_uid != pwuid: - print "%s: You may not change the shell for %s." % (progname, username) - sys.exit(1) - except KeyError: - print "%s: unknown user %s" % (progname, username) - sys.exit(1) - - try: - accounts.connect() - current_shell = accounts.get_shell(username) - - if pwuid: - authenticate(username) - - if not new_shell: - print "Changing the login shell for %s" % username - print "Enter the new value, or press ENTER for the default" - print " Login Shell [%s]:" % current_shell, - new_shell = raw_input() - if not new_shell: - new_shell = current_shell - - if new_shell != current_shell: - accounts.update_shell(username, new_shell, pwuid != 0) - - except InvalidArgument, e: - if e.argname == 'shell': - print "%s: %s: invalid shell" % (progname, new_shell) - sys.exit(1) - else: - raise - -if __name__ == '__main__': - exceps = ( accounts.ConfigurationException, accounts.LDAPException, - accounts.KrbException, accounts.AccountException ) - try: - main() - except KeyboardInterrupt: - sys.exit(130) - except IOError, e: - print "%s: %s: %s" % (progname, e.filename, e.strerror) - sys.exit(1) - except exceps, e: - print "%s: %s" % (progname, e) - sys.exit(1) diff --git a/ceo/members.py b/ceo/members.py index aaefff0..13a0a58 100644 --- a/ceo/members.py +++ b/ceo/members.py @@ -9,7 +9,7 @@ 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, subprocess, ldap +import os, re, subprocess, ldap from ceo import conf, ldapi from ceo.excep import InvalidArgument @@ -307,6 +307,32 @@ def change_group_member(action, group, userid): +### Shells ### + +def get_shell(userid): + member = ldapi.lookup(ld, 'uid', userid, cfg['users_base']) + if not member: + raise NoSuchMember(userid) + if 'loginShell' not in member: + return + return member['loginShell'][0] + + +def get_shells(): + return [ sh for sh in open(cfg['shells_file']).read().split("\n") + if sh + and sh[0] == '/' + and not '#' in sh + and os.access(sh, os.X_OK) ] + + +def set_shell(userid, shell): + if not shell in get_shells(): + raise InvalidArgument("shell", shell, "is not in %s" % cfg['shells_file']) + ldapi.modify(ld, 'uid', userid, cfg['users_base'], [ (ldap.MOD_REPLACE, 'loginShell', [ shell ]) ]) + + + ### Clubs ### def create_club(username, name): diff --git a/ceo/urwid/info.py b/ceo/urwid/info.py index b27a11a..b9df1fd 100644 --- a/ceo/urwid/info.py +++ b/ceo/urwid/info.py @@ -25,11 +25,13 @@ class InfoPage(WizardPanel): name = member.get('cn', [''])[0] userid = self.state['userid'] program = member.get('program', [''])[0] + shell = member.get('loginShell', [''])[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.program.set_text("Shell: %s" % shell) self.terms.set_text("Terms: %s" % ", ".join(terms)) def check(self): pop_window() diff --git a/ceo/urwid/main.py b/ceo/urwid/main.py index a3b61e6..d594444 100644 --- a/ceo/urwid/main.py +++ b/ceo/urwid/main.py @@ -2,7 +2,7 @@ import sys, random, ldap, urwid.curses_display from ceo import members, ldapi from ceo.urwid.widgets import * from ceo.urwid.window import * -from ceo.urwid import newmember, renew, info, search, positions, groups +from ceo.urwid import newmember, renew, info, search, positions, groups, shell ui = urwid.curses_display.Screen() @@ -52,15 +52,13 @@ syscom_data = { "groups" : [ "office", "staff", "adm", "src" ], } -def menu_items(items): - return [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ] - def main_menu(): menu = [ ("New Member", new_member, None), ("Renew Membership", renew_member, None), ("Create Club Account", new_club, None), ("Display Member", display_member, None), + ("Change Shell", change_shell, None), ("Search", search_members, None), ("Manage Club or Group Members", manage_group, None), ("Manage Positions", manage_positions, None), @@ -136,6 +134,14 @@ def manage_positions(data): positions.EndPage, ], (50, 15)) +def change_shell(data): + push_wizard("Change Shell", [ + shell.IntroPage, + shell.YouPage, + shell.ShellPage, + shell.EndPage + ], (50, 15)) + def run(): push_window( main_menu(), program_name() ) event_loop( ui ) diff --git a/ceo/urwid/shell.py b/ceo/urwid/shell.py new file mode 100644 index 0000000..c4e7b16 --- /dev/null +++ b/ceo/urwid/shell.py @@ -0,0 +1,94 @@ +import urwid, ldap, pwd, os +from ceo import members, terms, ldapi +from ceo.urwid.widgets import * +from ceo.urwid.window import * + +class IntroPage(WizardPanel): + def init_widgets(self): + self.widgets = [ + urwid.Text( "Changing Login Shell" ), + urwid.Divider(), + urwid.Text( "You can change your shell here. Request more shells " + "by emailing systems-committee." ) + ] + def focusable(self): + return False + +class YouPage(WizardPanel): + def init_widgets(self): + you = pwd.getpwuid(os.getuid()).pw_name + self.userid = WordEdit("Username: ", you) + + 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']: + 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 ShellPage(WizardPanel): + def init_widgets(self): + self.midtext = urwid.Text("") + + self.widgets = [ + urwid.Text("Choose a Shell"), + urwid.Divider(), + ] + + def set_shell(radio_button, new_state, shell): + if new_state: + self.state['shell'] = shell + + radio_group = [] + self.shells = members.get_shells() + self.shellw = [ urwid.RadioButton(radio_group, shell, + on_state_change=set_shell, user_data=shell) + for shell in self.shells ] + + self.widgets.extend(self.shellw) + def set_shell(self, shell): + i = self.shells.index(shell) + self.shellw[i].set_state(True) + def focusable(self): + return True + def activate(self): + self.set_shell(self.state['member']['loginShell'][0]) + +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): + problem = None + try: + user, shell = self.state['userid'], self.state['shell'] + members.set_shell(user, shell) + self.headtext.set_text("Login Shell Changed") + self.midtext.set_text("The shell for %s has been changed to %s." + % (user, shell)) + except ldap.LDAPError, e: + problem = ldapi.format_ldaperror(e) + except members.MemberException, e: + problem = str(e) + if problem: + self.headtext.set_text("Failed to Change Shell") + self.midtext.set_text("Perhaps you don't have permission to change %s's shell? " + "The error was:\n\n%s" % (user, problem)) + def check(self): + pop_window() diff --git a/ceo/urwid/widgets.py b/ceo/urwid/widgets.py index ca52416..4b1bd92 100644 --- a/ceo/urwid/widgets.py +++ b/ceo/urwid/widgets.py @@ -2,6 +2,9 @@ import urwid from ceo.urwid.ldapfilter import * from ceo.urwid.window import raise_back, push_window +def menu_items(items): + return [ urwid.AttrWrap( ButtonText( cb, data, txt ), 'menu', 'selected') for (txt, cb, data) in items ] + def push_wizard(name, pages, dimensions=(50, 10)): state = {} wiz = Wizard()