Remove chfn and chsh and allow shell changes in the gui
authorMichael Spang <mspang@csclub.uwaterloo.ca>
Sun, 16 Dec 2007 06:16:21 +0000 (01:16 -0500)
committerMichael Spang <mspang@csclub.uwaterloo.ca>
Sun, 16 Dec 2007 06:16:21 +0000 (01:16 -0500)
The chsh and chfn programs were broken anyway.

bin/csc-chfn [deleted file]
bin/csc-chsh [deleted file]
ceo/members.py
ceo/urwid/info.py
ceo/urwid/main.py
ceo/urwid/shell.py [new file with mode: 0644]
ceo/urwid/widgets.py

diff --git a/bin/csc-chfn b/bin/csc-chfn
deleted file mode 100755 (executable)
index 1f2e2d6..0000000
+++ /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 (executable)
index 4ec7790..0000000
+++ /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)
index aaefff0..13a0a58 100644 (file)
@@ -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):
index b27a11a..b9df1fd 100644 (file)
@@ -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()
index a3b61e6..d594444 100644 (file)
@@ -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 (file)
index 0000000..c4e7b16
--- /dev/null
@@ -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()
index ca52416..4b1bd92 100644 (file)
@@ -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()