Implement 'None Password Policy'-class for consistency

This commit is contained in:
Jolien Trog 2025-08-28 10:27:12 +02:00
parent caf29a127d
commit b618b10ead
8 changed files with 90 additions and 73 deletions

View File

@ -5,6 +5,7 @@ namespace Icinga\Forms\Account;
use Icinga\Application\Config;
use Icinga\Application\Hook\PasswordPolicyHook;
use Icinga\Application\ProvidedHook\DefaultPasswordPolicy;
use Icinga\Authentication\PasswordValidator;
use Icinga\Authentication\User\DbUserBackend;
use Icinga\Data\Filter\Filter;
@ -25,22 +26,12 @@ class ChangePasswordForm extends Form
protected $backend;
/**
* The password policy object
* The password policy object
*
* @var PasswordPolicyHook|null
* @var PasswordPolicyHook
*/
protected ?PasswordPolicyHook $passwordPolicyObject;
protected ?PasswordPolicyHook $passwordPolicyObject = null;
/**
* Constructor
*
* @param PasswordPolicyHook|null $passwordPolicyObject
*/
public function __construct(?PasswordPolicyHook $passwordPolicyObject = null)
{
$this->passwordPolicyObject = $passwordPolicyObject;
parent::__construct();
}
/**
* {@inheritdoc}
*/
@ -54,20 +45,15 @@ class ChangePasswordForm extends Form
*/
public function createElements(array $formData)
{
if ($this->passwordPolicyObject === null) {
$passwordPolicy = Config::app()->get(
'global',
'password_policy'
);
if (isset($passwordPolicy) && class_exists($passwordPolicy)) {
$this->passwordPolicyObject = new $passwordPolicy();
}
}
$passwordPolicy = Config::app()->get(
'global',
'password_policy'
);
$this->passwordPolicyObject = new $passwordPolicy();
$passwordPolicyDescription = $this->passwordPolicyObject->displayPasswordPolicy();
if ($this->passwordPolicyObject) {
$this->addDescription(
$this->passwordPolicyObject->displayPasswordPolicy()
);
if ($passwordPolicyDescription != '') {
$this->addDescription($passwordPolicyDescription);
}
$this->addElement(
@ -84,7 +70,9 @@ class ChangePasswordForm extends Form
array(
'label' => $this->translate('New Password'),
'required' => true,
'validators' => [new PasswordValidator($this->passwordPolicyObject)]
'validators' =>
$this->passwordPolicyObject !== null ?
[new PasswordValidator($this->passwordPolicyObject)] : [],
)
);
$this->addElement(

View File

@ -4,6 +4,7 @@
namespace Icinga\Forms\Config\General;
use Icinga\Application\Hook;
use Icinga\Application\ProvidedHook\DefaultPasswordPolicy;
use Icinga\Web\Form;
/**
@ -35,13 +36,12 @@ class PasswordPolicyConfigForm extends Form
'Enforce strong password requirements for new passwords'
),
'label' => $this->translate('Password Policy'),
'multiOptions' => array_merge(
['' => $this->translate('No Password Policy')],
$passwordPolicies
),
'value' => DefaultPasswordPolicy::class,
'multiOptions' =>$passwordPolicies
]
);
return $this;
}
}

View File

@ -17,7 +17,6 @@ class UserForm extends RepositoryForm
*
* @param array $formData The data sent by the user
*/
protected function createInsertElements(array $formData)
{
$passwordPolicy = Config::app()->get('global', 'password_policy');

View File

@ -8,6 +8,7 @@ use ErrorException;
use Exception;
use Icinga\Application\ProvidedHook\DbMigration;
use Icinga\Application\ProvidedHook\DefaultPasswordPolicy;
use Icinga\Application\ProvidedHook\NonePasswordPolicy;
use ipl\I18n\GettextTranslator;
use ipl\I18n\StaticTranslator;
use LogicException;
@ -742,6 +743,7 @@ abstract class ApplicationBootstrap
{
Hook::register('DbMigration', DbMigration::class, DbMigration::class);
Hook::register('passwordpolicy', DefaultPasswordPolicy::class, DefaultPasswordPolicy::class);
Hook::register('passwordpolicy', NonePasswordPolicy::class, NonePasswordPolicy::class);
return $this;
}

View File

@ -26,5 +26,5 @@ interface PasswordPolicyHook
* @return string|null Returns null if the password is valid,
* otherwise returns an error message describing the violations
*/
public function validatePassword(string $password): ?string;
public function validatePassword(string $password): ?array;
}

View File

@ -4,6 +4,7 @@
namespace Icinga\Application\ProvidedHook;
use Icinga\Application\Hook\PasswordPolicyHook;
use ipl\I18n\Translation;
/**
* Default implementation of a password policy
@ -17,56 +18,64 @@ use Icinga\Application\Hook\PasswordPolicyHook;
*/
class DefaultPasswordPolicy implements PasswordPolicyHook
{
/**
* @inheritdoc
*/
use Translation;
public function getName(): string
{
return 'Default Password Policy';
return 'Default';
}
/**
* @inheritdoc
*/
public function displayPasswordPolicy(): string
{
$message = (
'Password requirements: ' . 'minimum 12 characters, ' .
'at least 1 number, ' .
'1 special character, ' . 'uppercase and lowercase letters'
);
$message =
$this->translate(
'Password requirements: minimum 12 characters, at least 1 number, ' .
'1 special character, uppercase and lowercase letters.'
);
return $message;
}
/**
* @inheritdoc
*/
public function validatePassword(string $password): ?string
public function validatePassword(string $password): ?array
{
$violations = [];
if (strlen($password) < 12) {
$violations[] = ('Password must be at least 12 characters long');
$violations[] =
$this->translate(
'Password must be at least 12 characters long'
);
}
if (! preg_match('/[0-9]/', $password)) {
$violations[] = ('Password must contain at least one number');
$violations[] =
$this->translate(
'Password must contain at least one number'
);
}
if (! preg_match('/[^a-zA-Z0-9]/', $password)) {
$violations[] = ('Password must contain at least one special character');
$violations[] =
$this->translate(
'Password must contain at least one special character'
);
}
if (! preg_match('/[A-Z]/', $password)) {
$violations[] = ('Password must contain at least one uppercase letter');
$violations[] =
$this->translate(
'Password must contain at least one uppercase letter'
);
}
if (! preg_match('/[a-z]/', $password)) {
$violations[] = ('Password must contain at least one lowercase letter');
$violations[] =
$this->translate(
'Password must contain at least one lowercase letter'
);
}
if (! empty($violations)) {
return implode(", ", $violations);
return $violations;
}
return null;

View File

@ -0,0 +1,28 @@
<?php
/* Icinga Web 2 | (c) 2025 Icinga GmbH | GPLv2+ */
namespace Icinga\Application\ProvidedHook;
use Icinga\Application\Hook\PasswordPolicyHook;
/**
* None Password Policy to validate all passwords
*/
class NonePasswordPolicy implements PasswordPolicyHook
{
public function getName(): string
{
return 'None';
}
public function displayPasswordPolicy(): string
{
return '';
}
public function validatePassword(string $password): ?array
{
return null;
}
}

View File

@ -9,16 +9,18 @@ use Zend_Validate_Abstract;
class PasswordValidator extends Zend_Validate_Abstract
{
/**
* @var PasswordPolicyHook|null
* The password policy object
*
* @var PasswordPolicyHook
*/
private ?PasswordPolicyHook $passwordPolicyObject;
private PasswordPolicyHook $passwordPolicyObject;
/**
* Constructor
*
* @param PasswordPolicyHook|null $passwordPolicyObject
* @param PasswordPolicyHook $passwordPolicyObject
*/
public function __construct(?PasswordPolicyHook $passwordPolicyObject = null)
public function __construct(PasswordPolicyHook $passwordPolicyObject)
{
$this->passwordPolicyObject = $passwordPolicyObject;
}
@ -27,28 +29,17 @@ class PasswordValidator extends Zend_Validate_Abstract
* Checks if password matches with password policy
* throws a message if not
*
* If no password policy is set, all passwords are considered valid
*
* @param mixed $value The password to validate
*
* @return bool
*
*/
public function isValid($value): bool
{
$this->_messages = [];
if ($this->passwordPolicyObject === null) {
if ($this->passwordPolicyObject->validatePassword($value) === null) {
return true;
}
$errorMessage = $this->passwordPolicyObject->validatePassword($value);
if ($errorMessage !== null) {
$this->_messages[] = $errorMessage;
return false;
}
return true;
$this->_messages = $this->passwordPolicyObject->validatePassword($value);
return false;
}
}