Skip to content

Commit

Permalink
Implement %anaconda kickstart section for pwpolicy
Browse files Browse the repository at this point in the history
pwpolicy only applies to the installer. This adds an %anaconda section to
kickstart to use with installer specific commands.

Add pwpolicy command to control UI password settings

This command allows the user to specify what policies to apply to the
different password entries.

eg.
pwpolicy root --minlen=10 --minquality=60 --strict --noempty --nochange

The policy names are set by anaconda, pykickstart just checks for its
presence. The arguments are:

--minlen                minimum password length
--minquality            minumum pwquality value
--strict/nostrict       Whether to allow weak passwords via the double
                        done button method.
--empty/notempty        Allow empty passwords
--changesok/nochanges   Allow passwords to be changed if set in
                        kickstart.

This also adds %anaconda to interactive-defaults.ks matching the default object
values.  Users can override by replacing
/usr/share/anaconda/interactive-defaults.ks on the installer media or kickstart
users can include their own %anaconda section.
  • Loading branch information
bcl committed Mar 20, 2015
1 parent f3a8ca6 commit e7e86a2
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 9 deletions.
7 changes: 7 additions & 0 deletions data/interactive-defaults.ks
Expand Up @@ -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
65 changes: 64 additions & 1 deletion pyanaconda/kickstart.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
###
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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
Expand Down
140 changes: 140 additions & 0 deletions pyanaconda/pwpolicy.py
@@ -0,0 +1,140 @@
#
# Brian C. Lane <bcl@redhat.com>
#
# 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
4 changes: 2 additions & 2 deletions pyanaconda/ui/gui/spokes/lib/passphrase.py
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pyanaconda/ui/gui/spokes/password.py
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pyanaconda/ui/gui/spokes/user.py
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pyanaconda/ui/tui/spokes/__init__.py
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions tests/pyanaconda_tests/pwpolicy.py
@@ -0,0 +1,51 @@
#
# Brian C. Lane <bcl@redhat.com>
#
# 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

0 comments on commit e7e86a2

Please sign in to comment.