diff --git a/data/interactive-defaults.ks b/data/interactive-defaults.ks index f692ccdc8f9..b73c757e611 100644 --- a/data/interactive-defaults.ks +++ b/data/interactive-defaults.ks @@ -2,3 +2,10 @@ # This is not loaded if a kickstart file is provided on the command line. auth --enableshadow --passalgo=sha512 firstboot --enable + +%anaconda +# Default password policies +pwpolicy root --strict --minlen=8 --minquality=50 --nochanges --emptyok +pwpolicy user --strict --minlen=8 --minquality=50 --nochanges --emptyok +pwpolicy luks --strict --minlen=8 --minquality=50 --nochanges --emptyok +%end diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 8f754455580..72b7d130446 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -62,12 +62,15 @@ from pyanaconda.ui.common import collect from pyanaconda.addons import AddonSection, AddonData, AddonRegistry, collect_addon_paths from pyanaconda.bootloader import GRUB2, get_bootloader +from pyanaconda.pwpolicy import F22_PwPolicy, F22_PwPolicyData from pykickstart.constants import CLEARPART_TYPE_NONE, FIRSTBOOT_SKIP, FIRSTBOOT_RECONFIG, KS_SCRIPT_POST, KS_SCRIPT_PRE, \ KS_SCRIPT_TRACEBACK, SELINUX_DISABLED, SELINUX_ENFORCING, SELINUX_PERMISSIVE +from pykickstart.base import BaseHandler from pykickstart.errors import formatErrorMsg, KickstartError, KickstartValueError from pykickstart.parser import KickstartParser from pykickstart.parser import Script as KSScript +from pykickstart.sections import Section from pykickstart.sections import NullSection, PackageSection, PostScriptSection, PreScriptSection, TracebackScriptSection from pykickstart.version import returnClassForVersion @@ -1820,6 +1823,61 @@ def parse(self, *args): iutil.ipmi_report(IPMI_ABORTED) sys.exit(1) +### +### %anaconda Section +### + +class AnacondaSectionHandler(BaseHandler): + """A handler for only the anaconda ection's commands.""" + commandMap = { + "pwpolicy": F22_PwPolicy + } + + dataMap = { + "PwPolicyData": F22_PwPolicyData + } + + def __init__(self): + BaseHandler.__init__(self, mapping=self.commandMap, dataMapping=self.dataMap) + + def __str__(self): + """Return the %anaconda section""" + retval = "" + lst = sorted(self._writeOrder.keys()) + for prio in lst: + for obj in self._writeOrder[prio]: + retval += str(obj) + + if retval: + retval = "\n%anaconda\n" + retval + "%end\n" + return retval + +class AnacondaSection(Section): + """A section for anaconda specific commands.""" + sectionOpen = "%anaconda" + + def __init__(self, *args, **kwargs): + Section.__init__(self, *args, **kwargs) + self.cmdno = 0 + + def handleLine(self, line): + if not self.handler: + return + + self.cmdno += 1 + args = shlex.split(line, comments=True) + self.handler.currentCmd = args[0] + self.handler.currentLine = self.cmdno + return self.handler.dispatcher(args, self.cmdno) + + def handleHeader(self, lineno, args): + """Process the arguments to the %anaconda header.""" + Section.handleHeader(self, lineno, args) + + def finalize(self): + """Let %anaconda know no additional data will come.""" + Section.finalize(self) + ### ### HANDLERS ### @@ -1912,8 +1970,11 @@ def __init__(self, addon_paths=None, commandUpdates=None, dataUpdates=None): # Prepare the final structures for 3rd party addons self.addons = AddonRegistry(addons) + # The %anaconda section uses its own handler for a limited set of commands + self.anaconda = AnacondaSectionHandler() + def __str__(self): - return superclass.__str__(self) + "\n" + str(self.addons) + return superclass.__str__(self) + "\n" + str(self.addons) + str(self.anaconda) class AnacondaPreParser(KickstartParser): # A subclass of KickstartParser that only looks for %pre scripts and @@ -1931,6 +1992,7 @@ def setupSections(self): self.registerSection(NullSection(self.handler, sectionOpen="%traceback")) self.registerSection(NullSection(self.handler, sectionOpen="%packages")) self.registerSection(NullSection(self.handler, sectionOpen="%addon")) + self.registerSection(NullSection(self.handler.anaconda, sectionOpen="%anaconda")) class AnacondaKSParser(KickstartParser): @@ -1951,6 +2013,7 @@ def setupSections(self): self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(PackageSection(self.handler)) self.registerSection(AddonSection(self.handler)) + self.registerSection(AnacondaSection(self.handler.anaconda)) def preScriptPass(f): # The first pass through kickstart file processing - look for %pre scripts diff --git a/pyanaconda/pwpolicy.py b/pyanaconda/pwpolicy.py new file mode 100644 index 00000000000..c061e1af5ad --- /dev/null +++ b/pyanaconda/pwpolicy.py @@ -0,0 +1,140 @@ +# +# Brian C. Lane +# +# Copyright 2015 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. 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. +# +from pykickstart.base import BaseData, KickstartCommand +from pykickstart.errors import KickstartValueError, formatErrorMsg +from pykickstart.options import KSOptionParser + +import warnings +from pyanaconda.i18n import _ + +class F22_PwPolicyData(BaseData): + """ Kickstart Data object to hold information about pwpolicy. """ + removedKeywords = BaseData.removedKeywords + removedAttrs = BaseData.removedAttrs + + def __init__(self, *args, **kwargs): + BaseData.__init__(self, *args, **kwargs) + self.name = kwargs.get("name", "") + self.minlen = kwargs.get("minlen", 8) + self.minquality = kwargs.get("minquality", 50) + self.strict = kwargs.get("strict", True) + self.changesok = kwargs.get("changesok", False) + self.emptyok = kwargs.get("emptyok", True) + + def __eq__(self, y): + if not y: + return False + + return self.name == y.name + + def __ne__(self, y): + return not self == y + + def __str__(self): + retval = BaseData.__str__(self) + + if self.name != "": + retval += "pwpolicy" + retval += self._getArgsAsStr() + "\n" + + return retval + + def _getArgsAsStr(self): + retval = "" + + retval += " %s" % self.name + retval += " --minlen=%d" % self.minlen + retval += " --minquality=%d" % self.minquality + + if self.strict: + retval += " --strict" + else: + retval += " --notstrict" + if self.changesok: + retval += " --changesok" + else: + retval += " --nochanges" + if self.emptyok: + retval += " --emptyok" + else: + retval += " --notempty" + + return retval + +class F22_PwPolicy(KickstartCommand): + """ Kickstart command implementing password policy. """ + removedKeywords = KickstartCommand.removedKeywords + removedAttrs = KickstartCommand.removedAttrs + + def __init__(self, writePriority=0, *args, **kwargs): + KickstartCommand.__init__(self, writePriority, *args, **kwargs) + self.op = self._getParser() + + self.policyList = kwargs.get("policyList", []) + + def __str__(self): + retval = "" + for policy in self.policyList: + retval += policy.__str__() + + return retval + + def _getParser(self): + op = KSOptionParser() + op.add_option("--minlen", type="int") + op.add_option("--minquality", type="int") + op.add_option("--strict", action="store_true") + op.add_option("--notstrict", dest="strict", action="store_false") + op.add_option("--changesok", action="store_true") + op.add_option("--nochanges", dest="changesok", action="store_false") + op.add_option("--emptyok", action="store_true") + op.add_option("--notempty", dest="emptyok", action="store_false") + return op + + def parse(self, args): + (opts, extra) = self.op.parse_args(args=args, lineno=self.lineno) + if len(extra) != 1: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=_("policy name required for %s") % "pwpolicy")) + + pd = self.handler.PwPolicyData() + self._setToObj(self.op, opts, pd) + pd.lineno = self.lineno + pd.name = extra[0] + + # Check for duplicates in the data list. + if pd in self.dataList(): + warnings.warn(_("A %s with the name %s has already been defined.") % ("pwpolicy", pd.name)) + + return pd + + def dataList(self): + return self.policyList + + def get_policy(self, name): + """ Get the policy by name + + :param str name: Name of the policy to return. + + """ + policy = [p for p in self.policyList if p.name == name] + if policy: + return policy[0] + else: + return None diff --git a/pyanaconda/ui/gui/spokes/lib/passphrase.py b/pyanaconda/ui/gui/spokes/lib/passphrase.py index 0d45b479e01..c104194c430 100644 --- a/pyanaconda/ui/gui/spokes/lib/passphrase.py +++ b/pyanaconda/ui/gui/spokes/lib/passphrase.py @@ -61,9 +61,9 @@ def __init__(self, data): self._strength_bar.add_offset_value("high", 4) # Configure the password policy, if available. Otherwise use defaults. - self.policy = self.data.pwpolicy.get_policy("luks") + self.policy = self.data.anaconda.pwpolicy.get_policy("luks") if not self.policy: - self.policy = self.data.pwpolicy.handler.PwPolicyData() + self.policy = self.data.anaconda.PwPolicyData() # These will be set up later. self._pwq = None diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py index ffbe40fb231..6a73b194daf 100644 --- a/pyanaconda/ui/gui/spokes/password.py +++ b/pyanaconda/ui/gui/spokes/password.py @@ -108,9 +108,9 @@ def initialize(self): self.pw_bar.add_offset_value("high", 4) # Configure the password policy, if available. Otherwise use defaults. - self.policy = self.data.pwpolicy.get_policy("root") + self.policy = self.data.anaconda.pwpolicy.get_policy("root") if not self.policy: - self.policy = self.data.pwpolicy.handler.PwPolicyData() + self.policy = self.data.anaconda.PwPolicyData() def refresh(self): # Enable the input checks in case they were disabled on the last exit diff --git a/pyanaconda/ui/gui/spokes/user.py b/pyanaconda/ui/gui/spokes/user.py index bffb8250b1c..9b647afa68d 100644 --- a/pyanaconda/ui/gui/spokes/user.py +++ b/pyanaconda/ui/gui/spokes/user.py @@ -277,9 +277,9 @@ def initialize(self): self.pw_bar.add_offset_value("high", 4) # Configure the password policy, if available. Otherwise use defaults. - self.policy = self.data.pwpolicy.get_policy("user") + self.policy = self.data.anaconda.pwpolicy.get_policy("user") if not self.policy: - self.policy = self.data.pwpolicy.handler.PwPolicyData() + self.policy = self.data.anaconda.PwPolicyData() # indicate when the password was set by kickstart self._user.password_kickstarted = self.data.user.seen diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py index 0c9b4190631..868fdcce652 100644 --- a/pyanaconda/ui/tui/spokes/__init__.py +++ b/pyanaconda/ui/tui/spokes/__init__.py @@ -112,9 +112,9 @@ def __init__(self, app, data, storage, payload, instclass, policy_name=""): def refresh(self, args=None): # Configure the password policy, if available. Otherwise use defaults. - self.policy = self.data.pwpolicy.get_policy(policy_name) + self.policy = self.data.anaconda.pwpolicy.get_policy(policy_name) if not self.policy: - self.policy = self.data.pwpolicy.handler.PwPolicyData() + self.policy = self.data.anaconda.PwPolicyData() self._window = [] self.value = None diff --git a/tests/pyanaconda_tests/pwpolicy.py b/tests/pyanaconda_tests/pwpolicy.py new file mode 100644 index 00000000000..54dda51f440 --- /dev/null +++ b/tests/pyanaconda_tests/pwpolicy.py @@ -0,0 +1,51 @@ +# +# Brian C. Lane +# +# Copyright 2015 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. 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. +# +from mock import Mock +import unittest + +class BaseTestCase(unittest.TestCase): + def setUp(self): + import sys + + sys.modules["anaconda_log"] = Mock() + sys.modules["block"] = Mock() + + from pyanaconda import kickstart + self.kickstart = kickstart + self.handler = kickstart.AnacondaKSHandler() + self.ksparser = kickstart.AnacondaKSParser(self.handler) + +class PwPolicyTestCase(BaseTestCase): + ks = """ +%anaconda +pwpolicy root --strict --minlen=8 --minquality=50 --nochanges --emptyok +pwpolicy user --strict --minlen=8 --minquality=50 --nochanges --emptyok +pwpolicy luks --strict --minlen=8 --minquality=50 --nochanges --emptyok +%end +""" + def pwpolicy_test(self): + self.ksparser.readKickstartFromString(self.ks) + + self.assertIsInstance(self.handler, self.kickstart.AnacondaKSHandler) + self.assertIsInstance(self.handler.anaconda, self.kickstart.AnacondaSectionHandler) + + eq_template = "pwpolicy %s --minlen=8 --minquality=50 --strict --nochanges --emptyok\n" + for name in ["root", "user", "luks"]: + self.assertEqual(str(self.handler.anaconda.pwpolicy.get_policy(name)), eq_template % name) # pylint: disable=no-member