Skip to content

Commit

Permalink
EZP-29122: As a developer, I want to configure password policies in e…
Browse files Browse the repository at this point in the history
…zuser (ezsystems#262)

* EZP-29122: As a developer, I want to configure password policies in ezuser Field Definitions

* fixup! EZP-29122: As a developer, I want to configure password policies in ezuser Field Definitions

* fixup! EZP-29122: As a developer, I want to configure password policies in ezuser Field Definitions

* fixup! EZP-29122: As a developer, I want to configure password policies in ezuser Field Definitions

* fixup! EZP-29122: As a developer, I want to configure password policies in ezuser Field Definitions

* EZP-29122: As a developer, I want to configure password policies in ezuser Field Definitions (QA)
  • Loading branch information
adamwojs authored and Łukasz Serwatka committed Dec 13, 2018
1 parent efaac54 commit 23af137
Show file tree
Hide file tree
Showing 21 changed files with 698 additions and 33 deletions.
1 change: 1 addition & 0 deletions bundle/Resources/config/fieldtypes.yml
Expand Up @@ -140,6 +140,7 @@ services:

EzSystems\RepositoryForms\FieldType\Mapper\UserAccountFieldValueFormMapper:
tags:
- { name: ez.fieldFormMapper.definition, fieldType: ezuser }
- { name: ez.fieldFormMapper.value, fieldType: ezuser }

EzSystems\RepositoryForms\FieldType\Mapper\UrlFormMapper:
Expand Down
12 changes: 12 additions & 0 deletions bundle/Resources/config/services.yml
Expand Up @@ -142,6 +142,18 @@ services:
tags:
- { name: validator.constraint_validator, alias: ezrepoforms.validator.unique_url }

EzSystems\RepositoryForms\Validator\Constraints\PasswordValidator:
arguments:
$userService: '@ezpublish.api.service.user'
tags:
- { name: validator.constraint_validator }

EzSystems\RepositoryForms\Validator\Constraints\UserAccountPasswordValidator:
arguments:
$userService: '@ezpublish.api.service.user'
tags:
- { name: validator.constraint_validator }

ezrepoforms.twig.field_edit_rendering_extension:
class: "%ezrepoforms.twig.field_edit_rendering_extension.class%"
arguments: ["@ezpublish.templating.field_block_renderer"]
Expand Down
25 changes: 25 additions & 0 deletions bundle/Resources/translations/ezrepoforms_content_type.en.xlf
Expand Up @@ -451,6 +451,31 @@
<target>Use seconds</target>
<note>key: field_definition.eztime.use_seconds</note>
</trans-unit>
<trans-unit id="39ba847b3d5f6c06e6f42fe1b6c68300972b7cd9" resname="field_definition.ezuser.require_at_least_one_upper_case_character">
<source>Password must contain at least one uppercase letter</source>
<target>Password must contain at least one uppercase letter</target>
<note>key: field_definition.ezuser.require_at_least_one_upper_case_character</note>
</trans-unit>
<trans-unit id="28fc07f891f7d4347c73dbd8d112bf83f7dbd356" resname="field_definition.ezuser.require_at_least_one_lower_case_character">
<source>Password must contain at least one lowercase letter</source>
<target>Password must contain at least one lowercase letter</target>
<note>key: field_definition.ezuser.require_at_least_one_lower_case_character</note>
</trans-unit>
<trans-unit id="e6a7a2c03aeadbc72a6a7d32324a902cda64f0d0" resname="field_definition.ezuser.require_at_least_one_numeric_character">
<source>Password must contain at least one number</source>
<target>Password must contain at least one number</target>
<note>key: field_definition.ezuser.require_at_least_one_numeric_character</note>
</trans-unit>
<trans-unit id="2e7543e8b47d315472e5eababf1d5de80f406c68" resname="field_definition.ezuser.require_at_least_one_non_alphanumeric_character">
<source>Password must contain at least one non-alphanumeric character</source>
<target>Password must contain at least one non-alphanumeric character</target>
<note>key: field_definition.ezuser.require_at_least_one_non_alphanumeric_character</note>
</trans-unit>
<trans-unit id="09f53c8e49ff9cb20bf81f483095af473410937b" resname="field_definition.ezuser.min_length">
<source>Minimum password length</source>
<target>Minimum password length</target>
<note>key: field_definition.ezuser.min_length</note>
</trans-unit>
<trans-unit id="92d9243bb9fac540233dc376b41a3bcd41615ccc" resname="field_definition.field_group">
<source>Category</source>
<target>Category</target>
Expand Down
7 changes: 6 additions & 1 deletion lib/Data/Mapper/UserUpdateMapper.php
Expand Up @@ -47,7 +47,12 @@ public function mapToFormData(User $user, ContentType $contentType, array $param
$this->configureOptions($optionsResolver);
$params = $optionsResolver->resolve($params);

$data = new UserUpdateData(['user' => $user, 'enabled' => $user->enabled]);
$data = new UserUpdateData([
'user' => $user,
'enabled' => $user->enabled,
'contentType' => $contentType,
]);

$fields = $user->getFieldsByLanguage($params['languageCode']);
foreach ($contentType->fieldDefinitions as $fieldDef) {
$field = $fields[$fieldDef->identifier];
Expand Down
13 changes: 8 additions & 5 deletions lib/Data/User/UserUpdateData.php
Expand Up @@ -8,25 +8,28 @@
*/
namespace EzSystems\RepositoryForms\Data\User;

use eZ\Publish\API\Repository\Values\User\User;
use eZ\Publish\API\Repository\Values\User\UserUpdateStruct;
use EzSystems\RepositoryForms\Data\Content\ContentData;
use EzSystems\RepositoryForms\Data\Content\FieldData;
use EzSystems\RepositoryForms\Data\NewnessCheckable;

/**
* @property-read FieldData[] $fieldsData
* @property-read User $user
* @property-read \EzSystems\RepositoryForms\Data\Content\FieldData[] $fieldsData
* @property-read \eZ\Publish\API\Repository\Values\User\User $user
*/
class UserUpdateData extends UserUpdateStruct implements NewnessCheckable
{
use ContentData;

/**
* @var User
* @var \eZ\Publish\API\Repository\Values\User\User
*/
public $user;

/**
* @var \eZ\Publish\API\Repository\Values\ContentType\ContentType
*/
public $contentType;

public function isNew()
{
return false;
Expand Down
44 changes: 43 additions & 1 deletion lib/FieldType/Mapper/UserAccountFieldValueFormMapper.php
Expand Up @@ -10,22 +10,28 @@

use eZ\Publish\Core\FieldType\User\Value as ApiUserValue;
use EzSystems\RepositoryForms\Data\Content\FieldData;
use EzSystems\RepositoryForms\Data\FieldDefinitionData;
use EzSystems\RepositoryForms\Data\User\UserAccountFieldData;
use EzSystems\RepositoryForms\FieldType\FieldDefinitionFormMapperInterface;
use EzSystems\RepositoryForms\FieldType\FieldValueFormMapperInterface;
use EzSystems\RepositoryForms\Form\Type\FieldDefinition\User\PasswordConstraintCheckboxType;
use EzSystems\RepositoryForms\Form\Type\FieldType\UserAccountFieldType;
use EzSystems\RepositoryForms\Validator\Constraints\UserAccountPassword;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\AlreadySubmittedException;
use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Exception\AccessException;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Range;

/**
* Maps a user FieldType.
*/
final class UserAccountFieldValueFormMapper implements FieldValueFormMapperInterface
final class UserAccountFieldValueFormMapper implements FieldValueFormMapperInterface, FieldDefinitionFormMapperInterface
{
/**
* Maps Field form to current FieldType based on the configured form type (self::$formType).
Expand Down Expand Up @@ -54,13 +60,49 @@ public function mapFieldValueForm(FormInterface $fieldForm, FieldData $data)
'required' => true,
'label' => $label,
'intent' => $formIntent,
'constraints' => [
new UserAccountPassword(['contentType' => $rootForm->getData()->contentType]),
],
])
->addModelTransformer($this->getModelTransformer())
->setAutoInitialize(false)
->getForm()
);
}

/**
* {@inheritdoc}
*/
public function mapFieldDefinitionForm(FormInterface $fieldDefinitionForm, FieldDefinitionData $data)
{
$propertyPathPrefix = 'validatorConfiguration[PasswordValueValidator]';

$fieldDefinitionForm->add('requireAtLeastOneUpperCaseCharacter', PasswordConstraintCheckboxType::class, [
'property_path' => $propertyPathPrefix . '[requireAtLeastOneUpperCaseCharacter]',
]);

$fieldDefinitionForm->add('requireAtLeastOneLowerCaseCharacter', PasswordConstraintCheckboxType::class, [
'property_path' => $propertyPathPrefix . '[requireAtLeastOneLowerCaseCharacter]',
]);

$fieldDefinitionForm->add('requireAtLeastOneNumericCharacter', PasswordConstraintCheckboxType::class, [
'property_path' => $propertyPathPrefix . '[requireAtLeastOneNumericCharacter]',
]);

$fieldDefinitionForm->add('requireAtLeastOneNonAlphanumericCharacter', PasswordConstraintCheckboxType::class, [
'property_path' => $propertyPathPrefix . '[requireAtLeastOneNonAlphanumericCharacter]',
]);

$fieldDefinitionForm->add('minLength', IntegerType::class, [
'required' => false,
'property_path' => $propertyPathPrefix . '[minLength]',
'label' => 'field_definition.ezuser.min_length',
'constraints' => [
new Range(['min' => 0, 'max' => 255]),
],
]);
}

/**
* Fake method to set the translation domain for the extractor.
*
Expand Down
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace EzSystems\RepositoryForms\Form\Type\FieldDefinition\User;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PasswordConstraintCheckboxType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new CallbackTransformer('boolval', 'boolval'));
}

/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['label'] = 'field_definition.ezuser.' . $this->toSnakeCase($view->vars['name']);
}

/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'required' => false,
'translation_domain' => 'ezrepoforms_content_type',
]);
}

/**
* {@inheritdoc}
*/
public function getParent(): string
{
return CheckboxType::class;
}

/**
* Converts given $string to the snake case.
*
* @param string $string
*
* @return string
*/
private function toSnakeCase(string $string): string
{
return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($string)));
}
}
31 changes: 13 additions & 18 deletions lib/Validator/Constraints/FieldTypeValidator.php
Expand Up @@ -9,7 +9,7 @@
namespace EzSystems\RepositoryForms\Validator\Constraints;

use eZ\Publish\API\Repository\FieldTypeService;
use eZ\Publish\API\Repository\Values\Translation\Plural;
use EzSystems\RepositoryForms\Validator\ValidationErrorsProcessor;
use Symfony\Component\Validator\ConstraintValidator;

abstract class FieldTypeValidator extends ConstraintValidator
Expand All @@ -29,23 +29,8 @@ public function __construct(FieldTypeService $fieldTypeService)
*/
protected function processValidationErrors(array $validationErrors)
{
if (empty($validationErrors)) {
return;
}

foreach ($validationErrors as $i => $error) {
$message = $error->getTranslatableMessage();
/** @var \Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface $violationBuilder */
$violationBuilder = $this->context->buildViolation($message instanceof Plural ? $message->plural : $message->message);
$violationBuilder->setParameters($message->values);

$propertyPath = $this->generatePropertyPath($i, $error->getTarget());
if ($propertyPath) {
$violationBuilder->atPath($propertyPath);
}

$violationBuilder->addViolation();
}
$validationErrorsProcessor = $this->createValidationErrorProcessor();
$validationErrorsProcessor->processValidationErrors($validationErrors);
}

/**
Expand All @@ -64,4 +49,14 @@ protected function generatePropertyPath($errorIndex, $errorTarget)
{
return '';
}

/**
* @return \EzSystems\RepositoryForms\Validator\ValidationErrorsProcessor
*/
private function createValidationErrorProcessor(): ValidationErrorsProcessor
{
return new ValidationErrorsProcessor($this->context, function ($index, $target) {
return $this->generatePropertyPath($index, $target);
});
}
}
24 changes: 24 additions & 0 deletions lib/Validator/Constraints/Password.php
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace EzSystems\RepositoryForms\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

class Password extends Constraint
{
/** @var string */
public $message = 'ez.user.password.invalid';

/** @var \eZ\Publish\API\Repository\Values\ContentType\ContentType|null */
public $contentType;

/**
* {@inheritdoc}
*/
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}
53 changes: 53 additions & 0 deletions lib/Validator/Constraints/PasswordValidator.php
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace EzSystems\RepositoryForms\Validator\Constraints;

use eZ\Publish\API\Repository\UserService;
use eZ\Publish\API\Repository\Values\User\PasswordValidationContext;
use EzSystems\RepositoryForms\Validator\ValidationErrorsProcessor;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class PasswordValidator extends ConstraintValidator
{
/** @var \eZ\Publish\API\Repository\UserService */
private $userService;

/**
* @param \eZ\Publish\API\Repository\UserService $userService
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}

/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint): void
{
if (!\is_string($value) || empty($value)) {
return;
}

$passwordValidationContext = new PasswordValidationContext([
'contentType' => $constraint->contentType,
]);

$validationErrors = $this->userService->validatePassword($value, $passwordValidationContext);
if (!empty($validationErrors)) {
$validationErrorsProcessor = $this->createValidationErrorsProcessor();
$validationErrorsProcessor->processValidationErrors($validationErrors);
}
}

/**
* @return \EzSystems\RepositoryForms\Validator\ValidationErrorsProcessor
*/
protected function createValidationErrorsProcessor(): ValidationErrorsProcessor
{
return new ValidationErrorsProcessor($this->context);
}
}
9 changes: 9 additions & 0 deletions lib/Validator/Constraints/UserAccountPassword.php
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace EzSystems\RepositoryForms\Validator\Constraints;

class UserAccountPassword extends Password
{
}

0 comments on commit 23af137

Please sign in to comment.