Use the new SudoDialog

This commit is contained in:
Sean Davis 2014-08-24 11:46:34 -04:00
parent beab6c6611
commit 67513c2357
4 changed files with 215 additions and 126 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.16.1 --> <!-- Generated with glade 3.18.3 -->
<interface> <interface>
<requires lib="gtk+" version="3.6"/> <requires lib="gtk+" version="3.6"/>
<!-- interface-requires mugshot_window 1.0 --> <requires lib="mugshot_window" version="1.0"/>
<!-- interface-local-resource-path ../media --> <!-- interface-local-resource-path ../media -->
<object class="GtkBox" id="box8"> <object class="GtkBox" id="box8">
<property name="visible">True</property> <property name="visible">True</property>

View File

@ -192,7 +192,7 @@ class MugshotWindow(Window):
self.fax_entry = builder.get_object('fax') self.fax_entry = builder.get_object('fax')
# Users without sudo rights cannot change their name. # Users without sudo rights cannot change their name.
self.set_name_editable(SudoDialog.check_sudo()) self.set_name_editable(SudoDialog.check_dependencies(['chfn']))
# Stock photo browser # Stock photo browser
self.stock_browser = builder.get_object('stock_browser') self.stock_browser = builder.get_object('stock_browser')
@ -311,13 +311,24 @@ class MugshotWindow(Window):
changes.""" changes."""
logger.debug('Applying changes...') logger.debug('Applying changes...')
if self.get_chfn_details_updated(): if self.get_chfn_details_updated():
if not self.save_chfn_details(): success, response = self.save_chfn_details()
if not success:
# Password was incorrect, complain. # Password was incorrect, complain.
primary = _("Authentication Failed") if response in [Gtk.ResponseType.NONE,
Gtk.ResponseType.CANCEL,
Gtk.ResponseType.DELETE_EVENT]:
msg_type = Gtk.MessageType.WARNING
primary = _("Authentication cancelled.")
elif response == Gtk.ResponseType.REJECT:
msg_type = Gtk.MessageType.WARNING
primary = _("Authentication failed.")
else:
msg_type = Gtk.MessageType.ERROR
primary = _("An error occurred when saving changes.")
secondary = _("User details were not updated.") secondary = _("User details were not updated.")
dialog = Gtk.MessageDialog(transient_for=self, flags=0, dialog = Gtk.MessageDialog(transient_for=self, flags=0,
message_type= message_type=msg_type,
Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.OK, buttons=Gtk.ButtonsType.OK,
text=primary) text=primary)
dialog.format_secondary_text(secondary) dialog.format_secondary_text(secondary)
@ -555,8 +566,7 @@ class MugshotWindow(Window):
"""Handle password prompts from the interactive chfn commands.""" """Handle password prompts from the interactive chfn commands."""
# Force the C language for guaranteed english strings in the script. # Force the C language for guaranteed english strings in the script.
logger.debug('Executing: %s' % command) logger.debug('Executing: %s' % command)
child = pexpect.spawn(command, env={"LANG": "C"}) child = SudoDialog.env_spawn(command, 5)
child.timeout = 5
child.write_to_stdout = True child.write_to_stdout = True
try: try:
child.expect([".*ssword.*", pexpect.EOF]) child.expect([".*ssword.*", pexpect.EOF])
@ -582,13 +592,13 @@ class MugshotWindow(Window):
sudo_dialog.format_secondary_text(_("This is a security measure to " sudo_dialog.format_secondary_text(_("This is a security measure to "
"prevent unwanted updates\n" "prevent unwanted updates\n"
"to your personal information.")) "to your personal information."))
sudo_dialog.run() response = sudo_dialog.run()
sudo_dialog.hide() sudo_dialog.hide()
password = sudo_dialog.get_password() password = sudo_dialog.get_password()
sudo_dialog.destroy() sudo_dialog.destroy()
if not password: if not password:
return False return (False, response)
sudo = which('sudo') sudo = which('sudo')
chfn = which('chfn') chfn = which('chfn')
@ -606,7 +616,7 @@ class MugshotWindow(Window):
home_phone = 'none' home_phone = 'none'
# Full name can only be modified by root. Try using sudo to modify. # Full name can only be modified by root. Try using sudo to modify.
if SudoDialog.check_sudo(): if SudoDialog.check_dependencies(['chfn']):
logger.debug('Updating Full Name...') logger.debug('Updating Full Name...')
command = "%s %s -f \"%s\" %s" % (sudo, chfn, full_name, username) command = "%s %s -f \"%s\" %s" % (sudo, chfn, full_name, username)
if self.process_terminal_password(command, password): if self.process_terminal_password(command, password):
@ -629,7 +639,7 @@ class MugshotWindow(Window):
else: else:
success = False success = False
return success return (success, response)
# = LibreOffice ========================================================= # # = LibreOffice ========================================================= #
def get_libreoffice_details_updated(self): def get_libreoffice_details_updated(self):

View File

@ -5,7 +5,7 @@
# #
# This program is free software: you can redistribute it and/or modify it # This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by # under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or # the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, but # This program is distributed in the hope that it will be useful, but
@ -23,22 +23,70 @@ from locale import gettext as _
import pexpect import pexpect
gtk_version = (Gtk.get_major_version(),
Gtk.get_minor_version(),
Gtk.get_micro_version())
def check_sudo():
"""Return True if user has permission to use sudo.""" def check_gtk_version(major_version, minor_version, micro=0):
child = pexpect.spawn('sudo -v', env={"LANG": "C"}) """Return true if running gtk >= requested version"""
child.timeout = 1 return gtk_version >= (major_version, minor_version, micro)
# Check for failure message.
try: # Check if the LANG variable needs to be set
child.expect(["Sorry", pexpect.EOF]) use_env = False
child.close()
def check_dependencies(commands=[]):
"""Check for the existence of required commands, and sudo access"""
# Check for sudo
if pexpect.which("sudo") is None:
return False return False
except:
# Check for required commands
for command in commands:
if pexpect.which(command) is None:
return False
# Check for LANG requirements
child = env_spawn('sudo -v', 1)
if child.expect([".*ssword.*", "Sorry",
pexpect.EOF,
pexpect.TIMEOUT]) == 3:
global use_env
use_env = True
child.close()
# Check for sudo rights
child = env_spawn('sudo -v', 1)
try:
index = child.expect([".*ssword.*", "Sorry",
pexpect.EOF, pexpect.TIMEOUT])
child.close() child.close()
return True if index == 0 or index == 2:
# User in sudoers, or already admin
return True
elif index == 1 or index == 3:
# User not in sudoers
return False
except:
# Something else went wrong.
child.close()
return False
class SudoDialog(Gtk.MessageDialog): def env_spawn(command, timeout):
"""Use pexpect.spawn, adapt for timeout and env requirements."""
if use_env:
child = pexpect.spawn(command, env={"LANG": "C"})
else:
child = pexpect.spawn(command)
child.timeout = timeout
return child
class SudoDialog(Gtk.Dialog):
''' '''
Creates a new SudoDialog. This is a replacement for using gksudo which Creates a new SudoDialog. This is a replacement for using gksudo which
provides additional flexibility when performing sudo commands. provides additional flexibility when performing sudo commands.
@ -58,33 +106,93 @@ class SudoDialog(Gtk.MessageDialog):
- REJECT: Password invalid. - REJECT: Password invalid.
- ACCEPT: Password valid. - ACCEPT: Password valid.
''' '''
def __init__(self, parent=None, icon=None, message=None, name=None, def __init__(self, title=None, parent=None, icon=None, message=None,
retries=-1): name=None, retries=-1):
"""Initialize the SudoDialog.""" """Initialize the SudoDialog."""
# default dialog parameters
message_type = Gtk.MessageType.QUESTION
buttons = Gtk.ButtonsType.NONE
# initialize the dialog # initialize the dialog
super(SudoDialog, self).__init__(transient_for=parent, super(SudoDialog, self).__init__(title=title,
transient_for=parent,
modal=True, modal=True,
destroy_with_parent=True, destroy_with_parent=True)
message_type=message_type, #
buttons=buttons,
text='')
self.set_dialog_icon(icon)
self.connect("show", self.on_show) self.connect("show", self.on_show)
if title is None:
title = _("Password Required")
self.set_title(title)
# add buttons self.set_border_width(5)
button_box = self.get_children()[0].get_children()[1]
self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL) # Content Area
content_area = self.get_content_area()
grid = Gtk.Grid.new()
grid.set_row_spacing(6)
grid.set_column_spacing(12)
grid.set_margin_left(5)
grid.set_margin_right(5)
content_area.add(grid)
# Icon
self.dialog_icon = Gtk.Image.new_from_icon_name("dialog-password",
Gtk.IconSize.DIALOG)
grid.attach(self.dialog_icon, 0, 0, 1, 2)
# Text
self.primary_text = Gtk.Label.new("")
self.primary_text.set_use_markup(True)
self.primary_text.set_halign(Gtk.Align.START)
self.secondary_text = Gtk.Label.new("")
self.secondary_text.set_use_markup(True)
self.secondary_text.set_halign(Gtk.Align.START)
self.secondary_text.set_margin_top(6)
grid.attach(self.primary_text, 1, 0, 1, 1)
grid.attach(self.secondary_text, 1, 1, 1, 1)
# Infobar
self.infobar = Gtk.InfoBar.new()
self.infobar.set_margin_top(12)
self.infobar.set_message_type(Gtk.MessageType.WARNING)
content_area = self.infobar.get_content_area()
infobar_icon = Gtk.Image.new_from_icon_name("dialog-warning",
Gtk.IconSize.BUTTON)
label = Gtk.Label.new(_("Incorrect password... try again."))
content_area.add(infobar_icon)
content_area.add(label)
grid.attach(self.infobar, 0, 2, 2, 1)
content_area.show_all()
self.infobar.set_no_show_all(True)
# Password
label = Gtk.Label.new("")
label.set_use_markup(True)
label.set_markup("<b>%s</b>" % _("Password:"))
label.set_halign(Gtk.Align.START)
label.set_margin_top(12)
self.password_entry = Gtk.Entry()
self.password_entry.set_visibility(False)
self.password_entry.set_activates_default(True)
self.password_entry.set_margin_top(12)
grid.attach(label, 0, 3, 1, 1)
grid.attach(self.password_entry, 1, 3, 1, 1)
# Buttons
button = self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
button_box = button.get_parent()
button_box.set_margin_top(24)
ok_button = Gtk.Button.new_with_label(_("OK")) ok_button = Gtk.Button.new_with_label(_("OK"))
ok_button.connect("clicked", self.on_ok_clicked) ok_button.connect("clicked", self.on_ok_clicked)
ok_button.set_receives_default(True) ok_button.set_receives_default(True)
ok_button.set_can_default(True) ok_button.set_can_default(True)
ok_button.set_sensitive(False) ok_button.set_sensitive(False)
self.set_default(ok_button) self.set_default(ok_button)
button_box.pack_start(ok_button, False, False, 0) if check_gtk_version(3, 12):
button_box.pack_start(ok_button, True, True, 0)
else:
button_box.pack_start(ok_button, False, False, 0)
self.password_entry.connect("changed", self.on_password_changed,
ok_button)
self.set_dialog_icon(icon)
# add primary and secondary text # add primary and secondary text
if message: if message:
@ -98,47 +206,11 @@ class SudoDialog(Gtk.MessageDialog):
self.format_primary_text(primary_text) self.format_primary_text(primary_text)
self.format_secondary_text(secondary_text) self.format_secondary_text(secondary_text)
# Pack the content area with password-related widgets.
content_area = self.get_content_area()
# Use an alignment to move align the password widgets with the text.
self.password_alignment = Gtk.Alignment()
# Make an educated guess about how for to align.
left_align = Gtk.icon_size_lookup(Gtk.IconSize.DIALOG)[1] + 16
self.password_alignment.set_padding(12, 12, left_align, 0)
# Outer password box for incorrect password label and inner widgets.
password_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
spacing=12)
password_outer.set_orientation(Gtk.Orientation.VERTICAL)
# Password error label, only displayed when unsuccessful.
self.password_info = Gtk.Label(label="")
self.password_info.set_markup("<b>%s</b>" %
_("Incorrect password... try again."))
# Inner password box for Password: label and password entry.
password_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
spacing=12)
password_label = Gtk.Label(label=_("Password:"))
self.password_entry = Gtk.Entry()
self.password_entry.set_visibility(False)
self.password_entry.set_activates_default(True)
self.password_entry.connect("changed", self.on_password_changed,
ok_button)
# Pack all the widgets.
password_box.pack_start(password_label, False, False, 0)
password_box.pack_start(self.password_entry, True, True, 0)
password_outer.pack_start(self.password_info, True, True, 0)
password_outer.pack_start(password_box, True, True, 0)
self.password_alignment.add(password_outer)
content_area.pack_start(self.password_alignment, True, True, 0)
content_area.show_all()
self.password_info.set_visible(False)
self.attempted_logins = 0 self.attempted_logins = 0
self.max_attempted_logins = retries self.max_attempted_logins = retries
self.show_all()
def on_password_changed(self, widget, button): def on_password_changed(self, widget, button):
"""Set the apply button sensitivity based on password input.""" """Set the apply button sensitivity based on password input."""
button.set_sensitive(len(widget.get_text()) > 0) button.set_sensitive(len(widget.get_text()) > 0)
@ -146,11 +218,14 @@ class SudoDialog(Gtk.MessageDialog):
def format_primary_text(self, message_format): def format_primary_text(self, message_format):
''' '''
Format the primary text widget. Format the primary text widget.
API extension to match with format_secondary_text.
''' '''
label = self.get_message_area().get_children()[0] self.primary_text.set_markup("<big><b>%s</b></big>" % message_format)
label.set_text(message_format)
def format_secondary_text(self, message_format):
'''
Format the secondary text widget.
'''
self.secondary_text.set_markup(message_format)
def set_dialog_icon(self, icon=None): def set_dialog_icon(self, icon=None):
''' '''
@ -166,24 +241,21 @@ class SudoDialog(Gtk.MessageDialog):
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon, pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon,
icon_size, icon_size,
icon_size) icon_size)
image = Gtk.Image.new_from_pixbuf(pixbuf) self.dialog_icon.set_from_pixbuf(pixbuf)
self.set_icon_from_file(icon) self.set_icon_from_file(icon)
else: else:
# icon is an named icon, so load it directly to an image # icon is an named icon, so load it directly to an image
image = Gtk.Image.new_from_icon_name(icon, icon_size) self.dialog_icon.set_from_icon_name(icon, icon_size)
self.set_icon_name(icon) self.set_icon_name(icon)
else: else:
# fallback on password icon # fallback on password icon
image = Gtk.Image.new_from_icon_name('dialog-password', icon_size) self.dialog_icon.set_from_icon_name('dialog-password', icon_size)
self.set_icon_name('dialog-password') self.set_icon_name('dialog-password')
# align, show, and set the image.
image.set_alignment(Gtk.Align.CENTER, Gtk.Align.FILL)
image.show()
self.set_image(image)
def on_show(self, widget): def on_show(self, widget):
'''When the dialog is displayed, clear the password.''' '''When the dialog is displayed, clear the password.'''
self.set_password('') self.set_password('')
self.password_valid = False
def on_ok_clicked(self, widget): def on_ok_clicked(self, widget):
''' '''
@ -193,26 +265,22 @@ class SudoDialog(Gtk.MessageDialog):
If unsuccessful, try again until reaching maximum attempted logins, If unsuccessful, try again until reaching maximum attempted logins,
then emit the response signal with REJECT. then emit the response signal with REJECT.
''' '''
top, bottom, left, right = self.password_alignment.get_padding() if self.attempt_login():
# Password cannot be validated without sudo
if (not check_sudo()) or self.attempt_login():
self.password_valid = True self.password_valid = True
# Adjust the dialog for attactiveness.
self.password_alignment.set_padding(12, bottom, left, right)
self.password_info.hide()
self.emit("response", Gtk.ResponseType.ACCEPT) self.emit("response", Gtk.ResponseType.ACCEPT)
else: else:
self.password_valid = False self.password_valid = False
# Adjust the dialog for attactiveness. # Adjust the dialog for attactiveness.
self.password_alignment.set_padding(0, bottom, left, right) self.infobar.show()
self.password_info.show() self.password_entry.grab_focus()
self.set_password('')
if self.attempted_logins == self.max_attempted_logins: if self.attempted_logins == self.max_attempted_logins:
self.attempted_logins = 0 self.attempted_logins = 0
self.emit("response", Gtk.ResponseType.REJECT) self.emit("response", Gtk.ResponseType.REJECT)
def get_password(self): def get_password(self):
'''Return the currently entered password, or None if blank.''' '''Return the currently entered password, or None if blank.'''
if not self.password_valid:
return None
password = self.password_entry.get_text() password = self.password_entry.get_text()
if password == '': if password == '':
return None return None
@ -232,12 +300,11 @@ class SudoDialog(Gtk.MessageDialog):
Return True if successful. Return True if successful.
''' '''
# Set the pexpect variables and spawn the process. # Set the pexpect variables and spawn the process.
child = pexpect.spawn('sudo /bin/true', env={"LANG": "C"}) child = env_spawn('sudo /bin/true', 1)
child.timeout = 1
try: try:
# Check for password prompt or program exit. # Check for password prompt or program exit.
child.expect([".*ssword.*", pexpect.EOF]) child.expect([".*ssword.*", pexpect.EOF])
child.sendline(self.get_password()) child.sendline(self.password_entry.get_text())
child.expect(pexpect.EOF) child.expect(pexpect.EOF)
except pexpect.TIMEOUT: except pexpect.TIMEOUT:
# If we timeout, that means the password was unsuccessful. # If we timeout, that means the password was unsuccessful.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-08-24 10:25-0400\n" "POT-Creation-Date: 2014-08-24 11:44-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -61,7 +61,7 @@ msgstr ""
msgid "Browse…" msgid "Browse…"
msgstr "" msgstr ""
#: ../data/ui/MugshotWindow.ui.h:9 ../mugshot/MugshotWindow.py:579 #: ../data/ui/MugshotWindow.ui.h:9 ../mugshot/MugshotWindow.py:589
msgid "Mugshot" msgid "Mugshot"
msgstr "" msgstr ""
@ -130,70 +130,82 @@ msgstr ""
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
#. Password was incorrect, complain. #: ../mugshot/MugshotWindow.py:321
#: ../mugshot/MugshotWindow.py:316 msgid "Authentication cancelled."
msgid "Authentication Failed"
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:317 #: ../mugshot/MugshotWindow.py:324
msgid "Authentication failed."
msgstr ""
#: ../mugshot/MugshotWindow.py:327
msgid "An error occurred when saving changes."
msgstr ""
#: ../mugshot/MugshotWindow.py:329
msgid "User details were not updated." msgid "User details were not updated."
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:488 #: ../mugshot/MugshotWindow.py:499
msgid "Update Pidgin buddy icon?" msgid "Update Pidgin buddy icon?"
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:489 #: ../mugshot/MugshotWindow.py:500
msgid "Would you also like to update your Pidgin buddy icon?" msgid "Would you also like to update your Pidgin buddy icon?"
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:580 #: ../mugshot/MugshotWindow.py:590
msgid "Enter your password to change user details." msgid "Enter your password to change user details."
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:582 #: ../mugshot/MugshotWindow.py:592
msgid "" msgid ""
"This is a security measure to prevent unwanted updates\n" "This is a security measure to prevent unwanted updates\n"
"to your personal information." "to your personal information."
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:787 #: ../mugshot/MugshotWindow.py:797
msgid "Update LibreOffice user details?" msgid "Update LibreOffice user details?"
msgstr "" msgstr ""
#: ../mugshot/MugshotWindow.py:788 #: ../mugshot/MugshotWindow.py:798
msgid "Would you also like to update your user details in LibreOffice?" msgid "Would you also like to update your user details in LibreOffice?"
msgstr "" msgstr ""
#: ../mugshot_lib/SudoDialog.py:80 #: ../mugshot_lib/SudoDialog.py:120
msgid "Password Required"
msgstr ""
#: ../mugshot_lib/SudoDialog.py:157
msgid "Incorrect password... try again."
msgstr ""
#: ../mugshot_lib/SudoDialog.py:167
msgid "Password:"
msgstr ""
#. Buttons
#: ../mugshot_lib/SudoDialog.py:178
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: ../mugshot_lib/SudoDialog.py:81 #: ../mugshot_lib/SudoDialog.py:181
msgid "OK" msgid "OK"
msgstr "" msgstr ""
#: ../mugshot_lib/SudoDialog.py:94 #: ../mugshot_lib/SudoDialog.py:202
msgid "" msgid ""
"Enter your password to\n" "Enter your password to\n"
"perform administrative tasks." "perform administrative tasks."
msgstr "" msgstr ""
#: ../mugshot_lib/SudoDialog.py:96 #: ../mugshot_lib/SudoDialog.py:204
#, python-format #, python-format
msgid "" msgid ""
"The application '%s' lets you\n" "The application '%s' lets you\n"
"modify essential parts of your system." "modify essential parts of your system."
msgstr "" msgstr ""
#: ../mugshot_lib/SudoDialog.py:117
msgid "Incorrect password... try again."
msgstr ""
#: ../mugshot_lib/SudoDialog.py:122
msgid "Password:"
msgstr ""
#: ../data/appdata/mugshot.appdata.xml.in.h:1 #: ../data/appdata/mugshot.appdata.xml.in.h:1
msgid "Lightweight user configuration" msgid "Lightweight user configuration"
msgstr "" msgstr ""