Navigation Menu

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 (#2482)

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

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

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

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

* fixup! EZP-29122: As a developer, I want to configure password policies in ezuser
  • Loading branch information
adamwojs authored and Łukasz Serwatka committed Dec 13, 2018
1 parent e4e2cfc commit 7d2c125
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 2 deletions.
35 changes: 33 additions & 2 deletions Repository/Tests/FieldType/UserIntegrationTest.php
Expand Up @@ -69,7 +69,30 @@ public function getInvalidFieldSettings()
*/
public function getValidatorSchema()
{
return array();
return array(
'PasswordValueValidator' => array(
'requireAtLeastOneUpperCaseCharacter' => array(
'type' => 'int',
'default' => null,
),
'requireAtLeastOneLowerCaseCharacter' => array(
'type' => 'int',
'default' => null,
),
'requireAtLeastOneNumericCharacter' => array(
'type' => 'int',
'default' => null,
),
'requireAtLeastOneNonAlphanumericCharacter' => array(
'type' => 'int',
'default' => null,
),
'minLength' => array(
'type' => 'int',
'default' => null,
),
),
);
}

/**
Expand All @@ -79,7 +102,15 @@ public function getValidatorSchema()
*/
public function getValidValidatorConfiguration()
{
return array();
return array(
'PasswordValueValidator' => array(
'requireAtLeastOneUpperCaseCharacter' => false,
'requireAtLeastOneLowerCaseCharacter' => false,
'requireAtLeastOneNumericCharacter' => false,
'requireAtLeastOneNonAlphanumericCharacter' => false,
'minLength' => null,
),
);
}

/**
Expand Down
282 changes: 282 additions & 0 deletions Repository/Tests/UserServiceTest.php
Expand Up @@ -13,10 +13,13 @@
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
use eZ\Publish\API\Repository\Values\Content\VersionInfo as APIVersionInfo;
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
use eZ\Publish\API\Repository\Values\User\PasswordValidationContext;
use eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct;
use eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct;
use eZ\Publish\API\Repository\Values\User\UserUpdateStruct;
use eZ\Publish\API\Repository\Values\User\User;
use eZ\Publish\Core\FieldType\ValidationError;
use eZ\Publish\Core\Repository\Values\Content\Content;
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
use eZ\Publish\Core\Repository\Values\User\UserGroup;
Expand Down Expand Up @@ -1066,6 +1069,40 @@ public function testCreateUserThrowsNotFoundException()
$userService->createUser($userCreateStruct, [$parentGroup]);
}

/**
* Test creating a user throwing UserPasswordValidationException when password doesn't follow specific rules.
*
* @expectedException \eZ\Publish\Core\Base\Exceptions\UserPasswordValidationException
* @expectedExceptionMessage Argument 'password' is invalid: Password doesn't match the fallowing rules: User password must be at least 8 characters long, User password must include at least one upper case letter, User password must include at least one number, User password must include at least one special character
* @covers \eZ\Publish\API\Repository\UserService::createUser
*/
public function testCreateUserWithWeakPasswordThrowsUserPasswordValidationException()
{
$userContentType = $this->createUserWithStrongPasswordContentType();

/* BEGIN: Use Case */
// This call will fail with a "UserPasswordValidationException" because the
// the password does not follow specified rules.
$this->createUserWithPassword('pass', $userContentType);
/* END: Use Case */
}

/**
* Opposite test case for testCreateUserWithWeakPasswordThrowsUserPasswordValidationException.
*
* @covers \eZ\Publish\API\Repository\UserService::createUser
*/
public function testCreateUserWithStrongPassword()
{
$userContentType = $this->createUserWithStrongPasswordContentType();

/* BEGIN: Use Case */
$user = $this->createUserWithPassword('H@xxi0r!', $userContentType);
/* END: Use Case */

$this->assertInstanceOf(User::class, $user);
}

/**
* Test for the loadUser() method.
*
Expand Down Expand Up @@ -1811,6 +1848,52 @@ public function testUpdateUserThrowsInvalidArgumentExceptionOnFieldTypeNotAccept
/* END: Use Case */
}

/**
* Test updating a user throwing UserPasswordValidationException when password doesn't follow specified rules.
*
* @expectedException \eZ\Publish\Core\Base\Exceptions\UserPasswordValidationException
* @expectedExceptionMessage Argument 'password' is invalid: Password doesn't match the fallowing rules: User password must be at least 8 characters long, User password must include at least one upper case letter, User password must include at least one number, User password must include at least one special character
* @covers \eZ\Publish\API\Repository\UserService::updateUser
*/
public function testUpdateUserWithWeakPasswordThrowsUserPasswordValidationException()
{
$userService = $this->getRepository()->getUserService();

$user = $this->createUserWithPassword('H@xxxiR!_1', $this->createUserWithStrongPasswordContentType());

/* BEGIN: Use Case */
// Create a new update struct instance
$userUpdate = $userService->newUserUpdateStruct();
$userUpdate->password = 'pass';

// This call will fail with a "UserPasswordValidationException" because the
// the password does not follow specified rules
$userService->updateUser($user, $userUpdate);
/* END: Use Case */
}

/**
* Opposite test case for testUpdateUserWithWeakPasswordThrowsUserPasswordValidationException.
*
* @covers \eZ\Publish\API\Repository\UserService::updateUser
*/
public function testUpdateUserWithStrongPassword()
{
$userService = $this->getRepository()->getUserService();

$user = $this->createUserWithPassword('H@xxxiR!_1', $this->createUserWithStrongPasswordContentType());

/* BEGIN: Use Case */
// Create a new update struct instance
$userUpdate = $userService->newUserUpdateStruct();
$userUpdate->password = 'H@xxxiR!_2';

$user = $userService->updateUser($user, $userUpdate);
/* END: Use Case */

$this->assertInstanceOf(User::class, $user);
}

/**
* Test for the loadUserGroupsOfUser() method.
*
Expand Down Expand Up @@ -2666,4 +2749,203 @@ public function testExpireUserToken($userToken)
// should throw NotFoundException now
$userService->loadUserByToken($userToken);
}

/**
* @covers \eZ\Publish\API\Repository\UserService::validatePassword()
*/
public function testValidatePasswordWithDefaultContext()
{
$userService = $this->getRepository()->getUserService();

/* BEGIN: Use Case */
$errors = $userService->validatePassword('pass');
/* END: Use Case */

$this->assertEmpty($errors);
}

/**
* @covers \eZ\Publish\API\Repository\UserService::validatePassword()
* @dataProvider dataProviderForValidatePassword
*/
public function testValidatePassword(string $password, array $expectedErrorr)
{
$userService = $this->getRepository()->getUserService();
$contentType = $this->createUserWithStrongPasswordContentType();

/* BEGIN: Use Case */
$context = new PasswordValidationContext(array(
'contentType' => $contentType,
));

$actualErrors = $userService->validatePassword($password, $context);
/* END: Use Case */

$this->assertEquals($expectedErrorr, $actualErrors);
}

/**
* Data provider for testValidatePassword.
*
* @return array
*/
public function dataProviderForValidatePassword(): array
{
return array(
array(
'pass',
array(
new ValidationError('User password must be at least %length% characters long', null, array(
'%length%' => 8,
), 'password'),
new ValidationError('User password must include at least one upper case letter', null, array(), 'password'),
new ValidationError('User password must include at least one number', null, array(), 'password'),
new ValidationError('User password must include at least one special character', null, array(), 'password'),
),
),
array(
'H@xxxi0R!!!',
array(),
),
);
}

/**
* Creates a user with given password.
*
* @param string $password
* @param \eZ\Publish\API\Repository\Values\ContentType\ContentType $contentType
*
* @return \eZ\Publish\API\Repository\Values\User\User
*/
private function createUserWithPassword(string $password, ContentType $contentType): User
{
$userService = $this->getRepository()->getUserService();
// ID of the "Editors" user group in an eZ Publish demo installation
$editorsGroupId = 13;

// Instantiate a create struct with mandatory properties
$userCreate = $userService->newUserCreateStruct(
'johndoe',
'johndoe@example.com',
$password,
'eng-US',
$contentType
);
$userCreate->enabled = true;
$userCreate->setField('first_name', 'John');
$userCreate->setField('last_name', 'Doe');

return $userService->createUser($userCreate, array(
$userService->loadUserGroup($editorsGroupId),
));
}

/**
* Creates the User Content Type with password constraints.
*
* @return \eZ\Publish\API\Repository\Values\ContentType\ContentType
*/
private function createUserWithStrongPasswordContentType(): ContentType
{
$repository = $this->getRepository();

$contentTypeService = $repository->getContentTypeService();

$typeCreate = $contentTypeService->newContentTypeCreateStruct('user-with-strong-password');
$typeCreate->mainLanguageCode = 'eng-GB';
$typeCreate->remoteId = '384b94a1bd6bc06826410e284dd9684887bf56fc';
$typeCreate->urlAliasSchema = 'url|scheme';
$typeCreate->nameSchema = 'name|scheme';
$typeCreate->names = array(
'eng-GB' => 'User with strong password',
);
$typeCreate->descriptions = array(
'eng-GB' => '',
);
$typeCreate->creatorId = $this->generateId('user', $repository->getCurrentUser()->id);
$typeCreate->creationDate = $this->createDateTime();

$firstNameFieldCreate = $contentTypeService->newFieldDefinitionCreateStruct('first_name', 'ezstring');
$firstNameFieldCreate->names = array(
'eng-GB' => 'First name',
);
$firstNameFieldCreate->descriptions = array(
'eng-GB' => '',
);
$firstNameFieldCreate->fieldGroup = 'default';
$firstNameFieldCreate->position = 1;
$firstNameFieldCreate->isTranslatable = false;
$firstNameFieldCreate->isRequired = true;
$firstNameFieldCreate->isInfoCollector = false;
$firstNameFieldCreate->validatorConfiguration = array(
'StringLengthValidator' => array(
'minStringLength' => 0,
'maxStringLength' => 0,
),
);
$firstNameFieldCreate->fieldSettings = array();
$firstNameFieldCreate->isSearchable = true;
$firstNameFieldCreate->defaultValue = '';

$typeCreate->addFieldDefinition($firstNameFieldCreate);

$lastNameFieldCreate = $contentTypeService->newFieldDefinitionCreateStruct('last_name', 'ezstring');
$lastNameFieldCreate->names = array(
'eng-GB' => 'Last name',
);
$lastNameFieldCreate->descriptions = array(
'eng-GB' => '',
);
$lastNameFieldCreate->fieldGroup = 'default';
$lastNameFieldCreate->position = 2;
$lastNameFieldCreate->isTranslatable = false;
$lastNameFieldCreate->isRequired = true;
$lastNameFieldCreate->isInfoCollector = false;
$lastNameFieldCreate->validatorConfiguration = array(
'StringLengthValidator' => array(
'minStringLength' => 0,
'maxStringLength' => 0,
),
);
$lastNameFieldCreate->fieldSettings = array();
$lastNameFieldCreate->isSearchable = true;
$lastNameFieldCreate->defaultValue = '';

$typeCreate->addFieldDefinition($lastNameFieldCreate);

$accountFieldCreate = $contentTypeService->newFieldDefinitionCreateStruct('account', 'ezuser');
$accountFieldCreate->names = array(
'eng-GB' => 'User account',
);
$accountFieldCreate->descriptions = array(
'eng-GB' => '',
);
$accountFieldCreate->fieldGroup = 'default';
$accountFieldCreate->position = 3;
$accountFieldCreate->isTranslatable = false;
$accountFieldCreate->isRequired = true;
$accountFieldCreate->isInfoCollector = false;
$accountFieldCreate->validatorConfiguration = array(
'PasswordValueValidator' => array(
'requireAtLeastOneUpperCaseCharacter' => 1,
'requireAtLeastOneLowerCaseCharacter' => 1,
'requireAtLeastOneNumericCharacter' => 1,
'requireAtLeastOneNonAlphanumericCharacter' => 1,
'minLength' => 8,
),
);
$accountFieldCreate->fieldSettings = array();
$accountFieldCreate->isSearchable = false;
$accountFieldCreate->defaultValue = null;

$typeCreate->addFieldDefinition($accountFieldCreate);

$contentTypeDraft = $contentTypeService->createContentType($typeCreate, array(
$contentTypeService->loadContentTypeGroupByIdentifier('Users'),
));
$contentTypeService->publishContentTypeDraft($contentTypeDraft);

return $contentTypeService->loadContentTypeByIdentifier('user-with-strong-password');
}
}
13 changes: 13 additions & 0 deletions Repository/UserService.php
Expand Up @@ -8,6 +8,7 @@
*/
namespace eZ\Publish\API\Repository;

use eZ\Publish\API\Repository\Values\User\PasswordValidationContext;
use eZ\Publish\API\Repository\Values\User\UserTokenUpdateStruct;
use eZ\Publish\API\Repository\Values\User\UserCreateStruct;
use eZ\Publish\API\Repository\Values\User\UserUpdateStruct;
Expand Down Expand Up @@ -340,4 +341,16 @@ public function newUserUpdateStruct();
* @return \eZ\Publish\API\Repository\Values\User\UserGroupUpdateStruct
*/
public function newUserGroupUpdateStruct();

/**
* Validates given password.
*
* @param string $password
* @param \eZ\Publish\API\Repository\Values\User\PasswordValidationContext|null $context
*
* @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
*
* @return \eZ\Publish\SPI\FieldType\ValidationError[]
*/
public function validatePassword(string $password, PasswordValidationContext $context = null): array;
}
8 changes: 8 additions & 0 deletions Repository/Values/Translation/Message.php
Expand Up @@ -45,4 +45,12 @@ public function __construct($message, array $values = array())
$this->message = $message;
$this->values = $values;
}

/**
* {@inheritdoc}
*/
public function __toString()
{
return strtr($this->message, $this->values);
}
}

0 comments on commit 7d2c125

Please sign in to comment.