[PATCH 08/12] Use GUICheck checks for the root password strength

David Shea dshea at redhat.com
Fri Oct 4 18:28:14 UTC 2013


---
 pyanaconda/ui/gui/spokes/password.glade |   3 +-
 pyanaconda/ui/gui/spokes/password.py    | 213 ++++++++++++++++++++------------
 2 files changed, 132 insertions(+), 84 deletions(-)

diff --git a/pyanaconda/ui/gui/spokes/password.glade b/pyanaconda/ui/gui/spokes/password.glade
index 3ad5055..b57b7e4 100644
--- a/pyanaconda/ui/gui/spokes/password.glade
+++ b/pyanaconda/ui/gui/spokes/password.glade
@@ -87,7 +87,7 @@
                         <property name="can_focus">True</property>
                         <property name="visibility">False</property>
                         <property name="invisible_char">●</property>
-                        <signal name="changed" handler="_checkPassword" swapped="no"/>
+                        <signal name="changed" handler="_updatePwQuality" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
@@ -103,7 +103,6 @@
                         <property name="visibility">False</property>
                         <property name="invisible_char">●</property>
                         <property name="activates_default">True</property>
-                        <signal name="changed" handler="_checkPassword" swapped="no"/>
                       </object>
                       <packing>
                         <property name="left_attach">1</property>
diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py
index 022b287..0ce00a3 100644
--- a/pyanaconda/ui/gui/spokes/password.py
+++ b/pyanaconda/ui/gui/spokes/password.py
@@ -23,13 +23,14 @@ from pyanaconda.i18n import _, N_
 from pyanaconda.users import cryptPassword, validatePassword, checkPassword
 from pwquality import PWQError
 
+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_WITH_ERROR
+        PASSWORD_WEAK_CONFIRM, PASSWORD_WEAK_CONFIRM_WITH_ERROR
 
 __all__ = ["PasswordSpoke"]
 
@@ -47,9 +48,6 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
 
     def __init__(self, *args):
         NormalSpoke.__init__(self, *args)
-        self._password = None
-        self._error = False
-        self._oldweak = None
         self._kickstarted = False
 
     def initialize(self):
@@ -58,6 +56,34 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
         self.pw = self.builder.get_object("pw")
         self.confirm = self.builder.get_object("confirm")
 
+        # 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._kickstarted = self.data.rootpw.seen
         if self._kickstarted:
             self.pw.set_placeholder_text(_("The password is set."))
@@ -73,13 +99,11 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
 
     def refresh(self):
         self.pw.grab_focus()
-        self._checkPassword()
+        self.pw.emit("changed")
 
     @property
     def status(self):
-        if self._error:
-            return _("Error setting root password")
-        elif self.data.rootpw.password:
+        if self.data.rootpw.password:
             return _("Root password is set")
         elif self.data.rootpw.lock:
             return _("Root account is disabled")
@@ -92,10 +116,11 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
                             if "wheel" in user.groups)
 
     def apply(self):
-        if self._password is None and self._kickstarted:
+        pw = self.pw.get_text()
+        if (not pw) and (self._kickstarted):
             return
 
-        self.data.rootpw.password = cryptPassword(self._password)
+        self.data.rootpw.password = cryptPassword(pw)
         self.data.rootpw.isCrypted = True
         self.data.rootpw.lock = False
 
@@ -110,102 +135,126 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
     def completed(self):
         return bool(self.data.rootpw.password or self.data.rootpw.lock)
 
-    def _checkPassword(self, editable = None, data = None):
-        """This method updates the password indicators according
-        to the passwords entered by the user. It is called by
-        the changed Gtk event handler.
+    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()
+
+        # Skip the check if no password is required
+        if (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
+
         try:
-            strength = checkPassword(self.pw.get_text())
+            strength = checkPassword(pwtext)
             _pwq_error = None
         except PWQError as e:
             _pwq_error = e[1]
             strength = 0
 
-        if strength < 50:
+        if not pwtext:
+            val = 0
+        elif strength < 50:
             val = 1
-            if _pwq_error:
-                self._error = PASSWORD_WEAK_WITH_ERROR % _pwq_error
-            else:
-                self._error = PASSWORD_WEAK
         elif strength < 75:
             val = 2
-            self._error = False
         elif strength < 90:
             val = 3
-            self._error = False
         else:
             val = 4
-            self._error = False
-
-        if not self.pw.get_text():
-            val = 0
-            self._error = PASSWORD_EMPTY_ERROR
-        elif self.confirm.get_text() and self.pw.get_text() != self.confirm.get_text():
-            self._error = PASSWORD_CONFIRM_ERROR_GUI
-
         text = PASSWORD_STRENGTH_DESC[val]
 
         self.pw_bar.set_value(val)
         self.pw_label.set_text(text)
 
-        self.clear_info()
-        if self._error:
-            self.set_warning(self._error)
-            self.window.show_all()
-            return False
+    def _checkPasswordStrength(self, editable=None, data=None):
+        """Update the error message based on password strength.
 
-        return True
+           Convert the strength set by _updatePwQuality into an error message.
+        """
 
-    def _validatePassword(self):
-        # Do various steps to validate the password
-        # sets self._error to an error string
-        # Return True if valid, False otherwise
-        self._error = False
         pw = self.pw.get_text()
         confirm = self.confirm.get_text()
 
-        if not pw and not confirm:
-            if self._kickstarted:
-                return True
-            else:
-                self._error = _("You must provide and confirm a password.")
-                return False
-
-        try:
-            self._error = validatePassword(pw, confirm)
-        except PWQError as e:
-            if pw == self._oldweak:
-                # We got a second attempt with the same weak password
-                pass
+        # Skip the check if no password is required
+        if (not pw and not confirm) and self._kickstarted:
+            return GUICheck.CHECK_OK
+
+        pwstrength = self.pw_bar.get_value()
+
+        # If the password passed the pwquality tesxt, see if validatePassword
+        # catches anything else
+        if pwstrength >= 2:
+            self._pwq_error = validatePassword(self.pw.get_text())
+            if self._pwq_error:
+                pwstrength = 0
+
+        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:
-                self._error = PASSWORD_WEAK_CONFIRM_WITH_ERROR % e[1]
-                self._oldweak = pw
-                return False
-
-        if self._error:
-            return False
-
-        # the self._checkPassword function is used to indicate the password
-        # strength and need of hitting the Done button twice so use it here as
-        # well
-        if not self._checkPassword() and pw != self._oldweak:
-            # check failed and the Done button was clicked for the first time
-            self._oldweak = pw
-            return False
-
-        # if no errors, clear the info for next time we go into the spoke
-        self._password = pw
-        self.clear_info()
-        self._error = False
-        return True
+                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):
-        if self._validatePassword():
-            self.clear_info()
-            NormalSpoke.on_back_clicked(self, button)
-        else:
-            self.clear_info()
-            self.set_warning(self._error)
-            self.pw.grab_focus()
-            self.window.show_all()
+        # Add a click and re-check the password strength
+        self._waivePasswordClicks += 1
+        self._pwStrengthCheck.update_check_status()
+
+        NormalSpoke.on_back_clicked(self, button)
-- 
1.8.3.1



More information about the anaconda-patches mailing list