285 lines
10 KiB
Python
285 lines
10 KiB
Python
# root password spoke class
|
|
#
|
|
# Copyright (C) 2012 Red Hat, Inc.
|
|
#
|
|
# This copyrighted material is made available to anyone wishing to use,
|
|
# modify, copy, or redistribute it subject to the terms and conditions of
|
|
# the GNU General Public License v.2, or (at your option) any later version.
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY expressed or implied, including the implied warranties of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
# Public License for more details. You should have received a copy of the
|
|
# GNU General Public License along with this program; if not, write to the
|
|
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the
|
|
# source code or documentation are not subject to the GNU General Public
|
|
# License and may only be used or replicated with the express permission of
|
|
# Red Hat, Inc.
|
|
#
|
|
# Red Hat Author(s): Jesse Keating <jkeating@redhat.com>
|
|
#
|
|
|
|
from pyanaconda.i18n import _, N_
|
|
from pyanaconda.users import cryptPassword, validatePassword
|
|
|
|
from pyanaconda.ui.gui import GUICheck
|
|
from pyanaconda.ui.gui.spokes import NormalSpoke
|
|
from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory
|
|
from pyanaconda.ui.common import FirstbootSpokeMixIn
|
|
|
|
from pyanaconda.constants import PASSWORD_EMPTY_ERROR, PASSWORD_CONFIRM_ERROR_GUI,\
|
|
PASSWORD_STRENGTH_DESC, PASSWORD_WEAK, PASSWORD_WEAK_WITH_ERROR,\
|
|
PASSWORD_WEAK_CONFIRM, PASSWORD_WEAK_CONFIRM_WITH_ERROR
|
|
|
|
__all__ = ["PasswordSpoke"]
|
|
|
|
|
|
class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
|
|
builderObjects = ["passwordWindow"]
|
|
|
|
mainWidgetName = "passwordWindow"
|
|
uiFile = "spokes/password.glade"
|
|
|
|
category = UserSettingsCategory
|
|
|
|
icon = "dialog-password-symbolic"
|
|
title = N_("_ROOT PASSWORD")
|
|
|
|
def __init__(self, *args):
|
|
NormalSpoke.__init__(self, *args)
|
|
self._lock = self.data.rootpw.lock
|
|
self._kickstarted = False
|
|
|
|
def initialize(self):
|
|
NormalSpoke.initialize(self)
|
|
# place holders for the text boxes
|
|
self.pw = self.builder.get_object("pw")
|
|
self.confirm = self.builder.get_object("confirm")
|
|
self.lock = self.builder.get_object("lock")
|
|
|
|
# Install the password checks:
|
|
# - Has a password been specified?
|
|
# - If a password has been specified and there is data in the confirm box, do they match?
|
|
# - How strong is the password?
|
|
# - Is there any data in the confirm box?
|
|
self.add_check(self.pw, self._checkPasswordEmpty)
|
|
|
|
# The password confirmation needs to be checked whenever either of the password
|
|
# fields change. Separate checks are created for each field so that edits on either
|
|
# will trigger a new check and so that the last edited field will get focus when
|
|
# Done is clicked. The checks are saved here so that either check can trigger the
|
|
# other check in order to reset the status on both when either field is changed.
|
|
# The check_data field is used as a flag to prevent infinite recursion.
|
|
self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm)
|
|
self._password_check = self.add_check(self.pw, self._checkPasswordConfirm)
|
|
|
|
# Keep a reference for this check, since it has to be manually run for the
|
|
# click Done twice check.
|
|
self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength)
|
|
|
|
self.add_check(self.confirm, self._checkPasswordEmpty)
|
|
|
|
# Counter for the click Done twice check override
|
|
self._waivePasswordClicks = 0
|
|
|
|
# Password validation data
|
|
self._pwq_error = None
|
|
self._pwq_valid = True
|
|
|
|
self._kickstarted = self.data.rootpw.seen
|
|
if self._kickstarted:
|
|
self.pw.set_placeholder_text(_("The password is set."))
|
|
self.confirm.set_placeholder_text(_("The password is set."))
|
|
|
|
self.pw_bar = self.builder.get_object("password_bar")
|
|
self.pw_label = self.builder.get_object("password_label")
|
|
|
|
# Configure levels for the password bar
|
|
self.pw_bar.add_offset_value("low", 2)
|
|
self.pw_bar.add_offset_value("medium", 3)
|
|
self.pw_bar.add_offset_value("high", 4)
|
|
|
|
def refresh(self):
|
|
# Enable the input checks in case they were disabled on the last exit
|
|
for check in self.checks:
|
|
check.enable()
|
|
self.lock.set_active(self._lock)
|
|
self.on_lock_clicked(self.lock)
|
|
self.pw.emit("changed")
|
|
self.confirm.emit("changed")
|
|
|
|
def on_lock_clicked(self, lock):
|
|
self.pw.set_sensitive(not lock.get_active())
|
|
self.confirm.set_sensitive(not lock.get_active())
|
|
if not lock.get_active():
|
|
self.pw.grab_focus()
|
|
|
|
# Caps lock detection isn't hooked up right now
|
|
# def setCapsLockLabel(self):
|
|
# if isCapsLockEnabled():
|
|
# self.capslock.set_text("<b>" + _("Caps Lock is on.") + "</b>")
|
|
# self.capslock.set_use_markup(True)
|
|
# else:
|
|
# self.capslock..set_text("")
|
|
|
|
@property
|
|
def status(self):
|
|
if self.data.rootpw.lock:
|
|
return _("Root account is disabled")
|
|
elif self.data.rootpw.password:
|
|
return _("Root password is set")
|
|
else:
|
|
return _("Root password is not set")
|
|
|
|
@property
|
|
def mandatory(self):
|
|
return not any(user for user in self.data.user.userList
|
|
if "wheel" in user.groups)
|
|
|
|
def apply(self):
|
|
pw = self.pw.get_text()
|
|
if pw:
|
|
self.data.rootpw.password = cryptPassword(pw)
|
|
self.data.rootpw.isCrypted = True
|
|
self.data.rootpw.lock = self._lock
|
|
|
|
# value from the kickstart changed
|
|
self.data.rootpw.seen = False
|
|
self._kickstarted = False
|
|
|
|
self.pw.set_placeholder_text("")
|
|
self.confirm.set_placeholder_text("")
|
|
|
|
@property
|
|
def completed(self):
|
|
return bool(self.data.rootpw.password or self.data.rootpw.lock)
|
|
|
|
def _checkPasswordEmpty(self, editable, data):
|
|
"""Check whether a password has been specified at all."""
|
|
|
|
# If the password was set by kickstart, skip this check
|
|
if self._kickstarted:
|
|
return GUICheck.CHECK_OK
|
|
|
|
if not editable.get_text():
|
|
if editable == self.pw:
|
|
return _(PASSWORD_EMPTY_ERROR)
|
|
else:
|
|
return _(PASSWORD_CONFIRM_ERROR_GUI)
|
|
else:
|
|
return GUICheck.CHECK_OK
|
|
|
|
def _checkPasswordConfirm(self, editable=None, reset_status=None):
|
|
"""Check whether the password matches the confirmation data."""
|
|
|
|
# This check is triggered by changes to either the password field or the
|
|
# confirmation field. If this method is being run from a successful check
|
|
# to reset the status, just return success
|
|
if reset_status:
|
|
return GUICheck.CHECK_OK
|
|
|
|
pw = self.pw.get_text()
|
|
confirm = self.confirm.get_text()
|
|
lock = self.lock.get_active()
|
|
|
|
if lock:
|
|
self._lock = True
|
|
self._password = None
|
|
self.clear_info()
|
|
self._error = False
|
|
result = GUICheck.CHECK_OK
|
|
|
|
# Skip the check if no password is required
|
|
elif (not pw and not confirm) and self._kickstarted:
|
|
result = GUICheck.CHECK_OK
|
|
elif confirm and (pw != confirm):
|
|
result = _(PASSWORD_CONFIRM_ERROR_GUI)
|
|
else:
|
|
result = GUICheck.CHECK_OK
|
|
|
|
# If the check succeeded, reset the status of the other check object
|
|
if result == GUICheck.CHECK_OK:
|
|
if editable == self.confirm:
|
|
self._password_check.update_check_status(check_data=True)
|
|
else:
|
|
self._confirm_check.update_check_status(check_data=True)
|
|
|
|
return result
|
|
|
|
def _updatePwQuality(self, editable=None, data=None):
|
|
"""Update the password quality information.
|
|
|
|
This function is called by the ::changed signal handler on the
|
|
password field.
|
|
"""
|
|
|
|
pwtext = self.pw.get_text()
|
|
|
|
# Reset the counter used for the "press Done twice" logic
|
|
self._waivePasswordClicks = 0
|
|
|
|
self._pwq_valid, strength, self._pwq_error = validatePassword(pwtext, "root")
|
|
|
|
if not pwtext:
|
|
val = 0
|
|
elif strength < 50:
|
|
val = 1
|
|
elif strength < 75:
|
|
val = 2
|
|
elif strength < 90:
|
|
val = 3
|
|
else:
|
|
val = 4
|
|
text = _(PASSWORD_STRENGTH_DESC[val])
|
|
|
|
self.pw_bar.set_value(val)
|
|
self.pw_label.set_text(text)
|
|
|
|
def _checkPasswordStrength(self, editable=None, data=None):
|
|
"""Update the error message based on password strength.
|
|
|
|
Convert the strength set by _updatePwQuality into an error message.
|
|
"""
|
|
|
|
pw = self.pw.get_text()
|
|
confirm = self.confirm.get_text()
|
|
|
|
# Skip the check if no password is required
|
|
if (not pw and not confirm) and self._kickstarted:
|
|
return GUICheck.CHECK_OK
|
|
|
|
# Check for validity errors
|
|
if (not self._pwq_valid) and (self._pwq_error):
|
|
return self._pwq_error
|
|
|
|
pwstrength = self.pw_bar.get_value()
|
|
|
|
if pwstrength < 2:
|
|
# If Done has been clicked twice, waive the check
|
|
if self._waivePasswordClicks > 1:
|
|
return GUICheck.CHECK_OK
|
|
elif self._waivePasswordClicks == 1:
|
|
if self._pwq_error:
|
|
return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % self._pwq_error
|
|
else:
|
|
return _(PASSWORD_WEAK_CONFIRM)
|
|
else:
|
|
if self._pwq_error:
|
|
return _(PASSWORD_WEAK_WITH_ERROR) % self._pwq_error
|
|
else:
|
|
return _(PASSWORD_WEAK)
|
|
else:
|
|
return GUICheck.CHECK_OK
|
|
|
|
def on_back_clicked(self, button):
|
|
# Add a click and re-check the password strength
|
|
self._waivePasswordClicks += 1
|
|
self._pwStrengthCheck.update_check_status()
|
|
|
|
# If neither the password nor the confirm field are set, skip the checks
|
|
if (not self.pw.get_text()) and (not self.confirm.get_text()):
|
|
for check in self.checks:
|
|
check.disable()
|
|
|
|
NormalSpoke.on_back_clicked(self, button)
|