Skip to content

Commit e2a99b2

Browse files
authored
EZP-31462: Added command checking if there are any unsupported password hash types (#99)
* EZP-31462: Added command checking if there are any unsupported password hash types * EZP-31462: Added handling unsupported password hash type exception * EZP-31462: Changed password hash type to default when is not supported during the persistence value creation * fixup! EZP-31462: Fix integration test for case where during the persistence not supported password hash type changed to default
1 parent cddc4a9 commit e2a99b2

File tree

18 files changed

+332
-26
lines changed

18 files changed

+332
-26
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
namespace EzSystems\PlatformInstallerBundle\Command;
8+
9+
use eZ\Publish\Core\FieldType\User\UserStorage;
10+
use Symfony\Component\Console\Command\Command;
11+
use Symfony\Component\Console\Input\InputInterface;
12+
use Symfony\Component\Console\Output\OutputInterface;
13+
14+
final class ValidatePasswordHashesCommand extends Command
15+
{
16+
/** @var \eZ\Publish\Core\FieldType\User\UserStorage */
17+
private $userStorage;
18+
19+
public function __construct(
20+
UserStorage $userStorage
21+
) {
22+
$this->userStorage = $userStorage;
23+
parent::__construct();
24+
}
25+
26+
protected function configure()
27+
{
28+
$this->setName('ezplatform:user:validate-password-hashes');
29+
}
30+
31+
protected function execute(InputInterface $input, OutputInterface $output): int
32+
{
33+
$unsupportedHashesCounter = $this->userStorage->countUsersWithUnsupportedHashType();
34+
35+
if ($unsupportedHashesCounter > 0) {
36+
$output->writeln(sprintf('<error>Found %s users with unsupported password hash types</error>', $unsupportedHashesCounter));
37+
$output->writeln('<info>For more details check documentation:</info> <href=https://doc.ezplatform.com/en/latest/releases/ez_platform_v3.0_deprecations/#password-hashes>https://doc.ezplatform.com/en/latest/releases/ez_platform_v3.0_deprecations/#password-hashes</>');
38+
} else {
39+
$output->writeln('OK - <info>All users have supported password hash types</info>');
40+
}
41+
42+
return Command::SUCCESS;
43+
}
44+
}

eZ/Bundle/PlatformInstallerBundle/src/Resources/config/services.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ services:
2525
$repositoryConfigurationProvider: '@ezpublish.api.repository_configuration_provider'
2626
tags:
2727
- { name: console.command, command: ezplatform:install }
28+
29+
EzSystems\PlatformInstallerBundle\Command\ValidatePasswordHashesCommand:
30+
arguments:
31+
$userStorage: '@ezpublish.fieldType.ezuser.externalStorage'
32+
33+
tags:
34+
- { name: console.command, command: ezplatform:user:validate-password-hashes }
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace eZ\Publish\API\Repository\Exceptions;
10+
11+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
12+
use Throwable;
13+
14+
class PasswordInUnsupportedFormatException extends AuthenticationException
15+
{
16+
public function __construct(Throwable $previous = null)
17+
{
18+
parent::__construct("User's password is in a format which is not supported any more.", 0, $previous);
19+
}
20+
}

eZ/Publish/API/Repository/Tests/FieldType/UserIntegrationTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public function getValidUpdateFieldData()
260260
'login' => 'changeLogin',
261261
'email' => '[email protected]',
262262
'passwordHash' => '*2',
263-
'passwordHashType' => 1,
263+
'passwordHashType' => User::DEFAULT_PASSWORD_HASH,
264264
'enabled' => false,
265265
]
266266
);
@@ -284,7 +284,7 @@ public function assertUpdatedFieldDataLoadedCorrect(Field $field)
284284
'hasStoredLogin' => true,
285285
'login' => 'changeLogin',
286286
'email' => '[email protected]',
287-
'passwordHashType' => 1,
287+
'passwordHashType' => User::DEFAULT_PASSWORD_HASH,
288288
'enabled' => false,
289289
];
290290

eZ/Publish/API/Repository/Tests/UserServiceTest.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use DateTime;
1111
use DateTimeImmutable;
1212
use eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException;
13-
use eZ\Publish\API\Repository\Exceptions\InvalidArgumentException;
1413
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
1514
use eZ\Publish\API\Repository\Values\Content\ContentInfo;
1615
use eZ\Publish\API\Repository\Values\Content\Language;
@@ -2707,7 +2706,7 @@ private function createMultiLanguageUser($userGroupId = 13)
27072706
*
27082707
* @see \eZ\Publish\API\Repository\UserService::createUser()
27092708
*/
2710-
public function testCreateUserInvalidPasswordHashTypeThrowsException()
2709+
public function testCreateUserWithDefaultPasswordHashTypeWhenHashTypeIsUnsupported(): void
27112710
{
27122711
$repository = $this->getRepository();
27132712
$eventUserService = $repository->getUserService();
@@ -2743,12 +2742,11 @@ public function testCreateUserInvalidPasswordHashTypeThrowsException()
27432742
// Set not supported hash type.
27442743
$userValue->passwordHashType = 42424242;
27452744

2746-
$this->expectException(InvalidArgumentException::class);
2747-
$this->expectExceptionMessage("Argument 'hashType' is invalid: Password hash type '42424242' is not recognized");
2748-
27492745
// Create a new user instance.
27502746
// 13 is ID of the "Editors" user group in an eZ Publish demo installation.
2751-
$eventUserService->createUser($createStruct, [$eventUserService->loadUserGroup(13)]);
2747+
$createdUser = $eventUserService->createUser($createStruct, [$eventUserService->loadUserGroup(13)]);
2748+
2749+
self::assertEquals(User::DEFAULT_PASSWORD_HASH, $createdUser->hashAlgorithm);
27522750
}
27532751

27542752
/**

eZ/Publish/API/Repository/Values/User/User.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
*/
2525
abstract class User extends Content implements UserReference
2626
{
27+
public const SUPPORTED_PASSWORD_HASHES = [
28+
self::PASSWORD_HASH_BCRYPT,
29+
self::PASSWORD_HASH_PHP_DEFAULT,
30+
];
31+
2732
/** @var int Passwords in bcrypt */
2833
const PASSWORD_HASH_BCRYPT = 6;
2934

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Integration\User\UserStorage;
10+
11+
use eZ\Publish\Core\FieldType\Tests\Integration\User\UserStorage\UserStorageGatewayTest;
12+
use eZ\Publish\Core\FieldType\User\UserStorage\Gateway as UserStorageGateway;
13+
use eZ\Publish\Core\FieldType\User\UserStorage\Gateway\DoctrineStorage;
14+
15+
final class UserDoctrineStorageGatewayTest extends UserStorageGatewayTest
16+
{
17+
protected function getGateway(): UserStorageGateway
18+
{
19+
return new DoctrineStorage($this->getDatabaseConnection());
20+
}
21+
}

eZ/Publish/Core/FieldType/Tests/Integration/User/UserStorage/UserStorageGatewayTest.php

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@
77
namespace eZ\Publish\Core\FieldType\Tests\Integration\User\UserStorage;
88

99
use eZ\Publish\Core\FieldType\Tests\Integration\BaseCoreFieldTypeIntegrationTest;
10+
use eZ\Publish\Core\FieldType\User\UserStorage\Gateway;
1011
use eZ\Publish\Core\Repository\Values\User\User;
12+
use eZ\Publish\SPI\Tests\Persistence\FixtureImporter;
13+
use eZ\Publish\SPI\Tests\Persistence\YamlFixture;
1114

1215
/**
1316
* User Field Type external storage gateway tests.
1417
*/
1518
abstract class UserStorageGatewayTest extends BaseCoreFieldTypeIntegrationTest
1619
{
17-
/**
18-
* @return \eZ\Publish\Core\FieldType\User\UserStorage\Gateway
19-
*/
20-
abstract protected function getGateway();
20+
abstract protected function getGateway(): Gateway;
2121

22-
/**
23-
* @return array
24-
*/
25-
public function providerForGetFieldData()
22+
public function providerForGetFieldData(): array
2623
{
2724
$expectedUserData = [
2825
10 => [
@@ -59,14 +56,39 @@ public function providerForGetFieldData()
5956

6057
/**
6158
* @dataProvider providerForGetFieldData
62-
*
63-
* @param int|null $fieldId
64-
* @param int $userId
65-
* @param array $expectedUserData
6659
*/
67-
public function testGetFieldData($fieldId, $userId, array $expectedUserData)
60+
public function testGetFieldData(?int $fieldId, ?int $userId, array $expectedUserData): void
6861
{
6962
$data = $this->getGateway()->getFieldData($fieldId, $userId);
70-
$this->assertEquals($expectedUserData, $data);
63+
self::assertEquals($expectedUserData, $data);
64+
}
65+
66+
/**
67+
* @dataProvider getDataForTestCountUsersWithUnsupportedHashType
68+
*/
69+
public function testCountUsersWithUnsupportedHashType(
70+
int $expectedCount,
71+
?string $fixtureFilePath
72+
): void {
73+
if (null !== $fixtureFilePath) {
74+
$importer = new FixtureImporter($this->getDatabaseConnection());
75+
$importer->import(new YamlFixture($fixtureFilePath));
76+
}
77+
78+
$actualCount = $this->getGateway()->countUsersWithUnsupportedHashType();
79+
self::assertEquals($expectedCount, $actualCount);
80+
}
81+
82+
public function getDataForTestCountUsersWithUnsupportedHashType(): iterable
83+
{
84+
yield 'no unsupported hashes' => [
85+
0,
86+
null,
87+
];
88+
89+
yield 'with unsupported hash' => [
90+
1,
91+
__DIR__ . '/_fixtures/unsupported_hash.yaml',
92+
];
7193
}
7294
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ezuser:
2+
- { contentobject_id: 10, email: [email protected], login: anonymous, password_hash: $2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC, password_hash_type: 7 }
3+
- { contentobject_id: 16, email: [email protected], login: test, password_hash: $2y$10$35gOSQs6JK4u4whyERaeUuVeQBi2TUBIZIfP7HEj7sfz.MxvTuOeC, password_hash_type: 5 }
4+
- { contentobject_id: 14, email: [email protected], login: admin, password_hash: $2y$10$FDn9NPwzhq85cLLxfD5Wu.L3SL3Z/LNCvhkltJUV0wcJj7ciJg2oy, password_hash_type: 7 }

eZ/Publish/Core/FieldType/Tests/UserTest.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
use eZ\Publish\Core\FieldType\User\Type;
1313
use eZ\Publish\Core\FieldType\User\Type as UserType;
1414
use eZ\Publish\Core\FieldType\User\Value as UserValue;
15+
use eZ\Publish\Core\Repository\Values\User\User as RepositoryUser;
1516
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
1617
use eZ\Publish\Core\FieldType\ValidationError;
1718
use eZ\Publish\Core\Persistence\Cache\UserHandler;
1819
use eZ\Publish\Core\Repository\User\PasswordHashServiceInterface;
1920
use eZ\Publish\Core\Repository\User\PasswordValidatorInterface;
2021
use eZ\Publish\Core\Repository\Values\ContentType\FieldDefinition as CoreFieldDefinition;
22+
use eZ\Publish\SPI\Persistence\Content\FieldValue;
2123
use eZ\Publish\SPI\Persistence\User;
2224
use PHPUnit\Framework\MockObject\Builder\InvocationMocker;
2325

@@ -579,6 +581,77 @@ public function testEmailAlreadyTaken(): void
579581
], $validationErrors);
580582
}
581583

584+
/**
585+
* @covers \eZ\Publish\Core\FieldType\User\Type::toPersistenceValue
586+
*
587+
* @dataProvider providerForTestCreatePersistenceValue
588+
*/
589+
public function testCreatePersistenceValue(array $userValueDate, array $expectedFieldValueExternalData): void
590+
{
591+
$passwordHashServiceMock = $this->createMock(PasswordHashServiceInterface::class);
592+
$passwordHashServiceMock->method('getDefaultHashType')->willReturn(RepositoryUser::DEFAULT_PASSWORD_HASH);
593+
$userType = new UserType(
594+
$this->createMock(UserHandler::class),
595+
$passwordHashServiceMock,
596+
$this->createMock(PasswordValidatorInterface::class)
597+
);
598+
599+
$value = new UserValue($userValueDate);
600+
$fieldValue = $userType->toPersistenceValue($value);
601+
602+
$expected = new FieldValue(
603+
[
604+
'data' => null,
605+
'externalData' => $expectedFieldValueExternalData,
606+
'sortKey' => null,
607+
]);
608+
self::assertEquals($expected, $fieldValue);
609+
}
610+
611+
public function providerForTestCreatePersistenceValue(): iterable
612+
{
613+
$passwordUpdatedAt = new DateTimeImmutable();
614+
$userData = [
615+
'hasStoredLogin' => false,
616+
'contentId' => 46,
617+
'login' => 'validate_user',
618+
'email' => '[email protected]',
619+
'passwordHash' => '1234567890abcdef',
620+
'enabled' => true,
621+
'maxLogin' => 1000,
622+
'plainPassword' => '',
623+
'passwordUpdatedAt' => $passwordUpdatedAt,
624+
];
625+
626+
yield 'when password hash type is given' => [
627+
$userValueData = [
628+
'passwordHashType' => RepositoryUser::PASSWORD_HASH_PHP_DEFAULT,
629+
] + $userData,
630+
$expectedFieldValueExternalData = [
631+
'passwordHashType' => RepositoryUser::PASSWORD_HASH_PHP_DEFAULT,
632+
'passwordUpdatedAt' => $passwordUpdatedAt->getTimestamp(),
633+
] + $userData,
634+
];
635+
yield 'when password hash type is null' => [
636+
$userValueData = [
637+
'passwordHashType' => null,
638+
] + $userData,
639+
$expectedFieldValueExternalData = [
640+
'passwordHashType' => RepositoryUser::DEFAULT_PASSWORD_HASH,
641+
'passwordUpdatedAt' => $passwordUpdatedAt->getTimestamp(),
642+
] + $userData,
643+
];
644+
yield 'when password hash type is unsupported' => [
645+
$userValueData = [
646+
'passwordHashType' => '__UNSUPPORTED_HASH_TYPE__',
647+
] + $userData,
648+
$expectedFieldValueExternalData = [
649+
'passwordHashType' => RepositoryUser::DEFAULT_PASSWORD_HASH,
650+
'passwordUpdatedAt' => $passwordUpdatedAt->getTimestamp(),
651+
] + $userData,
652+
];
653+
}
654+
582655
public function testEmailFreeToUse(): void
583656
{
584657
$validateUserValue = new UserValue([

eZ/Publish/Core/FieldType/User/Type.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use DateTimeInterface;
1111
use eZ\Publish\Core\FieldType\FieldType;
1212
use eZ\Publish\Core\FieldType\ValidationError;
13+
use eZ\Publish\Core\Repository\Values\User\User;
1314
use eZ\Publish\SPI\Persistence\User\Handler as SPIUserHandler;
1415
use eZ\Publish\Core\Repository\User\PasswordHashServiceInterface;
1516
use eZ\Publish\Core\Repository\User\PasswordValidatorInterface;
@@ -251,7 +252,7 @@ public function toHash(SPIValue $value)
251252
*/
252253
public function toPersistenceValue(SPIValue $value)
253254
{
254-
$value->passwordHashType = $value->passwordHashType ?? $this->passwordHashService->getDefaultHashType();
255+
$value->passwordHashType = $this->getPasswordHashTypeForPersistenceValue($value);
255256
if ($value->plainPassword) {
256257
$value->passwordHash = $this->passwordHashService->createPasswordHash(
257258
$value->plainPassword,
@@ -269,6 +270,19 @@ public function toPersistenceValue(SPIValue $value)
269270
);
270271
}
271272

273+
private function getPasswordHashTypeForPersistenceValue(SPIValue $value): int
274+
{
275+
if (null === $value->passwordHashType) {
276+
return $this->passwordHashService->getDefaultHashType();
277+
}
278+
279+
if (!in_array($value->passwordHashType, User::SUPPORTED_PASSWORD_HASHES, true)) {
280+
return $this->passwordHashService->getDefaultHashType();
281+
}
282+
283+
return $value->passwordHashType;
284+
}
285+
272286
/**
273287
* Converts a persistence $fieldValue to a Value.
274288
*

eZ/Publish/Core/FieldType/User/UserStorage.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,9 @@ public function hasFieldData()
112112
public function getIndexData(VersionInfo $versionInfo, Field $field, array $context)
113113
{
114114
}
115+
116+
public function countUsersWithUnsupportedHashType(): int
117+
{
118+
return $this->gateway->countUsersWithUnsupportedHashType();
119+
}
115120
}

eZ/Publish/Core/FieldType/User/UserStorage/Gateway.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,6 @@ abstract public function storeFieldData(VersionInfo $versionInfo, Field $field):
4949
* @throws \Doctrine\DBAL\DBALException
5050
*/
5151
abstract public function deleteFieldData(VersionInfo $versionInfo, array $fieldIds): bool;
52+
53+
abstract public function countUsersWithUnsupportedHashType(): int;
5254
}

0 commit comments

Comments
 (0)