From 83dca356f03677782d9508477e92041428a6813c Mon Sep 17 00:00:00 2001 From: Martin Sivak Date: Fri, 1 Feb 2013 16:54:51 +0100 Subject: Add the User creation spoke including the Advanced dialog --- po/POTFILES.in | 3 + pyanaconda/ui/gui/spokes/advanced_user.glade | 437 +++++++++++++++++++++++++++ pyanaconda/ui/gui/spokes/password.py | 2 +- pyanaconda/ui/gui/spokes/user.glade | 377 +++++++++++++++++++++++ pyanaconda/ui/gui/spokes/user.py | 420 +++++++++++++++++++++++++ 5 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 pyanaconda/ui/gui/spokes/advanced_user.glade create mode 100644 pyanaconda/ui/gui/spokes/user.glade create mode 100644 pyanaconda/ui/gui/spokes/user.py diff --git a/po/POTFILES.in b/po/POTFILES.in index aaa1231b3..c2ad78d3c 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -66,6 +66,7 @@ pyanaconda/ui/gui/spokes/password.py pyanaconda/ui/gui/spokes/software.py pyanaconda/ui/gui/spokes/source.py pyanaconda/ui/gui/spokes/storage.py +pyanaconda/ui/gui/spokes/user.py pyanaconda/ui/gui/spokes/welcome.py pyanaconda/ui/gui/spokes/lib/accordion.py pyanaconda/ui/gui/spokes/lib/cart.py @@ -73,6 +74,7 @@ pyanaconda/ui/gui/spokes/lib/passphrase.py pyanaconda/ui/gui/spokes/lib/resize.py # Interface files. +pyanaconda/ui/gui/spokes/advanced_user.glade pyanaconda/ui/gui/spokes/datetime_spoke.glade pyanaconda/ui/gui/spokes/network.glade pyanaconda/ui/gui/spokes/software.glade @@ -81,6 +83,7 @@ pyanaconda/ui/gui/spokes/keyboard.glade pyanaconda/ui/gui/spokes/password.glade pyanaconda/ui/gui/spokes/source.glade pyanaconda/ui/gui/spokes/welcome.glade +pyanaconda/ui/gui/spokes/user.glade pyanaconda/ui/gui/spokes/custom.glade pyanaconda/ui/gui/spokes/lib/cart.glade pyanaconda/ui/gui/spokes/lib/detailederror.glade diff --git a/pyanaconda/ui/gui/spokes/advanced_user.glade b/pyanaconda/ui/gui/spokes/advanced_user.glade new file mode 100644 index 000000000..6159f5304 --- /dev/null +++ b/pyanaconda/ui/gui/spokes/advanced_user.glade @@ -0,0 +1,437 @@ + + + + + False + 5 + dialog + False + + + False + vertical + 2 + + + False + end + + + gtk-cancel + True + True + True + True + + + False + True + 0 + + + + + Save Changes + True + True + True + + + False + True + 1 + + + + + False + True + end + 0 + + + + + True + False + 16 + vertical + + + True + False + 0 + ADVANCED USER CONFIGURATION + + + + + + + + False + True + 0 + + + + + True + False + 8 + 0 + Home Directory + + + + + + False + True + 1 + + + + + True + False + 16 + vertical + + + Create a _home directory for this user. + True + True + False + 4 + True + 0 + True + + + + False + True + 0 + + + + + True + False + 42 + 3 + + + True + False + False + Home _directory: + True + t_home + + + False + True + 0 + + + + + True + False + True + 13 + + + + True + True + 1 + + + + + True + True + 1 + + + + + True + True + 2 + + + + + True + False + 8 + 0 + User and Group IDs + + + + + + False + True + 3 + + + + + True + False + 16 + 3 + + + Specify a _user ID manually: + True + True + False + True + 0 + True + + + + 0 + 0 + 1 + 1 + + + + + Specify a _group ID manually: + True + True + False + True + 0 + True + + + + 0 + 1 + 1 + 1 + + + + + True + False + True + 8 + + 5 + 0.0099999997764825821 + uid + + + 1 + 0 + 1 + 1 + + + + + True + False + True + 8 + + 5 + gid + + + 1 + 1 + 1 + 1 + + + + + False + True + 4 + + + + + True + False + 8 + 0 + Group Membership + + + + + + False + True + 5 + + + + + True + False + 16 + 3 + vertical + + + True + False + 0 + _Add user to the following groups: + True + t_groups + + + False + True + 0 + + + + + True + True + 3 + + + + False + True + 1 + + + + + True + False + 3 + + + True + False + 5 + 0 + wheel, my-team (1245), project-x (29935) + + + 1 + 0 + 1 + 1 + + + + + True + False + 5 + 3 + 0 + 0 + You may input a comma-separated list of group names and group IDs here. +Groups that do not already exist will be created; specify their GID in parentheses. + True + + + 1 + 1 + 1 + 1 + + + + + True + False + 1 + Example: + + + + + + 0 + 0 + 1 + 1 + + + + + True + False + 3 + 1 + 0 + Tip: + + + + + + 0 + 1 + 1 + 1 + + + + + False + True + 2 + + + + + False + True + 6 + + + + + False + True + 1 + + + + + + button2 + button1 + + + + 500 + 32535 + 1000 + 1 + 10 + + + 500 + 32535 + 1000 + 1 + 10 + + diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py index 6950472e6..441050e57 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -85,7 +85,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke): @property def mandatory(self): - return False + return not self.data.user.userList def apply(self): self.data.rootpw.password = cryptPassword(self._password) diff --git a/pyanaconda/ui/gui/spokes/user.glade b/pyanaconda/ui/gui/spokes/user.glade new file mode 100644 index 000000000..7dbd705e0 --- /dev/null +++ b/pyanaconda/ui/gui/spokes/user.glade @@ -0,0 +1,377 @@ + + + + + + filler + False + True + True + filler + CREATE USER + + + + False + vertical + 6 + + + True + False + + + False + 6 + 6 + 6 + + + + + False + False + 0 + + + + + False + 12 + 0 + 0.75 + 0.75 + 48 + 24 + 24 + + + False + vertical + 6 + + + True + False + 8 + 9 + + + True + False + 1 + 10 + _Full name + True + t_fullname + + + + + + 0 + 0 + 1 + 1 + + + + + True + False + 1 + 10 + _Username + True + t_username + + + + + + 0 + 1 + 1 + 1 + + + + + True + True + + True + False + + + + 1 + 0 + 1 + 1 + + + + + True + True + + True + + + + 1 + 1 + 1 + 1 + + + + + True + False + 1 + 10 + _Password + True + t_password + + + + + + 0 + 5 + 1 + 1 + + + + + True + False + 1 + 10 + _Confirm password + True + t_verifypassword + + + + + + 0 + 7 + 1 + 1 + + + + + True + True + False + + + + + 1 + 5 + 1 + 1 + + + + + True + True + False + + + + + 1 + 7 + 1 + 1 + + + + + True + False + 0 + <b>Tip:</b> lorem ipsum dolor sit amet... + True + + + 1 + 2 + 1 + 1 + + + + + Require a password to use this account + True + True + False + 0 + True + True + + + + 1 + 4 + 1 + 1 + + + + + True + False + + + True + False + vertical + 2 + GTK_LEVEL_BAR_MODE_DISCRETE + 0 + 4 + GTK_ORIENTATION_HORIZONTAL + 2 + fill + center + + + True + True + 0 + + + + + True + False + center + 6 + empty password + + + + + + False + True + 1 + + + + + 1 + 6 + 1 + 1 + + + + + Make this user administrator + True + True + False + 0 + True + + + 1 + 3 + 1 + 1 + + + + + True + False + + + _Advanced... + True + True + True + True + + + + 0 + 0 + 1 + 1 + + + + + Use network login... + True + True + True + 6 + + + 1 + 0 + 1 + 1 + + + + + 1 + 8 + 1 + 1 + + + + + + + + + + + + + + + + + + + + True + True + 0 + + + + + + + True + True + 1 + + + + + + + + + diff --git a/pyanaconda/ui/gui/spokes/user.py b/pyanaconda/ui/gui/spokes/user.py new file mode 100644 index 000000000..011d71d4f --- /dev/null +++ b/pyanaconda/ui/gui/spokes/user.py @@ -0,0 +1,420 @@ +# User creation spoke +# +# Copyright (C) 2013 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): Martin Sivak +# + +import gettext +_ = lambda x: gettext.ldgettext("anaconda", x) +N_ = lambda x: x + +from gi.repository import Gtk + +from pyanaconda.users import cryptPassword, validatePassword +from pwquality import PWQError + +from pyanaconda.ui.gui.spokes import NormalSpoke +from pyanaconda.ui.gui import GUIObject +from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory +from pyanaconda.ui.common import FirstbootSpokeMixIn +from pyanaconda.ui.gui.utils import enlightbox + +import unicodedata +import pwquality + +__all__ = ["UserSpoke", "AdvancedUserDialog"] + +def strip_accents(s): + """This function takes arbitrary unicode string + and returns it with all the diacritics removed. + + :param s: arbitrary string + :type s: unicode + + :return: s with diacritics removed + :rtype: unicode + + """ + return ''.join((c for c in unicodedata.normalize('NFD', s) + if unicodedata.category(c) != 'Mn')) + +class AdvancedUserDialog(GUIObject): + builderObjects = ["advancedUserDialog", "uid", "gid"] + mainWidgetName = "advancedUserDialog" + uiFile = "advanced_user.glade" + + def __init__(self, user, groupDict, data): + GUIObject.__init__(self, data) + self._user = user + self._groupDict = groupDict + + def initialize(self): + GUIObject.initialize(self) + + def _apply_checkboxes(self, _editable, data = None): + """Update the state of this screen according to the + checkbox states on the screen. It is called from + the toggled Gtk event. + """ + c_home = self.builder.get_object("c_home").get_active() + c_uid = self.builder.get_object("c_uid").get_active() + c_gid = self.builder.get_object("c_gid").get_active() + + self.builder.get_object("t_home").set_sensitive(c_home) + self.builder.get_object("l_home").set_sensitive(c_home) + self.builder.get_object("spin_uid").set_sensitive(c_uid) + self.builder.get_object("spin_gid").set_sensitive(c_gid) + + def refresh(self): + t_home = self.builder.get_object("t_home") + if self._user.homedir: + t_home.set_text(self._user.homedir) + elif self._user.name: + t_home.set_text("/home/%s" % self._user.name) + + groups = [] + for group_name in self._user.groups: + group = self._groupDict[group_name] + + if group.name and group.gid is not None: + groups.append("%s (%d)" % (group.name, group.gid)) + elif group.name: + groups.append(group.name) + elif group.gid is not None: + groups.append("(%d)" % (group.gid,)) + + self.builder.get_object("t_groups").set_text(", ".join(groups)) + + def run(self): + self.window.show() + rc = self.window.run() + self.window.hide() + + #OK clicked + if rc == 1: + if self.builder.get_object("c_home").get_active(): + self._user.homedir = self.builder.get_object("t_home").get_text() + else: + self._user.homedir = None + + if self.builder.get_object("c_uid").get_active(): + self._user.uid = int(self.builder.get_object("uid").get_value()) + else: + self._user.uid = None + + if self.builder.get_object("c_gid").get_active(): + pass + #self._user.gid = int(self.builder.get_widget("gid").get_value()) + else: + #self._user.gid = None + pass + + groups = self.builder.get_object("t_groups").get_text().split(",") + self._user.groups = [] + for group in groups: + group = group.strip() + if group not in self._groupDict: + self._groupDict[group] = self.data.GroupData(name = group) + self._user.groups.append(group) + + #Cancel clicked, window destroyed... + else: + pass + + return rc + + + +class UserSpoke(FirstbootSpokeMixIn, NormalSpoke): + builderObjects = ["userCreationWindow"] + + mainWidgetName = "userCreationWindow" + uiFile = "spokes/user.glade" + + category = UserSettingsCategory + + icon = "avatar-default-symbolic" + title = N_("_USER CREATION") + + def __init__(self, *args): + NormalSpoke.__init__(self, *args) + self._oldweak = None + self._error = False + + def initialize(self): + NormalSpoke.initialize(self) + + if self.data.user.userList: + self._user = self.data.user.userList[0] + else: + self._user = self.data.UserData() + self._wheel = self.data.GroupData(name = "wheel") + self._groupDict = {"wheel": self._wheel} + + # placeholders for the text boxes + self.fullname = self.builder.get_object("t_fullname") + self.username = self.builder.get_object("t_username") + self.pw = self.builder.get_object("t_password") + self.confirm = self.builder.get_object("t_verifypassword") + self.admin = self.builder.get_object("c_admin") + self.usepassword = self.builder.get_object("c_usepassword") + + self.guesser = { + self.username: True + } + + # set up passphrase quality checker + self._pwq = pwquality.PWQSettings() + self._pwq.read_config() + + self.pw_bar = self.builder.get_object("password_bar") + self.pw_label = self.builder.get_object("password_label") + + self._advanced = AdvancedUserDialog(self._user, self._groupDict, + self.data) + self._advanced.initialize() + + def refresh(self): + self.username.set_text(self._user.name) + self.fullname.set_text(self._user.gecos) + self.admin.set_active(self._wheel.name in self._user.groups) + + if self.usepassword.get_active(): + self._checkPassword() + + if self.username.get_text() and self.usepassword.get_active(): + self.pw.grab_focus() + elif self.fullname.get_text(): + self.username.grab_focus() + else: + self.fullname.grab_focus() + + @property + def status(self): + if self._error: + return _("Error creating user account: %s") % self._error + elif len(self.data.user.userList) == 0: + return _("No user will be created") + elif self._wheel.name in self.data.user.userList[0].groups: + return _("Administrator %s will be created") % self.data.user.userList[0].name + else: + return _("User %s will be created") % self.data.user.userList[0].name + + @property + def mandatory(self): + # mandatory only if root account is disabled + return (not self.data.rootpw.password) or self.data.rootpw.lock + + def apply(self): + if self.username.get_text(): + self._user.name = self.username.get_text() + self._user.gecos = self.fullname.get_text() + self._user.password = cryptPassword(self.pw.get_text()) + self._user.isCrypted = True + + if self.admin.get_active() and \ + self._wheel.name not in self._user.groups: + self._user.groups.append(self._wheel.name) + elif not self.admin.get_active() and \ + self._wheel.name in self._user.groups: + self._user.groups.remove(self._wheel.name) + + self.data.group.groupList += [self._groupDict[g] for g in self._user.groups + if g != self._wheel.name] + + if self._user not in self.data.user.userList: + self.data.user.userList.append(self._user) + + elif self._user in self.data.user.userList: + self.data.user.userList.remove(self._user) + + @property + def completed(self): + return self._user in self.data.user.userList + + def _passwordDisabler(self, editable = None, data = None): + """Called by Gtk callback when the "Use password" check + button is toggled. It will make password entries in/sensitive.""" + + self.pw.set_sensitive(self.usepassword.get_active()) + self.confirm.set_sensitive(self.usepassword.get_active()) + if not self.usepassword.get_active(): + self.clear_info() + else: + self._checkPassword() + + def _guessNameDisabler(self, editable = None, data = None): + """Called by Gtk callback when the username or hostname + entry changes. It disables the guess algorithm if the + user added his own text there and reenable it when the + user deletes the whole text.""" + + if editable.get_text() == "": + self.guesser[editable] = True + self._guessNames() + else: + self.guesser[editable] = False + + def _guessNames(self, editable = None, data = None): + """Called by Gtk callback when the full name field changes. + It guesses the username and hostname, strips diacritics + and make those lowercase. + """ + + fullname = self.fullname.get_text().split() + username = fullname[-1].decode("utf-8").lower() + if len(fullname) > 1: + username = fullname[0][0].decode("utf-8").lower() + username + username = strip_accents(username).encode("utf-8") + + # after the text is updated in guesser, the guess has to be reenabled + if self.guesser[self.username]: + self.username.set_text(username) + self.guesser[self.username] = True + + 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. + """ + if self.pw.get_text() == "": + strength = -2 + elif self.pw.get_text() != self.confirm.get_text(): + strength = -1 + else: + try: + strength = self._pwq.check(self.pw.get_text(), None, None) + _pwq_error = None + except pwquality.PWQError as (e, msg): + _pwq_error = msg + strength = 0 + + if strength == -1: + val = 0 + text = _("Mismatch!") + self._error = _("The passwords do not match!") + elif strength == -2: + val = 0 + text = _("Empty!") + self._error = _("The password is empty!") + elif strength < 50: + val = 1 + text = _("Weak") + self._error = _("The password you have provided is weak") + if _pwq_error: + self._error += ": %s. " % _pwq_error + else: + self._error += ". " + self._error += _("You will have to press Done twice to confirm it.") + elif strength < 75: + val = 2 + text = _("Fair") + self._error = False + elif strength < 90: + val = 3 + text = _("Good") + self._error = False + else: + val = 4 + text = _("Strong") + self._error = False + + 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() + + def _validatePassword(self): + """This method checks the password weakness and + implements the Press Done twice logic. It is used from + the on_back_clicked handler. + + It also sets the self._error of the password is not + sufficient or does not pass the pwquality checks. + + :return: True if the password should be accepted, False otherwise + :rtype: bool + + """ + + # 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: + self._error = _("You must provide and confirm a password.") + return False + + try: + self._error = validatePassword(pw, confirm) + except PWQError as (_e, msg): + if pw == self._oldweak: + # We got a second attempt with the same weak password + pass + else: + self._error = _("You have provided a weak password: %s. " + " Press Done again to use anyway.") % msg + self._oldweak = pw + return False + + if self._error: + 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 + + def on_advanced_clicked(self, _button): + """Handler for the Advanced.. button. It starts the Advanced dialog + for setting homedit, uid, gid and groups. + """ + + self._user.name = self.username.get_text() + + if self.admin.get_active() and \ + self._wheel.name not in self._user.groups: + self._user.groups.append(self._wheel.name) + elif not self.admin.get_active() and \ + self._wheel.name in self._user.groups: + self._user.groups.remove(self._wheel.name) + + self._advanced.refresh() + with enlightbox(self.window, self._advanced.window): + response = self._advanced.run() + + self.admin.set_active(self._wheel.name in self._user.groups) + + def on_back_clicked(self, button): + if not self.usepassword.get_active() or self._validatePassword(): + self._error = False + 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() + -- cgit