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\Config;
use Icinga\Application\Hook\PasswordPolicyHook; use Icinga\Application\Hook\PasswordPolicyHook;
use Icinga\Application\ProvidedHook\DefaultPasswordPolicy;
use Icinga\Authentication\PasswordValidator; use Icinga\Authentication\PasswordValidator;
use Icinga\Authentication\User\DbUserBackend; use Icinga\Authentication\User\DbUserBackend;
use Icinga\Data\Filter\Filter; use Icinga\Data\Filter\Filter;
@ -27,20 +28,10 @@ class ChangePasswordForm extends Form
/** /**
* 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} * {@inheritdoc}
*/ */
@ -54,20 +45,15 @@ class ChangePasswordForm extends Form
*/ */
public function createElements(array $formData) public function createElements(array $formData)
{ {
if ($this->passwordPolicyObject === null) {
$passwordPolicy = Config::app()->get( $passwordPolicy = Config::app()->get(
'global', 'global',
'password_policy' 'password_policy'
); );
if (isset($passwordPolicy) && class_exists($passwordPolicy)) {
$this->passwordPolicyObject = new $passwordPolicy(); $this->passwordPolicyObject = new $passwordPolicy();
} $passwordPolicyDescription = $this->passwordPolicyObject->displayPasswordPolicy();
}
if ($this->passwordPolicyObject) { if ($passwordPolicyDescription != '') {
$this->addDescription( $this->addDescription($passwordPolicyDescription);
$this->passwordPolicyObject->displayPasswordPolicy()
);
} }
$this->addElement( $this->addElement(
@ -84,7 +70,9 @@ class ChangePasswordForm extends Form
array( array(
'label' => $this->translate('New Password'), 'label' => $this->translate('New Password'),
'required' => true, 'required' => true,
'validators' => [new PasswordValidator($this->passwordPolicyObject)] 'validators' =>
$this->passwordPolicyObject !== null ?
[new PasswordValidator($this->passwordPolicyObject)] : [],
) )
); );
$this->addElement( $this->addElement(

View File

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

View File

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

View File

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

View File

@ -26,5 +26,5 @@ interface PasswordPolicyHook
* @return string|null Returns null if the password is valid, * @return string|null Returns null if the password is valid,
* otherwise returns an error message describing the violations * 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; namespace Icinga\Application\ProvidedHook;
use Icinga\Application\Hook\PasswordPolicyHook; use Icinga\Application\Hook\PasswordPolicyHook;
use ipl\I18n\Translation;
/** /**
* Default implementation of a password policy * Default implementation of a password policy
@ -17,56 +18,64 @@ use Icinga\Application\Hook\PasswordPolicyHook;
*/ */
class DefaultPasswordPolicy implements PasswordPolicyHook class DefaultPasswordPolicy implements PasswordPolicyHook
{ {
/** use Translation;
* @inheritdoc
*/
public function getName(): string public function getName(): string
{ {
return 'Default Password Policy'; return 'Default';
} }
/**
* @inheritdoc
*/
public function displayPasswordPolicy(): string public function displayPasswordPolicy(): string
{ {
$message = ( $message =
'Password requirements: ' . 'minimum 12 characters, ' . $this->translate(
'at least 1 number, ' . 'Password requirements: minimum 12 characters, at least 1 number, ' .
'1 special character, ' . 'uppercase and lowercase letters' '1 special character, uppercase and lowercase letters.'
); );
return $message; return $message;
} }
/** public function validatePassword(string $password): ?array
* @inheritdoc
*/
public function validatePassword(string $password): ?string
{ {
$violations = []; $violations = [];
if (strlen($password) < 12) { 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)) { 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)) { 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)) { 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)) { 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)) { if (! empty($violations)) {
return implode(", ", $violations); return $violations;
} }
return null; 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 class PasswordValidator extends Zend_Validate_Abstract
{ {
/** /**
* @var PasswordPolicyHook|null * The password policy object
*
* @var PasswordPolicyHook
*/ */
private ?PasswordPolicyHook $passwordPolicyObject; private PasswordPolicyHook $passwordPolicyObject;
/** /**
* Constructor * Constructor
* *
* @param PasswordPolicyHook|null $passwordPolicyObject * @param PasswordPolicyHook $passwordPolicyObject
*/ */
public function __construct(?PasswordPolicyHook $passwordPolicyObject = null) public function __construct(PasswordPolicyHook $passwordPolicyObject)
{ {
$this->passwordPolicyObject = $passwordPolicyObject; $this->passwordPolicyObject = $passwordPolicyObject;
} }
@ -27,28 +29,17 @@ class PasswordValidator extends Zend_Validate_Abstract
* Checks if password matches with password policy * Checks if password matches with password policy
* throws a message if not * throws a message if not
* *
* If no password policy is set, all passwords are considered valid
*
* @param mixed $value The password to validate * @param mixed $value The password to validate
* *
* @return bool * @return bool
*
*/ */
public function isValid($value): bool public function isValid($value): bool
{ {
$this->_messages = []; if ($this->passwordPolicyObject->validatePassword($value) === null) {
if ($this->passwordPolicyObject === null) {
return true; return true;
} }
$errorMessage = $this->passwordPolicyObject->validatePassword($value); $this->_messages = $this->passwordPolicyObject->validatePassword($value);
if ($errorMessage !== null) {
$this->_messages[] = $errorMessage;
return false; return false;
} }
return true;
}
} }