mirror of
https://github.com/Icinga/icingaweb2.git
synced 2025-09-24 10:27:46 +02:00
Implement password policy with hook
This commit is contained in:
parent
8603044881
commit
95f0c93c73
@ -3,11 +3,15 @@
|
||||
|
||||
namespace Icinga\Forms\Account;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Authentication\PasswordValidator;
|
||||
use Icinga\Authentication\User\DbUserBackend;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\User;
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Web\Notification;
|
||||
use ipl\Html\Text;
|
||||
use Icinga\Forms\Config\GeneralConfigForm;
|
||||
|
||||
/**
|
||||
* Form for changing user passwords
|
||||
@ -29,11 +33,19 @@ class ChangePasswordForm extends Form
|
||||
$this->setSubmitLabel($this->translate('Update Account'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws \Zend_Validate_Exception
|
||||
*/
|
||||
public function createElements(array $formData)
|
||||
{
|
||||
$passwordPolicy = Config::app()->get('global', 'password_policy');
|
||||
if(isset($passwordPolicy) && class_exists($passwordPolicy)) {
|
||||
$passwordPolicyObject = new $passwordPolicy();
|
||||
$this->addDescription($passwordPolicyObject->displayPasswordPolicy());
|
||||
}
|
||||
|
||||
$this->addElement(
|
||||
'password',
|
||||
'old_password',
|
||||
@ -47,7 +59,8 @@ class ChangePasswordForm extends Form
|
||||
'new_password',
|
||||
array(
|
||||
'label' => $this->translate('New Password'),
|
||||
'required' => true
|
||||
'required' => true,
|
||||
'validators' => array(new PasswordValidator())
|
||||
)
|
||||
);
|
||||
$this->addElement(
|
||||
@ -63,6 +76,7 @@ class ChangePasswordForm extends Form
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Forms\Config\General;
|
||||
|
||||
use Icinga\Application\Hook;
|
||||
use Icinga\Web\Form;
|
||||
|
||||
/**
|
||||
* Configuration form for password policy selection
|
||||
*
|
||||
* This form is not used directly but as subform for the {@link GeneralConfigForm}.
|
||||
*/
|
||||
class PasswordPolicyConfigForm extends Form
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
$this->setName('form_config_general_password_policy');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function createElements(array $formData)
|
||||
{
|
||||
$this->addElement(
|
||||
'checkbox',
|
||||
'global_password_policy',
|
||||
array(
|
||||
'label' => $this->translate('Password Policy'),
|
||||
'value' => true,
|
||||
'description' => $this->translate(
|
||||
'Enforce strong password requirements for new passwords'
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$passwordPolicies = [];
|
||||
|
||||
foreach (Hook::all('passwordpolicy') as $class => $policy) {
|
||||
$passwordPolicies[$class] = $policy->getName();
|
||||
}
|
||||
|
||||
asort($passwordPolicies);
|
||||
$this->addElement(
|
||||
'select',
|
||||
'global_password_policy',
|
||||
array(
|
||||
'description' => $this->translate('Enforce strong '.
|
||||
'password requirements for new passwords'),
|
||||
'label' => $this->translate('Password Policy'),
|
||||
'multiOptions' => array_merge(
|
||||
['' => sprintf(' - %s - ',
|
||||
$this->translate('No Password Policy'))],
|
||||
$passwordPolicies
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use Icinga\Forms\Config\General\ApplicationConfigForm;
|
||||
use Icinga\Forms\Config\General\DefaultAuthenticationDomainConfigForm;
|
||||
use Icinga\Forms\Config\General\LoggingConfigForm;
|
||||
use Icinga\Forms\Config\General\ThemingConfigForm;
|
||||
use Icinga\Forms\Config\General\PasswordPolicyConfigForm;
|
||||
use Icinga\Forms\ConfigForm;
|
||||
|
||||
/**
|
||||
@ -32,9 +33,11 @@ class GeneralConfigForm extends ConfigForm
|
||||
$loggingConfigForm = new LoggingConfigForm();
|
||||
$themingConfigForm = new ThemingConfigForm();
|
||||
$domainConfigForm = new DefaultAuthenticationDomainConfigForm();
|
||||
$passwordPolicyConfigForm = new PasswordPolicyConfigForm();
|
||||
$this->addSubForm($appConfigForm->create($formData));
|
||||
$this->addSubForm($loggingConfigForm->create($formData));
|
||||
$this->addSubForm($themingConfigForm->create($formData));
|
||||
$this->addSubForm($domainConfigForm->create($formData));
|
||||
$this->addSubForm($passwordPolicyConfigForm->create($formData));
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
namespace Icinga\Forms\Config\User;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Application\Hook\ConfigFormEventsHook;
|
||||
use Icinga\Authentication\PasswordValidator;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Forms\RepositoryForm;
|
||||
use Icinga\Web\Notification;
|
||||
@ -15,8 +17,15 @@ 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');
|
||||
if(isset($passwordPolicy) && class_exists($passwordPolicy)) {
|
||||
$passwordPolicyObject = new $passwordPolicy();
|
||||
$this->addDescription($passwordPolicyObject->displayPasswordPolicy());
|
||||
}
|
||||
|
||||
$this->addElement(
|
||||
'checkbox',
|
||||
'is_active',
|
||||
@ -39,7 +48,8 @@ class UserForm extends RepositoryForm
|
||||
'password',
|
||||
array(
|
||||
'required' => true,
|
||||
'label' => $this->translate('Password')
|
||||
'label' => $this->translate('Password'),
|
||||
'validators' => array(new PasswordValidator())
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -7,6 +7,7 @@ use DirectoryIterator;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use Icinga\Application\ProvidedHook\DbMigration;
|
||||
use Icinga\Application\ProvidedHook\DefaultPasswordPolicy;
|
||||
use ipl\I18n\GettextTranslator;
|
||||
use ipl\I18n\StaticTranslator;
|
||||
use LogicException;
|
||||
@ -740,6 +741,7 @@ abstract class ApplicationBootstrap
|
||||
protected function registerApplicationHooks(): self
|
||||
{
|
||||
Hook::register('DbMigration', DbMigration::class, DbMigration::class);
|
||||
Hook::register('passwordpolicy', DefaultPasswordPolicy::class, DefaultPasswordPolicy::class);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
30
library/Icinga/Application/Hook/PasswordPolicyHook.php
Normal file
30
library/Icinga/Application/Hook/PasswordPolicyHook.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Application\Hook;
|
||||
|
||||
interface PasswordPolicyHook
|
||||
{
|
||||
/**
|
||||
* Get the name of the password policy
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Displays the rules of the password policy for users
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function displayPasswordPolicy(): string;
|
||||
|
||||
/**
|
||||
* Validate a given password against the defined policy
|
||||
*
|
||||
* @param string $password
|
||||
* @return string|null Returns null if the password is valid,
|
||||
* otherwise returns an error message describing the violations
|
||||
*/
|
||||
public function validatePassword(string $password): ?string;
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Application\ProvidedHook;
|
||||
|
||||
use Icinga\Application\Hook\PasswordPolicyHook;
|
||||
|
||||
/**
|
||||
* Default implementation of a password policy
|
||||
*
|
||||
* Enforces:
|
||||
* - Minimum length of 12 characters
|
||||
* - At least one number
|
||||
* - At least one special character
|
||||
* - At least one uppercase letter
|
||||
* - At least one lowercase letter
|
||||
*/
|
||||
class DefaultPasswordPolicy implements PasswordPolicyHook
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Default Password Policy';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function displayPasswordPolicy(): string
|
||||
{
|
||||
$message = (
|
||||
'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
|
||||
{
|
||||
$violations = [];
|
||||
|
||||
if (strlen($password) < 12) {
|
||||
$violations[] = ('Password must be at least 12 characters long');
|
||||
}
|
||||
|
||||
if (! preg_match('/[0-9]/', $password)) {
|
||||
$violations[] = ('Password must contain at least one number');
|
||||
}
|
||||
|
||||
if (! preg_match('/[^a-zA-Z0-9]/', $password)) {
|
||||
$violations[] = ('Password must contain at least one special character');
|
||||
}
|
||||
|
||||
if (! preg_match('/[A-Z]/', $password)) {
|
||||
$violations[] = ('Password must contain at least one uppercase letter');
|
||||
}
|
||||
|
||||
if (! preg_match('/[a-z]/', $password)) {
|
||||
$violations[] = ('Password must contain at least one lowercase letter');
|
||||
}
|
||||
|
||||
if (! empty($violations)) {
|
||||
return implode(", ", $violations);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
41
library/Icinga/Authentication/PasswordValidator.php
Normal file
41
library/Icinga/Authentication/PasswordValidator.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Icinga\Authentication;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Zend_Validate_Abstract;
|
||||
|
||||
class PasswordValidator extends Zend_Validate_Abstract
|
||||
{
|
||||
/**
|
||||
* Checks if password matches with password policy
|
||||
* throws a message if not
|
||||
*
|
||||
* If no password policy is configured, all passwords are considered valid
|
||||
*
|
||||
* @param mixed $value The password to validate
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
public function isValid($value): bool
|
||||
{
|
||||
$this->_messages = [];
|
||||
$passwordPolicy = Config::app()
|
||||
->get('global', 'password_policy');
|
||||
|
||||
if (! isset($passwordPolicy) || ! class_exists($passwordPolicy)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$passwordPolicyObject = new $passwordPolicy();
|
||||
$errorMessage = $passwordPolicyObject->validatePassword($value);
|
||||
|
||||
if ($errorMessage != null) {
|
||||
$this->_messages[] = $errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user