Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions application/forms/Account/ChangePasswordForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

Comment thread
Al2Klimov marked this conversation as resolved.
namespace Icinga\Forms\Account;

use Icinga\Authentication\PasswordPolicyHelper;
use Icinga\Authentication\User\DbUserBackend;
use Icinga\Data\Filter\Filter;
use Icinga\User;
use Icinga\Web\Form;
use Icinga\Web\Notification;

Expand Down Expand Up @@ -40,27 +40,27 @@ public function createElements(array $formData)
'password',
'old_password',
[
'label' => $this->translate('Old Password'),
'required' => true
'label' => $this->translate('Old Password'),
'required' => true
]
);
$this->addElement(
'password',
'new_password',
[
'label' => $this->translate('New Password'),
'required' => true
'label' => $this->translate('New Password'),
'required' => true
]
);
PasswordPolicyHelper::apply($this, 'new_password', 'old_password');

$this->addElement(
'password',
'new_password_confirmation',
[
'label' => $this->translate('Confirm New Password'),
'required' => true,
'validators' => [
['identical', false, ['new_password']]
]
'label' => $this->translate('Confirm New Password'),
'required' => true,
'validators' => [['identical', false, ['new_password']]]
]
);
}
Expand Down
70 changes: 70 additions & 0 deletions application/forms/Config/General/PasswordPolicyConfigForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

// SPDX-FileCopyrightText: 2026 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

Comment thread
lippserd marked this conversation as resolved.
namespace Icinga\Forms\Config\General;

use Exception;
use Icinga\Application\Config;
use Icinga\Application\Hook\PasswordPolicyHook;
use Icinga\Application\Logger;
use Icinga\Authentication\PasswordPolicyHelper;
use Icinga\Exception\IcingaException;
use Icinga\Web\Form;
use Throwable;

/**
* Configuration form for password policy selection
*
* This form is not used directly but as subform for the {@see GeneralConfigForm}.
*/
class PasswordPolicyConfigForm extends Form

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There already was some discussion with @lippserd to move this form into a new Security section in IW2.
We should consider converting this form into an ipl-web based form instead during this process.

{
/**
* @param Config $config The config to load the configured policy from
*/
public function __construct(protected Config $config)
{
parent::__construct();
}

public function init(): void
{
$this->setName('form_config_general_password_policy');
}

public function createElements(array $formData): static
{
$defaultPolicy = PasswordPolicyHook::DEFAULT_PASSWORD_POLICY;
$elementName = sprintf('%s_%s', PasswordPolicyHook::CONFIG_SECTION, PasswordPolicyHook::CONFIG_KEY);
$this->addElement('select', $elementName, [
'description' => $this->translate('Enforce password requirements for new passwords'),
'label' => $this->translate('Password Policy'),
'value' => $defaultPolicy,
'multiOptions' => iterator_to_array(PasswordPolicyHook::yieldPolicies()),
'autosubmit' => true,
]);

Comment thread
lippserd marked this conversation as resolved.
try {
$policy = PasswordPolicyHook::fromCanonicalName($formData[$elementName] ?? $defaultPolicy);
} catch (Throwable $e) {
Logger::error("%s\n%s", $e, IcingaException::getConfidentialTraceAsString($e));
PasswordPolicyHelper::addError($this, true);

return $this;
}

PasswordPolicyHelper::addDescription($this, $policy);

// Surface a load error if the saved policy is unavailable, e.g. because
// its providing module was disabled. The result is intentionally discarded.
try {
PasswordPolicyHook::loadConfigured($this->config);
} catch (Throwable) {
PasswordPolicyHelper::addError($this, true);
}

return $this;
}
}
3 changes: 3 additions & 0 deletions application/forms/Config/GeneralConfigForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;

/**
Expand All @@ -34,9 +35,11 @@ public function createElements(array $formData)
$loggingConfigForm = new LoggingConfigForm();
$themingConfigForm = new ThemingConfigForm();
$domainConfigForm = new DefaultAuthenticationDomainConfigForm();
$passwordPolicyConfigForm = new PasswordPolicyConfigForm($this->config);
$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));
}
}
44 changes: 32 additions & 12 deletions application/forms/Config/User/UserForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,61 @@
namespace Icinga\Forms\Config\User;

use Icinga\Application\Hook\ConfigFormEventsHook;
use Icinga\Authentication\PasswordPolicyHelper;
use Icinga\Data\Filter\Filter;
use Icinga\Forms\RepositoryForm;
use Icinga\Web\Notification;

class UserForm extends RepositoryForm
{
/**
* Create and add elements to this form to insert or update a user
* Create and add common elements to this form
*
* @param array $formData The data sent by the user
* @param array $formData The data sent by the user
*
* @return void
*/
protected function createInsertElements(array $formData)
protected function createCommonElements(array $formData): void
{
$this->addElement(
'checkbox',
'is_active',
[
'value' => true,
'label' => $this->translate('Active'),
'description' => $this->translate('Prevents the user from logging in if unchecked')
'value' => true,
'label' => $this->translate('Active'),
'description' => $this->translate('Prevents the user from logging in if unchecked')
]
);
$this->addElement(
'text',
'user_name',
[
'required' => true,
'label' => $this->translate('Username')
'required' => true,
'label' => $this->translate('Username')
]
);
}

/**
* Create and add elements to this form to insert or update a user
*
* @param array $formData The data sent by the user
*
* @return void
*/
protected function createInsertElements(array $formData)
{
$this->createCommonElements($formData);

$this->addElement(
'password',
'password',
[
'required' => true,
'label' => $this->translate('Password')
'required' => true,
'label' => $this->translate('Password')
]
);
PasswordPolicyHelper::apply($this, 'password');

$this->setTitle($this->translate('Add a new user'));
$this->setSubmitLabel($this->translate('Add'));
Expand All @@ -52,11 +69,13 @@ protected function createInsertElements(array $formData)
/**
* Create and add elements to this form to update a user
*
* @param array $formData The data sent by the user
* @param array $formData The data sent by the user
*
* @return void
*/
protected function createUpdateElements(array $formData)
{
$this->createInsertElements($formData);
$this->createCommonElements($formData);

$this->addElement(
'password',
Expand All @@ -66,6 +85,7 @@ protected function createUpdateElements(array $formData)
'label' => $this->translate('Password'),
]
);
PasswordPolicyHelper::apply($this, 'password');

$this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
$this->setSubmitLabel($this->translate('Save'));
Expand Down
111 changes: 111 additions & 0 deletions doc/05-Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,117 @@ resource = icingaweb-mysql
Please read [this chapter](20-Advanced-Topics.md#advanced-topics-authentication-tips-manual-user-database-auth)
in order to manually create users directly inside the database.

### Password Policy <a id="authentication-password-policy"></a>
Comment thread
lippserd marked this conversation as resolved.

Icinga Web supports password policies when using database authentication. You can configure this under
**Configuration > Application > General**.

By default, no password policy is enforced (`None`). Icinga Web provides a built-in policy called `Common` with the
following requirements:

* Minimum length of 12 characters
* At least one number
* At least one special character
* At least one lowercase letter
* At least one uppercase letter

#### Custom Password Policy <a id="authentication-custom-password-policy"></a>

You can create custom password policies by developing a module with a provided hook.

**Create Module Structure**
Comment thread
JolienTrog marked this conversation as resolved.

```bash
mkdir -p /usr/share/icingaweb2/modules/mypasswordpolicy/library/Mypasswordpolicy/ProvidedHook
cd /usr/share/icingaweb2/modules/mypasswordpolicy
```

Create `module.info`:
Comment thread
JolienTrog marked this conversation as resolved.

```ini
Module: mypasswordpolicy
Name: My Password Policy
Comment thread
lippserd marked this conversation as resolved.
Version: 1.0.0
Description: Custom password policy implementation
```

**Implement the Hook**

Icinga Web provides the `PasswordPolicyHook` with predefined methods that simplify the extension of custom password
policies.

Create `library/Mypasswordpolicy/ProvidedHook/PasswordPolicy.php`:

```php
<?php

namespace Icinga\Module\Mypasswordpolicy\ProvidedHook;

use Icinga\Application\Hook\PasswordPolicyHook;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\I18n\Translation;

class MyPasswordPolicy extends PasswordPolicyHook
{
use Translation;

public function getDisplayName(): string
{
return $this->translate('My Custom Policy');
}

public function getName(): string
{
return 'my-custom-policy';
}

public function getDescription(): ?ValidHtml
{
return new Text(
$this->translate('More than 8 chars, at least 1 number, and must differ from the last password'),
);
}

public function validate(string $newPassword, ?string $oldPassword = null): array
{
$violations = [];

if (strlen($newPassword) < 8) {
$violations[] = 'Password must be at least 8 characters';
}

if (! preg_match('/[0-9]/', $newPassword)) {
$violations[] = 'Password must contain at least one number';
}

if ($oldPassword !== null && hash_equals($oldPassword, $newPassword)) {
$violations[] = 'New password must be different from the old password';
}

return $violations;
}
}
```

**Register the Hook**

Create `run.php`:
Comment thread
JolienTrog marked this conversation as resolved.

```php
<?php

use Icinga\Module\Mypasswordpolicy\ProvidedHook\MyPasswordPolicy;
MyPasswordPolicy::register();
```


Enable the module:
Comment thread
JolienTrog marked this conversation as resolved.
```shell
icingacli module enable mypasswordpolicy
```

The custom policy will now appear in **Configuration > Application > General** under Password Policy.

## Groups <a id="authentication-configuration-groups"></a>

Expand Down
14 changes: 9 additions & 5 deletions library/Icinga/Application/ApplicationBootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,18 @@
use DirectoryIterator;
use ErrorException;
use Exception;
use Icinga\Application\ProvidedHook\DbMigration;
use ipl\I18n\GettextTranslator;
use ipl\I18n\StaticTranslator;
use LogicException;
use Icinga\Application\Modules\Manager as ModuleManager;
use Icinga\Application\ProvidedHook\AnyPasswordPolicy;
use Icinga\Application\ProvidedHook\CommonPasswordPolicy;
use Icinga\Application\ProvidedHook\DbMigration;
use Icinga\Authentication\User\UserBackend;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotReadableError;
use ipl\I18n\GettextTranslator;
use ipl\I18n\StaticTranslator;
use LogicException;

/**
* This class bootstraps a thin Icinga application layer
Expand Down Expand Up @@ -742,6 +744,8 @@ public function hasLocales()
protected function registerApplicationHooks(): self
{
DbMigration::register();
CommonPasswordPolicy::register();
AnyPasswordPolicy::register();

return $this;
}
Expand Down
Loading
Loading