-
Notifications
You must be signed in to change notification settings - Fork 3
IBX-11959: Exposed configured password requirements to the frontend #132
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 6.0
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:jms="urn:jms:translation" version="1.2"> | ||
| <file source-language="en" target-language="en" datatype="plaintext" original="not.available"> | ||
| <header> | ||
| <tool tool-id="JMSTranslationBundle" tool-name="JMSTranslationBundle" tool-version="1.1.0-DEV"/> | ||
| <note>The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message.</note> | ||
| </header> | ||
| <body> | ||
| <trans-unit id="be5d9fef59e98b6cce9bffc63da99e911f47700f" resname="password_requirement.lower_case"> | ||
| <source>At least one lowercase letter</source> | ||
| <target state="new">At least one lowercase letter</target> | ||
| <note>key: password_requirement.lower_case</note> | ||
| </trans-unit> | ||
| <trans-unit id="d7dcd12a831cd755b3628422201b85a6cfdcfaec" resname="password_requirement.min_length"> | ||
| <source>At least %length% characters long</source> | ||
| <target state="new">At least %length% characters long</target> | ||
| <note>key: password_requirement.min_length</note> | ||
| </trans-unit> | ||
| <trans-unit id="da32e36982b0ed40439bf55a8c9a94908f99a877" resname="password_requirement.new_password"> | ||
| <source>Different from your current password</source> | ||
| <target state="new">Different from your current password</target> | ||
| <note>key: password_requirement.new_password</note> | ||
| </trans-unit> | ||
| <trans-unit id="6416aeffea0b83b4ff8d2c3e2b077cb050e1151b" resname="password_requirement.non_alphanumeric"> | ||
| <source>At least one special character</source> | ||
| <target state="new">At least one special character</target> | ||
| <note>key: password_requirement.non_alphanumeric</note> | ||
| </trans-unit> | ||
| <trans-unit id="b6d90391b37d26c9b854d5ca2cf2171d107d0679" resname="password_requirement.not_compromised"> | ||
| <source>Not found in known data breaches</source> | ||
| <target state="new">Not found in known data breaches</target> | ||
| <note>key: password_requirement.not_compromised</note> | ||
| </trans-unit> | ||
| <trans-unit id="7c6c7efd8270a131e81c6ae2026eb34efd7a17bf" resname="password_requirement.numeric"> | ||
| <source>At least one number</source> | ||
| <target state="new">At least one number</target> | ||
| <note>key: password_requirement.numeric</note> | ||
| </trans-unit> | ||
| <trans-unit id="7545e6f371413792c31a8cc3393846d1af3ff075" resname="password_requirement.upper_case"> | ||
| <source>At least one uppercase letter</source> | ||
| <target state="new">At least one uppercase letter</target> | ||
| <note>key: password_requirement.upper_case</note> | ||
| </trans-unit> | ||
| </body> | ||
| </file> | ||
| </xliff> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
| * @license For full copyright and license information view LICENSE file distributed with this source code. | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Ibexa\User\Password; | ||
|
|
||
| final readonly class PasswordRequirement | ||
| { | ||
| public const string MIN_LENGTH = 'min_length'; | ||
| public const string UPPER_CASE = 'upper_case'; | ||
| public const string LOWER_CASE = 'lower_case'; | ||
| public const string NUMERIC = 'numeric'; | ||
| public const string NON_ALPHANUMERIC = 'non_alphanumeric'; | ||
| public const string NEW_PASSWORD = 'new_password'; | ||
| public const string NOT_COMPROMISED = 'not_compromised'; | ||
|
|
||
| private const string TRANSLATION_KEY_PREFIX = 'password_requirement.'; | ||
|
|
||
| /** | ||
| * @param array<string, scalar> $parameters | ||
| */ | ||
| public function __construct( | ||
| private string $identifier, | ||
| private array $parameters = [] | ||
| ) { | ||
| } | ||
|
|
||
| public function getIdentifier(): string | ||
| { | ||
| return $this->identifier; | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, scalar> | ||
| */ | ||
| public function getParameters(): array | ||
| { | ||
| return $this->parameters; | ||
| } | ||
|
|
||
| public function getTranslationKey(): string | ||
| { | ||
| return self::TRANSLATION_KEY_PREFIX . $this->identifier; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Copyright (C) Ibexa AS. All rights reserved. | ||
| * @license For full copyright and license information view LICENSE file distributed with this source code. | ||
| */ | ||
| declare(strict_types=1); | ||
|
|
||
| namespace Ibexa\User\Password; | ||
|
|
||
| use Ibexa\Contracts\Core\Repository\Values\ContentType\ContentType; | ||
| use Ibexa\Core\FieldType\User\Type as UserType; | ||
| use JMS\TranslationBundle\Model\Message; | ||
| use JMS\TranslationBundle\Translation\TranslationContainerInterface; | ||
|
|
||
| final readonly class PasswordRequirementsResolver implements TranslationContainerInterface | ||
| { | ||
| /** | ||
| * Built-in password rules: constraint key in the core PasswordValueValidator | ||
| * schema => requirement identifier and its English label. Adding a rule here | ||
| * is all that is needed — translations are generated from this list. | ||
| */ | ||
| /** Constraint key in the core PasswordValueValidator schema, unlike {@see PasswordRequirement::MIN_LENGTH}. */ | ||
| private const string MIN_LENGTH_CONSTRAINT = 'minLength'; | ||
|
|
||
| private const array RULES = [ | ||
| self::MIN_LENGTH_CONSTRAINT => [PasswordRequirement::MIN_LENGTH, 'At least %length% characters long'], | ||
| 'requireAtLeastOneUpperCaseCharacter' => [PasswordRequirement::UPPER_CASE, 'At least one uppercase letter'], | ||
| 'requireAtLeastOneLowerCaseCharacter' => [PasswordRequirement::LOWER_CASE, 'At least one lowercase letter'], | ||
| 'requireAtLeastOneNumericCharacter' => [PasswordRequirement::NUMERIC, 'At least one number'], | ||
| 'requireAtLeastOneNonAlphanumericCharacter' => [PasswordRequirement::NON_ALPHANUMERIC, 'At least one special character'], | ||
| 'requireNewPassword' => [PasswordRequirement::NEW_PASSWORD, 'Different from your current password'], | ||
| 'requireNotCompromisedPassword' => [PasswordRequirement::NOT_COMPROMISED, 'Not found in known data breaches'], | ||
| ]; | ||
|
|
||
| /** | ||
| * @return \Ibexa\User\Password\PasswordRequirement[] | ||
| */ | ||
| public function getRequirements(ContentType $contentType): array | ||
| { | ||
| $fieldDefinition = $contentType->getFirstFieldDefinitionOfType(UserType::FIELD_TYPE_IDENTIFIER); | ||
| if ($fieldDefinition === null) { | ||
| return []; | ||
| } | ||
|
|
||
| $constraints = $fieldDefinition->getValidatorConfiguration()['PasswordValueValidator'] ?? []; | ||
| $fieldSettings = $fieldDefinition->getFieldSettings(); | ||
|
|
||
| $requirements = []; | ||
| foreach (self::RULES as $constraintKey => [$identifier]) { | ||
| if ($this->isEnabled($constraintKey, $constraints, $fieldSettings)) { | ||
| $requirements[] = new PasswordRequirement($identifier, $this->getParameters($constraintKey, $constraints)); | ||
| } | ||
| } | ||
|
|
||
| return $requirements; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $constraints | ||
| * @param array<string, mixed> $fieldSettings | ||
| */ | ||
| private function isEnabled(string $constraintKey, array $constraints, array $fieldSettings): bool | ||
| { | ||
| return match ($constraintKey) { | ||
| self::MIN_LENGTH_CONSTRAINT => (int)($constraints[self::MIN_LENGTH_CONSTRAINT] ?? 0) > 0, | ||
| // A configured password TTL implies this rule, {@see \Ibexa\Core\FieldType\User\Type::isNewPasswordRequired()} | ||
| 'requireNewPassword' => !empty($constraints['requireNewPassword']) | ||
| || (int)($fieldSettings[UserType::PASSWORD_TTL_SETTING] ?? 0) > 0, | ||
| // Covers boolean on/off flags only; a numeric rule needs its own arm, like minLength above | ||
| default => !empty($constraints[$constraintKey]), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I propose to make more strict check here,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i used !empty() because it behaves the same way as the core code. If requireNewPassword is false, the rule should be disabled. If we use isset(), requireNewPassword: false would still be treated as enabled, which would be incorrect.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My point was, it works now, but if there is a new password rule like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, it follows the current core behavior, but a rule like minSpecialChars = 0 would need its own match case anyway. I’ll add a comment to make that clear |
||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, mixed> $constraints | ||
| * | ||
| * @return array<string, scalar> | ||
| */ | ||
| private function getParameters(string $constraintKey, array $constraints): array | ||
| { | ||
| return $constraintKey === self::MIN_LENGTH_CONSTRAINT | ||
| ? ['%length%' => (int)$constraints[self::MIN_LENGTH_CONSTRAINT]] | ||
| : []; | ||
| } | ||
|
|
||
| /** | ||
| * @return \JMS\TranslationBundle\Model\Message[] | ||
| */ | ||
| public static function getTranslationMessages(): array | ||
| { | ||
| $messages = []; | ||
| foreach (self::RULES as [$identifier, $label]) { | ||
| $messages[] = Message::create( | ||
| (new PasswordRequirement($identifier))->getTranslationKey(), | ||
| 'ibexa_password_requirements' | ||
| )->setDesc($label); | ||
| } | ||
|
|
||
| return $messages; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.