757 lines
26 KiB
Python
757 lines
26 KiB
Python
|
#
|
||
|
# input_checking.py : input & password/passphrase input checking
|
||
|
#
|
||
|
# Copyright (C) 2013, 2017 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.
|
||
|
#
|
||
|
|
||
|
import pwquality
|
||
|
|
||
|
from pyanaconda.core.signal import Signal
|
||
|
from pyanaconda.core.i18n import _
|
||
|
from pyanaconda.core.kernel import kernel_arguments
|
||
|
from pyanaconda.core import constants, regexes
|
||
|
from pyanaconda.core import users
|
||
|
from pyanaconda.anaconda_loggers import get_module_logger
|
||
|
from pyanaconda.modules.common.constants.objects import USER_INTERFACE
|
||
|
from pyanaconda.modules.common.constants.services import RUNTIME
|
||
|
from pyanaconda.modules.common.structures.policy import PasswordPolicy
|
||
|
|
||
|
log = get_module_logger(__name__)
|
||
|
|
||
|
|
||
|
def get_policy(policy_name) -> PasswordPolicy:
|
||
|
"""Get the password policy data.
|
||
|
|
||
|
:param policy_name: a name of the policy
|
||
|
:return: a password policy data
|
||
|
"""
|
||
|
proxy = RUNTIME.get_proxy(USER_INTERFACE)
|
||
|
policies = PasswordPolicy.from_structure_dict(proxy.PasswordPolicies)
|
||
|
|
||
|
if policy_name in policies:
|
||
|
return policies[policy_name]
|
||
|
|
||
|
return PasswordPolicy.from_defaults(policy_name)
|
||
|
|
||
|
|
||
|
class PwqualitySettingsCache(object):
|
||
|
"""Cache for libpwquality settings used for password validation.
|
||
|
|
||
|
Libpwquality settings instantiation is probably not exactly cheap
|
||
|
and we might need the settings for checking every password (even when
|
||
|
it is being typed by the user) so it makes sense to cache the objects
|
||
|
for reuse. As there might be multiple active policies for different
|
||
|
passwords we need to be able to cache multiple policies based on
|
||
|
minimum password length, as we don't input anything else to libpwquality
|
||
|
than minimum password length and the password itself.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
self._pwq_settings = {}
|
||
|
|
||
|
def get_settings_by_minlen(self, minlen):
|
||
|
settings = self._pwq_settings.get(minlen)
|
||
|
if settings is None:
|
||
|
settings = pwquality.PWQSettings() # pylint: disable=c-extension-no-member
|
||
|
settings.read_config()
|
||
|
settings.minlen = minlen
|
||
|
self._pwq_settings[minlen] = settings
|
||
|
return settings
|
||
|
|
||
|
|
||
|
pwquality_settings_cache = PwqualitySettingsCache()
|
||
|
|
||
|
|
||
|
class PasswordCheckRequest(object):
|
||
|
"""A wrapper for a password check request.
|
||
|
|
||
|
This in general means the password to be checked as well as its validation criteria
|
||
|
such as minimum length, if it can be empty, etc.
|
||
|
"""
|
||
|
|
||
|
def __init__(self):
|
||
|
self._password = ""
|
||
|
self._password_confirmation = ""
|
||
|
self._policy = None
|
||
|
self._pwquality_settings = None
|
||
|
self._username = "root"
|
||
|
self._fullname = ""
|
||
|
self._secret_type = constants.SecretType.PASSWORD
|
||
|
|
||
|
@property
|
||
|
def password(self):
|
||
|
"""Password string to be checked.
|
||
|
|
||
|
:returns: password string for the check
|
||
|
:rtype: str
|
||
|
"""
|
||
|
return self._password
|
||
|
|
||
|
@password.setter
|
||
|
def password(self, new_password):
|
||
|
self._password = new_password
|
||
|
|
||
|
@property
|
||
|
def password_confirmation(self):
|
||
|
"""Content of the password confirmation field.
|
||
|
|
||
|
:returns: password confirmation string
|
||
|
:rtype: str
|
||
|
"""
|
||
|
return self._password_confirmation
|
||
|
|
||
|
@password_confirmation.setter
|
||
|
def password_confirmation(self, new_password_confirmation):
|
||
|
self._password_confirmation = new_password_confirmation
|
||
|
|
||
|
@property
|
||
|
def policy(self):
|
||
|
"""Password quality policy.
|
||
|
|
||
|
:returns: password quality policy
|
||
|
"""
|
||
|
return self._policy
|
||
|
|
||
|
@policy.setter
|
||
|
def policy(self, new_policy):
|
||
|
self._policy = new_policy
|
||
|
|
||
|
@property
|
||
|
def pwquality_settings(self):
|
||
|
"""Settings for libpwquality (if any).
|
||
|
|
||
|
:returns: libpwquality settings
|
||
|
:rtype: pwquality settings object or None
|
||
|
"""
|
||
|
if not self._pwquality_settings:
|
||
|
# pylint: disable=c-extension-no-member
|
||
|
self._pwquality_settings = pwquality_settings_cache.get_settings_by_minlen(
|
||
|
self.policy.min_length
|
||
|
)
|
||
|
return self._pwquality_settings
|
||
|
|
||
|
@property
|
||
|
def username(self):
|
||
|
"""The username for which the password is being set.
|
||
|
|
||
|
If no username is provided, "root" will be used.
|
||
|
Use username=None to disable the username check.
|
||
|
|
||
|
:returns: username corresponding to the password
|
||
|
:rtype: str or None
|
||
|
"""
|
||
|
return self._username
|
||
|
|
||
|
@username.setter
|
||
|
def username(self, new_username):
|
||
|
self._username = new_username
|
||
|
|
||
|
@property
|
||
|
def fullname(self):
|
||
|
"""The full name of the user for which the password is being set.
|
||
|
|
||
|
If no full name is provided, "root" will be used.
|
||
|
|
||
|
:returns: full user name corresponding to the password
|
||
|
:rtype: str or None
|
||
|
"""
|
||
|
return self._fullname
|
||
|
|
||
|
@fullname.setter
|
||
|
def fullname(self, new_fullname):
|
||
|
self._fullname = new_fullname
|
||
|
|
||
|
@property
|
||
|
def secret_type(self):
|
||
|
"""Type of secret that is being checked.
|
||
|
|
||
|
At the moment is either a password or a passphrase and this property decides how it will be
|
||
|
called in error and status messages.
|
||
|
|
||
|
:returns: secret type
|
||
|
:rtype: Enum
|
||
|
"""
|
||
|
return self._secret_type
|
||
|
|
||
|
@secret_type.setter
|
||
|
def secret_type(self, new_type):
|
||
|
"""Set type of secret being checked.
|
||
|
|
||
|
:param Enum new_type: new secret type
|
||
|
"""
|
||
|
# Prevent uknown/unsuported secret type from being set, so that
|
||
|
# this does no blow up later on once we try to access the status/error message
|
||
|
# corresponding to the secret type.
|
||
|
if new_type not in constants.SecretType:
|
||
|
raise RuntimeError("Unknown secret type: {}".format(new_type))
|
||
|
self._secret_type = new_type
|
||
|
|
||
|
|
||
|
class CheckResult(object):
|
||
|
"""Result of an input check."""
|
||
|
|
||
|
def __init__(self):
|
||
|
self._success = False
|
||
|
self._error_message = ""
|
||
|
self.error_message_changed = Signal()
|
||
|
|
||
|
@property
|
||
|
def success(self):
|
||
|
return self._success
|
||
|
|
||
|
@success.setter
|
||
|
def success(self, value):
|
||
|
self._success = value
|
||
|
|
||
|
@property
|
||
|
def error_message(self):
|
||
|
"""Optional error message describing why the input is not valid.
|
||
|
|
||
|
:returns: why the input is bad (provided it is bad) or None
|
||
|
:rtype: str or None
|
||
|
"""
|
||
|
return self._error_message
|
||
|
|
||
|
@error_message.setter
|
||
|
def error_message(self, new_error_message):
|
||
|
self._error_message = new_error_message
|
||
|
self.error_message_changed.emit(new_error_message)
|
||
|
|
||
|
|
||
|
class PasswordValidityCheckResult(CheckResult):
|
||
|
"""A wrapper for results for a password check."""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self._password_score = 0
|
||
|
self.password_score_changed = Signal()
|
||
|
self._status_text = ""
|
||
|
self.status_text_changed = Signal()
|
||
|
self._password_quality = 0
|
||
|
self.password_quality_changed = Signal()
|
||
|
self._length_ok = False
|
||
|
self.length_ok_changed = Signal()
|
||
|
|
||
|
@property
|
||
|
def password_score(self):
|
||
|
"""A high-level integer score indicating password quality.
|
||
|
|
||
|
Goes from 0 (invalid password) to 4 (valid & very strong password).
|
||
|
Mainly used to drive the password quality indicator in the GUI.
|
||
|
"""
|
||
|
return self._password_score
|
||
|
|
||
|
@password_score.setter
|
||
|
def password_score(self, new_score):
|
||
|
self._password_score = new_score
|
||
|
self.password_score_changed.emit(new_score)
|
||
|
|
||
|
@property
|
||
|
def status_text(self):
|
||
|
"""A short overall status message describing the password.
|
||
|
|
||
|
Generally something like "Good.", "Too short.", "Empty.", etc.
|
||
|
|
||
|
:rtype: short status message
|
||
|
:rtype: str
|
||
|
"""
|
||
|
return self._status_text
|
||
|
|
||
|
@status_text.setter
|
||
|
def status_text(self, new_status_text):
|
||
|
self._status_text = new_status_text
|
||
|
self.status_text_changed.emit(new_status_text)
|
||
|
|
||
|
@property
|
||
|
def password_quality(self):
|
||
|
"""More fine grained integer indicator describing password strength.
|
||
|
|
||
|
This basically exports the quality score assigned by libpwquality to the password,
|
||
|
which goes from 0 (unacceptable password) to 100 (strong password).
|
||
|
|
||
|
Note of caution though about using the password quality value - it is intended
|
||
|
mainly for on-line password strength hints, not for long-term stability,
|
||
|
even just because password dictionary updates and other peculiarities of password
|
||
|
strength judging.
|
||
|
|
||
|
:returns: password quality value as reported by libpwquality
|
||
|
:rtype: int
|
||
|
"""
|
||
|
return self._password_quality
|
||
|
|
||
|
@password_quality.setter
|
||
|
def password_quality(self, value):
|
||
|
self._password_quality = value
|
||
|
self.password_quality_changed.emit(value)
|
||
|
|
||
|
@property
|
||
|
def length_ok(self):
|
||
|
"""Reports if the password is long enough.
|
||
|
|
||
|
:returns: if the password is long enough
|
||
|
:rtype: bool
|
||
|
"""
|
||
|
return self._length_ok
|
||
|
|
||
|
@length_ok.setter
|
||
|
def length_ok(self, value):
|
||
|
self._length_ok = value
|
||
|
self.length_ok_changed.emit(value)
|
||
|
|
||
|
|
||
|
class InputCheck(object):
|
||
|
"""Input checking base class."""
|
||
|
|
||
|
def __init__(self):
|
||
|
self._result = CheckResult()
|
||
|
self._skip = False
|
||
|
|
||
|
@property
|
||
|
def result(self):
|
||
|
return self._result
|
||
|
|
||
|
@property
|
||
|
def skip(self):
|
||
|
"""A flag hinting if this check should be skipped."""
|
||
|
return self._skip
|
||
|
|
||
|
@skip.setter
|
||
|
def skip(self, value):
|
||
|
# Checks flagged as skipped are
|
||
|
# considered successful.
|
||
|
# Otherwise old state will linger even if the
|
||
|
# check is skipped during checking runs.
|
||
|
if value:
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
self._skip = value
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""Run the check.
|
||
|
|
||
|
:param check_request: arbitrary input data to be processed
|
||
|
|
||
|
Subclasses need to always implement this.
|
||
|
"""
|
||
|
raise NotImplementedError
|
||
|
|
||
|
|
||
|
class PasswordValidityCheck(InputCheck):
|
||
|
"""Check the validity and quality of a password."""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self._result = PasswordValidityCheckResult()
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""Check the validity and quality of a password.
|
||
|
|
||
|
This is how password quality checking works:
|
||
|
- starts with a password and an optional parameters
|
||
|
- will report if this password can be used at all (score >0)
|
||
|
- will report how strong the password approximately is on a scale of 1-100
|
||
|
- if the password is unusable it will be reported why
|
||
|
|
||
|
This function uses libpwquality to check the password strength.
|
||
|
Pwquality will raise a PWQError on a weak password but this function does
|
||
|
not pass that forward.
|
||
|
|
||
|
If the password fails the PWQSettings conditions, the score will be set to 0
|
||
|
and the resulting error message will contain the reason why the password is bad.
|
||
|
|
||
|
:param check_request: a password check request wrapper
|
||
|
:type check_request: a PasswordCheckRequest instance
|
||
|
:returns: a password check result wrapper
|
||
|
:rtype: a PasswordCheckResult instance
|
||
|
"""
|
||
|
|
||
|
length_ok = False
|
||
|
error_message = ""
|
||
|
pw_quality = 0
|
||
|
try:
|
||
|
# lets run the password through libpwquality
|
||
|
pw_quality = check_request.pwquality_settings.check(check_request.password, None, check_request.username)
|
||
|
# pylint: disable=c-extension-no-member
|
||
|
except pwquality.PWQError as e:
|
||
|
# Leave valid alone here: the password is weak but can still
|
||
|
# be accepted.
|
||
|
# PWQError values are built as a tuple of (int, str)
|
||
|
error_message = e.args[1]
|
||
|
|
||
|
if check_request.policy.allow_empty and not check_request.password:
|
||
|
# if we are OK with empty passwords, then empty passwords are also fine length wise
|
||
|
length_ok = True
|
||
|
else:
|
||
|
length_ok = len(check_request.password) >= check_request.policy.min_length
|
||
|
|
||
|
if not check_request.password:
|
||
|
if check_request.policy.allow_empty:
|
||
|
pw_score = 1
|
||
|
else:
|
||
|
pw_score = 0
|
||
|
status_text = _(constants.SecretStatus.EMPTY.value)
|
||
|
elif not length_ok:
|
||
|
pw_score = 0
|
||
|
status_text = _(constants.SecretStatus.TOO_SHORT.value)
|
||
|
# If the password is too short replace the libpwquality error
|
||
|
# message with a generic "password is too short" message.
|
||
|
# This is because the error messages returned by libpwquality
|
||
|
# for short passwords don't make much sense.
|
||
|
error_message = _(constants.SECRET_TOO_SHORT[check_request.secret_type])
|
||
|
elif error_message:
|
||
|
pw_score = 1
|
||
|
status_text = _(constants.SecretStatus.WEAK.value)
|
||
|
elif pw_quality < 30:
|
||
|
pw_score = 2
|
||
|
status_text = _(constants.SecretStatus.FAIR.value)
|
||
|
elif pw_quality < 70:
|
||
|
pw_score = 3
|
||
|
status_text = _(constants.SecretStatus.GOOD.value)
|
||
|
else:
|
||
|
pw_score = 4
|
||
|
status_text = _(constants.SecretStatus.STRONG.value)
|
||
|
|
||
|
# the policy influences the overall success of the check
|
||
|
# - score 0 & strict == True -> success = False
|
||
|
# - score 0 & strict == False -> success = True
|
||
|
success = not error_message
|
||
|
|
||
|
# set the result now so that the *_changed signals fire only once the check is done
|
||
|
self.result.success = success # pylint: disable=attribute-defined-outside-init
|
||
|
self.result.password_score = pw_score # pylint: disable=attribute-defined-outside-init
|
||
|
self.result.status_text = status_text # pylint: disable=attribute-defined-outside-init
|
||
|
self.result.password_quality = pw_quality # pylint: disable=attribute-defined-outside-init
|
||
|
self.result.error_message = error_message # pylint: disable=attribute-defined-outside-init
|
||
|
self.result.length_ok = length_ok # pylint: disable=attribute-defined-outside-init
|
||
|
|
||
|
|
||
|
class PasswordFIPSCheck(InputCheck):
|
||
|
"""Check if the password is valid under FIPS, if applicable."""
|
||
|
|
||
|
def run(self, check_request):
|
||
|
self.result.success = True
|
||
|
self.result.error_message = ""
|
||
|
|
||
|
if not kernel_arguments.is_enabled("fips"):
|
||
|
return
|
||
|
|
||
|
if len(check_request.password) >= constants.FIPS_PASSPHRASE_MIN_LENGTH:
|
||
|
return
|
||
|
|
||
|
self.result.success = False
|
||
|
self.result.error_message = _(
|
||
|
"In FIPS mode, the passphrase must be at least {} letters long."
|
||
|
).format(constants.FIPS_PASSPHRASE_MIN_LENGTH)
|
||
|
|
||
|
|
||
|
class PasswordConfirmationCheck(InputCheck):
|
||
|
"""Check if the password & password confirmation match."""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self._success_if_confirmation_empty = False
|
||
|
|
||
|
@property
|
||
|
def success_if_confirmation_empty(self):
|
||
|
"""Enables success-if-confirmation-empty mode.
|
||
|
|
||
|
This property can be used to tell the check to report success if the confirmation filed is empty,
|
||
|
which is a paradigm used by Anaconda uses for two things:
|
||
|
- to make it possible for users to exit without setting a valid password
|
||
|
- to make it possible to exit the spoke if only the password is set
|
||
|
but confirmation is empty
|
||
|
"""
|
||
|
return self._success_if_confirmation_empty
|
||
|
|
||
|
@success_if_confirmation_empty.setter
|
||
|
def success_if_confirmation_empty(self, value):
|
||
|
self._success_if_confirmation_empty = value
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""If the user has entered confirmation data, check whether it matches the password."""
|
||
|
if self.success_if_confirmation_empty and not check_request.password_confirmation:
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
elif check_request.password != check_request.password_confirmation:
|
||
|
self.result.error_message = _(constants.SECRET_CONFIRM_ERROR_GUI[check_request.secret_type])
|
||
|
self.result.success = False
|
||
|
else:
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
|
||
|
|
||
|
class PasswordASCIICheck(InputCheck):
|
||
|
"""Check if the password contains non-ASCII characters.
|
||
|
|
||
|
Non-ASCII characters might be hard to type in the console and in the LUKS volume unlocking
|
||
|
screen, so check if the password contains them so we can warn the user.
|
||
|
"""
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""Fail if the password contains non-ASCII characters."""
|
||
|
has_non_ASCII = any(char not in constants.PW_ASCII_CHARS for char in check_request.password)
|
||
|
if check_request.password and has_non_ASCII:
|
||
|
self.result.error_message = _(constants.SECRET_ASCII[check_request.secret_type])
|
||
|
self.result.success = False
|
||
|
else:
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
|
||
|
|
||
|
class PasswordEmptyCheck(InputCheck):
|
||
|
"""Check if the password is set."""
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""Check whether a password has been specified at all."""
|
||
|
if check_request.password:
|
||
|
# password set is always success
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
else:
|
||
|
# otherwise empty password is an error
|
||
|
self.result.error_message = _(constants.SECRET_EMPTY_ERROR[check_request.secret_type])
|
||
|
self.result.success = False
|
||
|
|
||
|
|
||
|
class UsernameCheck(InputCheck):
|
||
|
"""Check if the username is valid."""
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self._success_if_username_empty = False
|
||
|
|
||
|
@property
|
||
|
def success_if_username_empty(self):
|
||
|
"""Should empty username be considered a success ?"""
|
||
|
return self._success_if_username_empty
|
||
|
|
||
|
@success_if_username_empty.setter
|
||
|
def success_if_username_empty(self, value):
|
||
|
self._success_if_username_empty = value
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""Check if the username is valid."""
|
||
|
# in some cases an empty username is also considered valid,
|
||
|
# so that the user can exit the User spoke without filling it in
|
||
|
if self.success_if_username_empty and not check_request.username:
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
else:
|
||
|
success, error_message = users.check_username(check_request.username)
|
||
|
self.result.error_message = error_message
|
||
|
self.result.success = success
|
||
|
|
||
|
|
||
|
class FullnameCheck(InputCheck):
|
||
|
"""Check if the full user name is valid.
|
||
|
|
||
|
Most importantly the full user name cannot contain colons.
|
||
|
"""
|
||
|
|
||
|
def run(self, check_request):
|
||
|
"""Check if the full user name is valid."""
|
||
|
if regexes.GECOS_VALID.match(check_request.fullname):
|
||
|
self.result.error_message = ""
|
||
|
self.result.success = True
|
||
|
else:
|
||
|
self.result.error_message = _("Full name cannot contain colon characters")
|
||
|
self.result.success = False
|
||
|
|
||
|
|
||
|
class InputField(object):
|
||
|
"""An input field containing data to be checked.
|
||
|
|
||
|
The input field can have an initial value that can be
|
||
|
monitored for change via signals.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, initial_content):
|
||
|
self._initial_content = initial_content
|
||
|
self._content = initial_content
|
||
|
self.changed = Signal()
|
||
|
self._initial_change_signal_fired = False
|
||
|
self.changed_from_initial_state = Signal()
|
||
|
|
||
|
@property
|
||
|
def content(self):
|
||
|
return self._content
|
||
|
|
||
|
@content.setter
|
||
|
def content(self, new_content):
|
||
|
old_content = self._content
|
||
|
self._content = new_content
|
||
|
# check if the input changed from the initial state
|
||
|
if old_content != new_content:
|
||
|
self.changed.emit()
|
||
|
# also fire the changed-from-initial-state signal if required
|
||
|
if not self._initial_change_signal_fired and new_content != self._initial_content:
|
||
|
self.changed_from_initial_state.emit()
|
||
|
self._initial_change_signal_fired = True
|
||
|
|
||
|
|
||
|
class PasswordChecker(object):
|
||
|
"""Run multiple password and input checks in a given order and report the results.
|
||
|
|
||
|
All added checks (in insertion order) will be run and results returned as error message
|
||
|
and success value (True/False). If any check fails success will be False and the
|
||
|
error message of the first check to fail will be returned.
|
||
|
|
||
|
It's also possible to mark individual checks to be skipped by setting their skip property to True.
|
||
|
Such check will be skipped during the checking run.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, initial_password_content, initial_password_confirmation_content,
|
||
|
policy_name):
|
||
|
self._password = InputField(initial_password_content)
|
||
|
self._password_confirmation = InputField(initial_password_confirmation_content)
|
||
|
self._checks = []
|
||
|
self._success = False
|
||
|
self._error_message = ""
|
||
|
self._failed_checks = []
|
||
|
self._policy_name = policy_name
|
||
|
self._username = None
|
||
|
self._fullname = ""
|
||
|
self._secret_type = constants.SecretType.PASSWORD
|
||
|
# connect to the password field signals
|
||
|
self.password.changed.connect(self.run_checks)
|
||
|
self.password_confirmation.changed.connect(self.run_checks)
|
||
|
|
||
|
# signals
|
||
|
self.checks_done = Signal()
|
||
|
|
||
|
@property
|
||
|
def password(self):
|
||
|
"""Main password field."""
|
||
|
return self._password
|
||
|
|
||
|
@property
|
||
|
def password_confirmation(self):
|
||
|
"""Password confirmation field."""
|
||
|
return self._password_confirmation
|
||
|
|
||
|
@property
|
||
|
def checks(self):
|
||
|
return self._checks
|
||
|
|
||
|
@property
|
||
|
def success(self):
|
||
|
return self._success
|
||
|
|
||
|
@property
|
||
|
def error_message(self):
|
||
|
return self._error_message
|
||
|
|
||
|
@property
|
||
|
def failed_checks(self):
|
||
|
"""List of checks failed during the last checking run.
|
||
|
|
||
|
If no checks have failed the list will be empty.
|
||
|
|
||
|
:returns: list of failed checks (if any)
|
||
|
:rtype: list
|
||
|
"""
|
||
|
return self._failed_checks
|
||
|
|
||
|
@property
|
||
|
def policy(self):
|
||
|
return get_policy(self._policy_name)
|
||
|
|
||
|
@property
|
||
|
def username(self):
|
||
|
return self._username
|
||
|
|
||
|
@username.setter
|
||
|
def username(self, new_username):
|
||
|
self._username = new_username
|
||
|
|
||
|
@property
|
||
|
def fullname(self):
|
||
|
"""The full name of the user for which the password is being set.
|
||
|
|
||
|
If no full name is provided, "root" will be used.
|
||
|
|
||
|
:returns: full user name corresponding to the password
|
||
|
:rtype: str or None
|
||
|
"""
|
||
|
return self._fullname
|
||
|
|
||
|
@fullname.setter
|
||
|
def fullname(self, new_fullname):
|
||
|
self._fullname = new_fullname
|
||
|
|
||
|
@property
|
||
|
def secret_type(self):
|
||
|
"""Type of secret that is being checked.
|
||
|
|
||
|
At the moment is either a password or a passphrase and this property decides how it will be
|
||
|
called in error and status messages.
|
||
|
|
||
|
:returns: secret type
|
||
|
:rtype: Enum
|
||
|
"""
|
||
|
return self._secret_type
|
||
|
|
||
|
@secret_type.setter
|
||
|
def secret_type(self, new_type):
|
||
|
"""Set type of secret being checked.
|
||
|
|
||
|
:param Enum new_type: new secret type
|
||
|
"""
|
||
|
# Prevent uknown/unsuported secret type from being set, so that
|
||
|
# this does no blow up later on once we try to access the status/error message
|
||
|
# corresponding to the secret type.
|
||
|
if new_type not in constants.SecretType:
|
||
|
raise RuntimeError("Unknown secret type: {}".format(new_type))
|
||
|
self._secret_type = new_type
|
||
|
|
||
|
def add_check(self, check_instance):
|
||
|
"""Add check instance to list of checks."""
|
||
|
self._checks.append(check_instance)
|
||
|
|
||
|
def run_checks(self):
|
||
|
# first we need to prepare a check request instance
|
||
|
check_request = PasswordCheckRequest()
|
||
|
check_request.password = self.password.content
|
||
|
check_request.password_confirmation = self.password_confirmation.content
|
||
|
check_request.policy = self.policy
|
||
|
check_request.username = self.username
|
||
|
check_request.fullname = self.fullname
|
||
|
check_request.secret_type = self.secret_type
|
||
|
|
||
|
# reset the list of failed checks
|
||
|
self._failed_checks = []
|
||
|
|
||
|
error_message = ""
|
||
|
for check in self.checks:
|
||
|
if not check.skip:
|
||
|
check.run(check_request)
|
||
|
if not check.result.success:
|
||
|
if not self.failed_checks:
|
||
|
# a check failed:
|
||
|
# - remember that & it's error message
|
||
|
# - run other checks as well and ignore their error messages (if any)
|
||
|
# - fail the overall check run (success = False)
|
||
|
error_message = check.result.error_message
|
||
|
self._failed_checks.append(check)
|
||
|
if self.failed_checks:
|
||
|
self._error_message = error_message
|
||
|
self._success = False
|
||
|
else:
|
||
|
self._success = True
|
||
|
self._error_message = ""
|
||
|
# trigger the success changed signal
|
||
|
self.checks_done.emit(self._error_message)
|