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;
|
namespace Icinga\Forms\Account;
|
||||||
|
|
||||||
|
use Icinga\Application\Config;
|
||||||
|
use Icinga\Authentication\PasswordValidator;
|
||||||
use Icinga\Authentication\User\DbUserBackend;
|
use Icinga\Authentication\User\DbUserBackend;
|
||||||
use Icinga\Data\Filter\Filter;
|
use Icinga\Data\Filter\Filter;
|
||||||
use Icinga\User;
|
use Icinga\User;
|
||||||
use Icinga\Web\Form;
|
use Icinga\Web\Form;
|
||||||
use Icinga\Web\Notification;
|
use Icinga\Web\Notification;
|
||||||
|
use ipl\Html\Text;
|
||||||
|
use Icinga\Forms\Config\GeneralConfigForm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form for changing user passwords
|
* Form for changing user passwords
|
||||||
@ -29,40 +33,50 @@ class ChangePasswordForm extends Form
|
|||||||
$this->setSubmitLabel($this->translate('Update Account'));
|
$this->setSubmitLabel($this->translate('Update Account'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
* @throws \Zend_Validate_Exception
|
||||||
*/
|
*/
|
||||||
public function createElements(array $formData)
|
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(
|
$this->addElement(
|
||||||
'password',
|
'password',
|
||||||
'old_password',
|
'old_password',
|
||||||
array(
|
array(
|
||||||
'label' => $this->translate('Old Password'),
|
'label' => $this->translate('Old Password'),
|
||||||
'required' => true
|
'required' => true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'password',
|
'password',
|
||||||
'new_password',
|
'new_password',
|
||||||
array(
|
array(
|
||||||
'label' => $this->translate('New Password'),
|
'label' => $this->translate('New Password'),
|
||||||
'required' => true
|
'required' => true,
|
||||||
|
'validators' => array(new PasswordValidator())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'password',
|
'password',
|
||||||
'new_password_confirmation',
|
'new_password_confirmation',
|
||||||
array(
|
array(
|
||||||
'label' => $this->translate('Confirm New Password'),
|
'label' => $this->translate('Confirm New Password'),
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'validators' => array(
|
'validators' => array(
|
||||||
array('identical', false, array('new_password'))
|
array('identical', false, array('new_password'))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
@ -83,13 +97,13 @@ class ChangePasswordForm extends Form
|
|||||||
public function isValid($formData)
|
public function isValid($formData)
|
||||||
{
|
{
|
||||||
$valid = parent::isValid($formData);
|
$valid = parent::isValid($formData);
|
||||||
if (! $valid) {
|
if (!$valid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$oldPasswordEl = $this->getElement('old_password');
|
$oldPasswordEl = $this->getElement('old_password');
|
||||||
|
|
||||||
if (! $this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) {
|
if (!$this->backend->authenticate($this->Auth()->getUser(), $oldPasswordEl->getValue())) {
|
||||||
$oldPasswordEl->addError($this->translate('Old password is invalid'));
|
$oldPasswordEl->addError($this->translate('Old password is invalid'));
|
||||||
$this->markAsError();
|
$this->markAsError();
|
||||||
return false;
|
return false;
|
||||||
@ -111,7 +125,7 @@ class ChangePasswordForm extends Form
|
|||||||
/**
|
/**
|
||||||
* Set the user backend
|
* Set the user backend
|
||||||
*
|
*
|
||||||
* @param DbUserBackend $backend
|
* @param DbUserBackend $backend
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
|
@ -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\DefaultAuthenticationDomainConfigForm;
|
||||||
use Icinga\Forms\Config\General\LoggingConfigForm;
|
use Icinga\Forms\Config\General\LoggingConfigForm;
|
||||||
use Icinga\Forms\Config\General\ThemingConfigForm;
|
use Icinga\Forms\Config\General\ThemingConfigForm;
|
||||||
|
use Icinga\Forms\Config\General\PasswordPolicyConfigForm;
|
||||||
use Icinga\Forms\ConfigForm;
|
use Icinga\Forms\ConfigForm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,9 +33,11 @@ class GeneralConfigForm extends ConfigForm
|
|||||||
$loggingConfigForm = new LoggingConfigForm();
|
$loggingConfigForm = new LoggingConfigForm();
|
||||||
$themingConfigForm = new ThemingConfigForm();
|
$themingConfigForm = new ThemingConfigForm();
|
||||||
$domainConfigForm = new DefaultAuthenticationDomainConfigForm();
|
$domainConfigForm = new DefaultAuthenticationDomainConfigForm();
|
||||||
|
$passwordPolicyConfigForm = new PasswordPolicyConfigForm();
|
||||||
$this->addSubForm($appConfigForm->create($formData));
|
$this->addSubForm($appConfigForm->create($formData));
|
||||||
$this->addSubForm($loggingConfigForm->create($formData));
|
$this->addSubForm($loggingConfigForm->create($formData));
|
||||||
$this->addSubForm($themingConfigForm->create($formData));
|
$this->addSubForm($themingConfigForm->create($formData));
|
||||||
$this->addSubForm($domainConfigForm->create($formData));
|
$this->addSubForm($domainConfigForm->create($formData));
|
||||||
|
$this->addSubForm($passwordPolicyConfigForm->create($formData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
namespace Icinga\Forms\Config\User;
|
namespace Icinga\Forms\Config\User;
|
||||||
|
|
||||||
|
use Icinga\Application\Config;
|
||||||
use Icinga\Application\Hook\ConfigFormEventsHook;
|
use Icinga\Application\Hook\ConfigFormEventsHook;
|
||||||
|
use Icinga\Authentication\PasswordValidator;
|
||||||
use Icinga\Data\Filter\Filter;
|
use Icinga\Data\Filter\Filter;
|
||||||
use Icinga\Forms\RepositoryForm;
|
use Icinga\Forms\RepositoryForm;
|
||||||
use Icinga\Web\Notification;
|
use Icinga\Web\Notification;
|
||||||
@ -15,8 +17,15 @@ 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');
|
||||||
|
if(isset($passwordPolicy) && class_exists($passwordPolicy)) {
|
||||||
|
$passwordPolicyObject = new $passwordPolicy();
|
||||||
|
$this->addDescription($passwordPolicyObject->displayPasswordPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'checkbox',
|
'checkbox',
|
||||||
'is_active',
|
'is_active',
|
||||||
@ -39,7 +48,8 @@ class UserForm extends RepositoryForm
|
|||||||
'password',
|
'password',
|
||||||
array(
|
array(
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'label' => $this->translate('Password')
|
'label' => $this->translate('Password'),
|
||||||
|
'validators' => array(new PasswordValidator())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ use DirectoryIterator;
|
|||||||
use ErrorException;
|
use ErrorException;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Icinga\Application\ProvidedHook\DbMigration;
|
use Icinga\Application\ProvidedHook\DbMigration;
|
||||||
|
use Icinga\Application\ProvidedHook\DefaultPasswordPolicy;
|
||||||
use ipl\I18n\GettextTranslator;
|
use ipl\I18n\GettextTranslator;
|
||||||
use ipl\I18n\StaticTranslator;
|
use ipl\I18n\StaticTranslator;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
@ -740,6 +741,7 @@ abstract class ApplicationBootstrap
|
|||||||
protected function registerApplicationHooks(): self
|
protected function registerApplicationHooks(): self
|
||||||
{
|
{
|
||||||
Hook::register('DbMigration', DbMigration::class, DbMigration::class);
|
Hook::register('DbMigration', DbMigration::class, DbMigration::class);
|
||||||
|
Hook::register('passwordpolicy', DefaultPasswordPolicy::class, DefaultPasswordPolicy::class);
|
||||||
|
|
||||||
return $this;
|
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