anaconda/anaconda-40.22.3.13/pyanaconda/input_checking.py

757 lines
26 KiB
Python
Raw Normal View History

2024-11-14 21:39:56 -08:00
#
# 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)