From e44627bd2bf02c4e1e214f29b960eeda224a43a3 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:05:31 +0200 Subject: [PATCH 01/28] IBX-10124: Added basic support for Argon2I(D) password hashes --- .../Configuration/Parser/PasswordHash.php | 72 +++++++++++++++++++ src/bundle/Core/IbexaCoreBundle.php | 1 + .../Resources/config/default_settings.yml | 4 ++ .../PasswordInUnsupportedFormatException.php | 2 +- .../Repository/PasswordHashService.php | 19 +++++ src/contracts/Repository/Values/User/User.php | 6 ++ ...RepositoryUserAuthenticationSubscriber.php | 3 +- .../Exception/PasswordHashTypeNotCompiled.php | 22 ++++++ .../Repository/User/PasswordHashService.php | 50 ++++++++++--- src/lib/Repository/UserService.php | 39 ++++++++-- .../Resources/settings/fieldtype_services.yml | 4 +- src/lib/Resources/settings/settings.yml | 4 ++ .../Core/Repository/UserServiceTest.php | 32 ++++++++- .../User/PasswordHashServiceTest.php | 2 + 14 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php create mode 100644 src/lib/Repository/User/Exception/PasswordHashTypeNotCompiled.php diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php new file mode 100644 index 0000000000..f946d60908 --- /dev/null +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -0,0 +1,72 @@ + + */ + public function addSemanticConfig(NodeBuilder $nodeBuilder): void + { + $nodeBuilder + ->arrayNode('password_hash') + ->info('Password hash options') + ->children() + ->integerNode('default_type') + ->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.') + ->example('7') + ->end() + ->booleanNode('update_type_on_change') + ->info('Whether the password hash type should be changed when the password is changed if it differs from the default type.') + ->example('false') + ->end() + ->end() + ->end(); + } + + /** + * @param array $scopeSettings + */ + public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerInterface $contextualizer): void + { + if (!isset($scopeSettings['password_hash'])) { + return; + } + + $settings = $scopeSettings['password_hash']; + if (isset($settings['default_type'])) { + $contextualizer->setContextualParameter('password_hash.default_type', $currentScope, $settings['default_type']); + } + if (isset($settings['update_type_on_change'])) { + $contextualizer->setContextualParameter('password_hash.update_type_on_change', $currentScope, $settings['update_type_on_change']); + } + } +} diff --git a/src/bundle/Core/IbexaCoreBundle.php b/src/bundle/Core/IbexaCoreBundle.php index 3f0e76f71a..02e0a74263 100644 --- a/src/bundle/Core/IbexaCoreBundle.php +++ b/src/bundle/Core/IbexaCoreBundle.php @@ -123,6 +123,7 @@ public function getContainerExtension(): ExtensionInterface new ConfigParser\UrlChecker(), new ConfigParser\TwigVariablesParser(), new ConfigParser\UserContentTypeIdentifier(), + new ConfigParser\PasswordHash(), ], [ new RepositoryConfigParser\Storage(), diff --git a/src/bundle/Core/Resources/config/default_settings.yml b/src/bundle/Core/Resources/config/default_settings.yml index 19c1bc175a..620a4f6326 100644 --- a/src/bundle/Core/Resources/config/default_settings.yml +++ b/src/bundle/Core/Resources/config/default_settings.yml @@ -99,6 +99,10 @@ parameters: ibexa.site_access.config.default.users_group_root_subtree_path: '/1/5' ibexa.site_access.config.default.api_keys: {} # Google Maps APIs v3 key (https://developers.google.com/maps/documentation/javascript/get-api-key) + # Password hash + ibexa.site_access.config.default.password_hash.default_type: 7 # Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User + ibexa.site_access.config.default.password_hash.update_type_on_change: false # Whether the password hash type should be changed when the password is changed if it differs from the default hash type + # IO ibexa.site_access.config.default.io.metadata_handler: "default" ibexa.site_access.config.default.io.binarydata_handler: "default" diff --git a/src/contracts/Repository/Exceptions/PasswordInUnsupportedFormatException.php b/src/contracts/Repository/Exceptions/PasswordInUnsupportedFormatException.php index d11db4e1f0..dcaa9bb415 100644 --- a/src/contracts/Repository/Exceptions/PasswordInUnsupportedFormatException.php +++ b/src/contracts/Repository/Exceptions/PasswordInUnsupportedFormatException.php @@ -15,6 +15,6 @@ class PasswordInUnsupportedFormatException extends AuthenticationException { public function __construct(?Throwable $previous = null) { - parent::__construct("User's password is in a format which is not supported any more.", 0, $previous); + parent::__construct("User's password is in a format which is not supported.", 0, $previous); } } diff --git a/src/contracts/Repository/PasswordHashService.php b/src/contracts/Repository/PasswordHashService.php index a2f74da9eb..a8afbfd7b7 100644 --- a/src/contracts/Repository/PasswordHashService.php +++ b/src/contracts/Repository/PasswordHashService.php @@ -8,8 +8,17 @@ namespace Ibexa\Contracts\Core\Repository; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; + interface PasswordHashService { + /** + * Sets the ConfigResolver instance. + * + * @param \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface $configResolver + */ + public function setConfigResolver(ConfigResolverInterface $configResolver): void; + /** * Returns default password hash type. * @@ -33,6 +42,9 @@ public function isHashTypeSupported(int $hashType): bool; * Create hash from given plain password. * * If non-provided, the default password hash type will be used. + * + * @throws \Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled + * @throws \Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType */ public function createPasswordHash(string $plainPassword, ?int $hashType = null): string; @@ -42,4 +54,11 @@ public function createPasswordHash(string $plainPassword, ?int $hashType = null) * If non-provided, the default password hash type will be used. */ public function isValidPassword(string $plainPassword, string $passwordHash, ?int $hashType = null): bool; + + /** + * Returns true if password hash type should be updated when the user changes password. + * + * @return bool + */ + public function updatePasswordHashTypeOnChange(): bool; } diff --git a/src/contracts/Repository/Values/User/User.php b/src/contracts/Repository/Values/User/User.php index 07457de91c..92c119e5f1 100644 --- a/src/contracts/Repository/Values/User/User.php +++ b/src/contracts/Repository/Values/User/User.php @@ -28,6 +28,8 @@ abstract class User extends Content implements UserReference public const array SUPPORTED_PASSWORD_HASHES = [ self::PASSWORD_HASH_BCRYPT, self::PASSWORD_HASH_PHP_DEFAULT, + self::PASSWORD_HASH_ARGON2I, + self::PASSWORD_HASH_ARGON2ID, self::PASSWORD_HASH_INVALID, ]; @@ -35,6 +37,10 @@ abstract class User extends Content implements UserReference public const int PASSWORD_HASH_PHP_DEFAULT = 7; + public const int PASSWORD_HASH_ARGON2I = 8; + + public const int PASSWORD_HASH_ARGON2ID = 9; + public const int PASSWORD_HASH_INVALID = 256; public const int DEFAULT_PASSWORD_HASH = self::PASSWORD_HASH_PHP_DEFAULT; diff --git a/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php b/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php index 707578fbe3..b49b0a72e2 100644 --- a/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php +++ b/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/RepositoryUserAuthenticationSubscriber.php @@ -12,6 +12,7 @@ use Ibexa\Contracts\Core\Repository\Exceptions\PasswordInUnsupportedFormatException; use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Core\MVC\Symfony\Security\UserInterface as IbexaUserInterface; +use Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled; use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType; use Psr\Log\LoggerAwareTrait; use Psr\Log\LoggerInterface; @@ -77,7 +78,7 @@ public function validateRepositoryUser(CheckPassportEvent $event): void $user->getAPIUser(), $user->getPassword() ?? '' ); - } catch (UnsupportedPasswordHashType $exception) { + } catch (UnsupportedPasswordHashType|PasswordHashTypeNotCompiled $exception) { $this->sleepUsingConstantTimer($startTime); throw new PasswordInUnsupportedFormatException($exception); diff --git a/src/lib/Repository/User/Exception/PasswordHashTypeNotCompiled.php b/src/lib/Repository/User/Exception/PasswordHashTypeNotCompiled.php new file mode 100644 index 0000000000..bd7a5f092f --- /dev/null +++ b/src/lib/Repository/User/Exception/PasswordHashTypeNotCompiled.php @@ -0,0 +1,22 @@ +defaultHashType = $hashType; } + public function setConfigResolver(ConfigResolverInterface $configResolver): void + { + $this->configResolver = $configResolver; + $this->defaultHashType = $this->configResolver->getParameter('password_hash.default_type'); + } + public function getSupportedHashTypes(): array { return User::SUPPORTED_PASSWORD_HASHES; @@ -39,26 +50,37 @@ public function getDefaultHashType(): int return $this->defaultHashType; } - /** - * @throws \Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType - */ public function createPasswordHash( #[\SensitiveParameter] - string $password, + string $plainPassword, ?int $hashType = null ): string { $hashType = $hashType ?? $this->defaultHashType; switch ($hashType) { case User::PASSWORD_HASH_BCRYPT: - return password_hash($password, PASSWORD_BCRYPT); + return password_hash($plainPassword, PASSWORD_BCRYPT); case User::PASSWORD_HASH_PHP_DEFAULT: - return password_hash($password, PASSWORD_DEFAULT); + return password_hash($plainPassword, PASSWORD_DEFAULT); case User::PASSWORD_HASH_INVALID: return ''; + case User::PASSWORD_HASH_ARGON2I: + if (!defined('PASSWORD_ARGON2I')) { + throw new PasswordHashTypeNotCompiled('PASSWORD_ARGON2I'); + } + + return password_hash($plainPassword, PASSWORD_ARGON2I); + + case User::PASSWORD_HASH_ARGON2ID: + if (!defined('PASSWORD_ARGON2ID')) { + throw new PasswordHashTypeNotCompiled('PASSWORD_ARGON2ID'); + } + + return password_hash($plainPassword, PASSWORD_ARGON2ID); + default: throw new UnsupportedPasswordHashType($hashType); } @@ -74,12 +96,24 @@ public function isValidPassword( if ( $hashType === User::PASSWORD_HASH_BCRYPT || $hashType === User::PASSWORD_HASH_PHP_DEFAULT + || $hashType === User::PASSWORD_HASH_ARGON2I + || $hashType === User::PASSWORD_HASH_ARGON2ID || $hashType === User::PASSWORD_HASH_INVALID ) { - // In case of bcrypt let PHP's password functionality do its magic + // Let PHP's password functionality do its magic return password_verify($plainPassword, $passwordHash); } - return $passwordHash === $this->createPasswordHash($plainPassword, $hashType); + try { + return $passwordHash === $this->createPasswordHash($plainPassword, $hashType); + } catch (PasswordHashTypeNotCompiled|UnsupportedPasswordHashType $e) { + // If the hash type is not compiled or unsupported we can't verify the password so it's not valid + return false; + } + } + + public function updatePasswordHashTypeOnChange(): bool + { + return $this->configResolver->getParameter('password_hash.update_type_on_change'); } } diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index 85c2d2050c..b92c536ed8 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -49,6 +49,7 @@ use Ibexa\Core\Base\Exceptions\MissingUserFieldTypeException; use Ibexa\Core\Base\Exceptions\UnauthorizedException; use Ibexa\Core\FieldType\User\Value as UserValue; +use Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled; use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType; use Ibexa\Core\Repository\User\PasswordValidatorInterface; use Ibexa\Core\Repository\Values\User\User; @@ -56,6 +57,7 @@ use Ibexa\Core\Repository\Values\User\UserGroup; use Ibexa\Core\Repository\Values\User\UserGroupCreateStruct; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; /** * This service provides methods for managing users and user groups. @@ -778,14 +780,39 @@ public function updateUserPassword( ); } - $passwordHashAlgorithm = (int) $loadedUser->hashAlgorithm; - try { - $passwordHash = $this->passwordHashService->createPasswordHash($newPassword, $passwordHashAlgorithm); - } catch (UnsupportedPasswordHashType $e) { - $passwordHashAlgorithm = $this->passwordHashService->getDefaultHashType(); - $passwordHash = $this->passwordHashService->createPasswordHash($newPassword, $passwordHashAlgorithm); + $defaultPasswordHashAlgorithm = $this->passwordHashService->getDefaultHashType(); + $userCurrentPasswordHashAlgorithm = (int) $loadedUser->hashAlgorithm; + if ($this->passwordHashService->updatePasswordHashTypeOnChange()) { + $passwordHashAlgorithm = $defaultPasswordHashAlgorithm; + } else { + $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; } + $passwordHash = null; + do { + try { + $passwordHash = $this->passwordHashService->createPasswordHash($newPassword, $passwordHashAlgorithm); + } catch (UnsupportedPasswordHashType|PasswordHashTypeNotCompiled $e) { + if (isset($this->logger)) { + $this->logger->log(LogLevel::WARNING, $e->getMessage(), [ + 'exception' => $e, + ]); + } + + if ($passwordHashAlgorithm !== $userCurrentPasswordHashAlgorithm) { + // If we're trying to upgrade the password hash algorithm but the upgrade failed, + // we fall back to the user's current password hash algorithm. + $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; + } else { + // If the user's current password hash algorithm fails, we can't proceed. + throw new InvalidArgumentException( + 'passwordHashAlgorithm', + 'The password hash algorithm is not supported or not compiled.' + ); + } + } + } while ($passwordHash === null); + $this->repository->beginTransaction(); try { $this->userHandler->updatePassword( diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index c31dd758a6..92dd324d51 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -60,7 +60,9 @@ services: Ibexa\Core\FieldType\FieldTypeRegistry: ~ - Ibexa\Core\Repository\User\PasswordHashService: ~ + Ibexa\Core\Repository\User\PasswordHashService: + calls: + - [ setConfigResolver, [ '@ibexa.config.resolver' ] ] Ibexa\Contracts\Core\Repository\PasswordHashService: alias: Ibexa\Core\Repository\User\PasswordHashService diff --git a/src/lib/Resources/settings/settings.yml b/src/lib/Resources/settings/settings.yml index cf27f43870..f5356a1063 100644 --- a/src/lib/Resources/settings/settings.yml +++ b/src/lib/Resources/settings/settings.yml @@ -11,6 +11,10 @@ parameters: ibexa.site_access.config.default.io.permissions.files: 0o644 ibexa.site_access.config.default.io.permissions.directories: 0o755 + # Password hash + ibexa.site_access.config.password_hash.default_type: 7 # Password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User + ibexa.site_access.config.password_hash.update_type_on_change: false # Whether the password hash type should be changed when the password is changed if it differs from the default hash type + services: ibexa.api.persistence_handler: #To disable cache, switch alias to Ibexa\Contracts\Core\Persistence\Handler diff --git a/tests/integration/Core/Repository/UserServiceTest.php b/tests/integration/Core/Repository/UserServiceTest.php index 041bdf967d..fb7d1b8c16 100644 --- a/tests/integration/Core/Repository/UserServiceTest.php +++ b/tests/integration/Core/Repository/UserServiceTest.php @@ -1325,7 +1325,7 @@ public function testCreateUserWithWeakPasswordThrowsUserPasswordValidationExcept try { // This call will fail with a "UserPasswordValidationException" because the - // the password does not follow specified rules. + // password does not follow specified rules. $this->createTestUserWithPassword('pass', $userContentType); } catch (ContentFieldValidationException $e) { // Exception is caught, as there is no other way to check exception properties. @@ -2177,13 +2177,41 @@ public function testUpdateUserPasswordWithUnsupportedHashType(): void $wrongHashType = 1; $this->updateRawPasswordHash($user->getUserId(), $wrongHashType); $newPassword = 'new_secret123'; - // no need to invalidate cache since there was no load between create & raw database update + // no need to invalidate cache since there was no load between creation + // and raw database update $user = $userService->updateUserPassword($user, $newPassword); self::assertTrue($userService->checkUserCredentials($user, $newPassword)); self::assertNotEquals($oldPasswordHash, $user->passwordHash); } + /** + * @throws \Doctrine\DBAL\Exception + * @throws \ErrorException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\ContentFieldValidationException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + */ + public function testUpdateUserPasswordHashToArgon2Id(): void + { + $repository = $this->getRepository(); + $userService = $repository->getUserService(); + + $user = $this->createUser('john.doe', 'John', 'Doe'); + $oldPasswordHash = $user->passwordHash; + + $argon2IdHashType = User::PASSWORD_HASH_ARGON2ID; + $this->updateRawPasswordHash($user->getUserId(), $argon2IdHashType); + $newPassword = 'new_secret123'; + // no need to invalidate cache since there was no load between creation + // and raw database update + $user = $userService->updateUserPassword($user, $newPassword); + $passwordInfo = password_get_info($user->passwordHash); + + self::assertTrue($userService->checkUserCredentials($user, $newPassword)); + self::assertNotEquals($oldPasswordHash, $user->passwordHash); + self::assertEquals(PASSWORD_ARGON2ID, $passwordInfo['algo']); + } + /** * Test for the loadUserGroupsOfUser() method. * diff --git a/tests/lib/Repository/User/PasswordHashServiceTest.php b/tests/lib/Repository/User/PasswordHashServiceTest.php index 80aaedee8e..5b8a6ae01f 100644 --- a/tests/lib/Repository/User/PasswordHashServiceTest.php +++ b/tests/lib/Repository/User/PasswordHashServiceTest.php @@ -30,6 +30,8 @@ public function testGetSupportedHashTypes(): void [ User::PASSWORD_HASH_BCRYPT, User::PASSWORD_HASH_PHP_DEFAULT, + User::PASSWORD_HASH_ARGON2I, + User::PASSWORD_HASH_ARGON2ID, User::PASSWORD_HASH_INVALID, ], $this->passwordHashService->getSupportedHashTypes() From 5066a1421c46f2c4fbce6c5e06f0f8053c8c0447 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:08:35 +0200 Subject: [PATCH 02/28] Disable setConfigResolver for CI test --- src/lib/Resources/settings/fieldtype_services.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index 92dd324d51..c31dd758a6 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -60,9 +60,7 @@ services: Ibexa\Core\FieldType\FieldTypeRegistry: ~ - Ibexa\Core\Repository\User\PasswordHashService: - calls: - - [ setConfigResolver, [ '@ibexa.config.resolver' ] ] + Ibexa\Core\Repository\User\PasswordHashService: ~ Ibexa\Contracts\Core\Repository\PasswordHashService: alias: Ibexa\Core\Repository\User\PasswordHashService From 07b297b27ee01827b10b5dcd3a7eac6228d70a20 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Wed, 30 Jul 2025 17:22:10 +0200 Subject: [PATCH 03/28] Revert "Disable setConfigResolver for CI test" This reverts commit 0cc17bf77f8b4d64eb52ca2055e206735b2083d6. --- src/lib/Resources/settings/fieldtype_services.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index c31dd758a6..92dd324d51 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -60,7 +60,9 @@ services: Ibexa\Core\FieldType\FieldTypeRegistry: ~ - Ibexa\Core\Repository\User\PasswordHashService: ~ + Ibexa\Core\Repository\User\PasswordHashService: + calls: + - [ setConfigResolver, [ '@ibexa.config.resolver' ] ] Ibexa\Contracts\Core\Repository\PasswordHashService: alias: Ibexa\Core\Repository\User\PasswordHashService From 3ef4b6ba578e42ccf31c7454125de12c2ae1ffa4 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:31:42 +0200 Subject: [PATCH 04/28] Set config resolver in constructor --- .../Repository/PasswordHashService.php | 9 --------- .../Repository/User/PasswordHashService.php | 18 ++++++++++++------ .../Resources/settings/fieldtype_services.yml | 5 +++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/contracts/Repository/PasswordHashService.php b/src/contracts/Repository/PasswordHashService.php index a8afbfd7b7..dbae47876c 100644 --- a/src/contracts/Repository/PasswordHashService.php +++ b/src/contracts/Repository/PasswordHashService.php @@ -8,17 +8,8 @@ namespace Ibexa\Contracts\Core\Repository; -use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; - interface PasswordHashService { - /** - * Sets the ConfigResolver instance. - * - * @param \Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface $configResolver - */ - public function setConfigResolver(ConfigResolverInterface $configResolver): void; - /** * Returns default password hash type. * diff --git a/src/lib/Repository/User/PasswordHashService.php b/src/lib/Repository/User/PasswordHashService.php index 100808fa55..c00008f291 100644 --- a/src/lib/Repository/User/PasswordHashService.php +++ b/src/lib/Repository/User/PasswordHashService.php @@ -23,14 +23,15 @@ final class PasswordHashService implements PasswordHashServiceInterface private ConfigResolverInterface $configResolver; - public function __construct(int $hashType = User::DEFAULT_PASSWORD_HASH) + public function __construct(int $hashType = User::DEFAULT_PASSWORD_HASH, ConfigResolverInterface $configResolver = null) { - // Kept for BC, but overwritten by @see setConfigResolver() - $this->defaultHashType = $hashType; - } + if ($configResolver === null) { + // Kept for BC, but overwritten by @see setConfigResolver() if set + $this->defaultHashType = $hashType; + + return; + } - public function setConfigResolver(ConfigResolverInterface $configResolver): void - { $this->configResolver = $configResolver; $this->defaultHashType = $this->configResolver->getParameter('password_hash.default_type'); } @@ -114,6 +115,11 @@ public function isValidPassword( public function updatePasswordHashTypeOnChange(): bool { + if (!isset($this->configResolver)) { + // If the ConfigResolver is not set, default to false + return false; + } + return $this->configResolver->getParameter('password_hash.update_type_on_change'); } } diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index 92dd324d51..bf0ff75a83 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -61,8 +61,9 @@ services: Ibexa\Core\FieldType\FieldTypeRegistry: ~ Ibexa\Core\Repository\User\PasswordHashService: - calls: - - [ setConfigResolver, [ '@ibexa.config.resolver' ] ] + arguments: + $hashType: 7 + $configResolver: '@ibexa.config.resolver' Ibexa\Contracts\Core\Repository\PasswordHashService: alias: Ibexa\Core\Repository\User\PasswordHashService From 688559ce4d9552f1956d39e23282f0b5bae6961c Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Thu, 31 Jul 2025 08:38:09 +0200 Subject: [PATCH 05/28] Add settings to CI --- .../integration/Core/Resources/settings/integration_legacy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/Core/Resources/settings/integration_legacy.yml b/tests/integration/Core/Resources/settings/integration_legacy.yml index f6bdd6a364..1bdda12ed5 100644 --- a/tests/integration/Core/Resources/settings/integration_legacy.yml +++ b/tests/integration/Core/Resources/settings/integration_legacy.yml @@ -14,6 +14,9 @@ parameters: ibexa.site_access.config.default.user_content_type_identifier: ['user'] + ibexa.site_access.config.default.password_hash.default_type: 7 + ibexa.site_access.config.default.password_hash.update_type_on_change: false + ibexa.repositories: default: storage: ~ From ef4cafc900a309913130d5d1bb9af2e508322e6b Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Thu, 31 Jul 2025 09:12:10 +0200 Subject: [PATCH 06/28] Fix password fail logic when not upgrading hash type --- src/lib/Repository/UserService.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index b92c536ed8..b74da02299 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -799,12 +799,21 @@ public function updateUserPassword( ]); } - if ($passwordHashAlgorithm !== $userCurrentPasswordHashAlgorithm) { - // If we're trying to upgrade the password hash algorithm but the upgrade failed, + if ( + $this->passwordHashService->updatePasswordHashTypeOnChange() && + $passwordHashAlgorithm !== $userCurrentPasswordHashAlgorithm + ) { + // If we're trying to upgrade the password hash algorithm but the upgrade fails, // we fall back to the user's current password hash algorithm. $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; + } elseif ( + !$this->passwordHashService->updatePasswordHashTypeOnChange() && + $passwordHashAlgorithm !== $defaultPasswordHashAlgorithm + ) { + // If we're not trying to upgrade the password hash algorithm and the user's current + // password hash algorithm fails, we fall back to the default one. + $passwordHashAlgorithm = $defaultPasswordHashAlgorithm; } else { - // If the user's current password hash algorithm fails, we can't proceed. throw new InvalidArgumentException( 'passwordHashAlgorithm', 'The password hash algorithm is not supported or not compiled.' From 29933214458314758d57a5798482f040494ac949 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:47:48 +0200 Subject: [PATCH 07/28] Remove unneeded docblock Co-authored-by: Konrad Oboza --- .../Configuration/Parser/PasswordHash.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index f946d60908..a3e8d7ebb1 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -29,11 +29,6 @@ */ final class PasswordHash extends AbstractParser { - /** - * Adds semantic configuration definition. - * - * @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $nodeBuilder Node just under ibexa.system. - */ public function addSemanticConfig(NodeBuilder $nodeBuilder): void { $nodeBuilder From 77b1de00cceca46b525ef913f6975da9a1b5056d Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:49:39 +0200 Subject: [PATCH 08/28] CS? Co-authored-by: Konrad Oboza --- .../DependencyInjection/Configuration/Parser/PasswordHash.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index a3e8d7ebb1..6b78225a97 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -48,7 +48,7 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void } /** - * @param array $scopeSettings + * @param array $scopeSettings */ public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerInterface $contextualizer): void { From 132e4ab13b9ad2cddac5f8596be592728ba5c9f6 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:50:13 +0200 Subject: [PATCH 09/28] Remove unneeded docblock Co-authored-by: Konrad Oboza --- src/contracts/Repository/PasswordHashService.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/contracts/Repository/PasswordHashService.php b/src/contracts/Repository/PasswordHashService.php index dbae47876c..030f572102 100644 --- a/src/contracts/Repository/PasswordHashService.php +++ b/src/contracts/Repository/PasswordHashService.php @@ -48,8 +48,6 @@ public function isValidPassword(string $plainPassword, string $passwordHash, ?in /** * Returns true if password hash type should be updated when the user changes password. - * - * @return bool */ public function updatePasswordHashTypeOnChange(): bool; } From 7cddb7b3506204e9a46a8579e6d8719cb126ce7e Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:21:16 +0200 Subject: [PATCH 10/28] Simplify by breaking constructor BC --- .../Repository/User/PasswordHashService.php | 21 +++--------------- .../Resources/settings/fieldtype_services.yml | 1 - .../User/PasswordHashServiceTest.php | 22 ++++++++++++++++++- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/lib/Repository/User/PasswordHashService.php b/src/lib/Repository/User/PasswordHashService.php index c00008f291..9c10a26d6f 100644 --- a/src/lib/Repository/User/PasswordHashService.php +++ b/src/lib/Repository/User/PasswordHashService.php @@ -19,21 +19,11 @@ */ final class PasswordHashService implements PasswordHashServiceInterface { - private int $defaultHashType; - private ConfigResolverInterface $configResolver; - public function __construct(int $hashType = User::DEFAULT_PASSWORD_HASH, ConfigResolverInterface $configResolver = null) + public function __construct(ConfigResolverInterface $configResolver) { - if ($configResolver === null) { - // Kept for BC, but overwritten by @see setConfigResolver() if set - $this->defaultHashType = $hashType; - - return; - } - $this->configResolver = $configResolver; - $this->defaultHashType = $this->configResolver->getParameter('password_hash.default_type'); } public function getSupportedHashTypes(): array @@ -48,7 +38,7 @@ public function isHashTypeSupported(int $hashType): bool public function getDefaultHashType(): int { - return $this->defaultHashType; + return $this->configResolver->getParameter('password_hash.default_type'); } public function createPasswordHash( @@ -56,7 +46,7 @@ public function createPasswordHash( string $plainPassword, ?int $hashType = null ): string { - $hashType = $hashType ?? $this->defaultHashType; + $hashType = $hashType ?? $this->getDefaultHashType(); switch ($hashType) { case User::PASSWORD_HASH_BCRYPT: @@ -115,11 +105,6 @@ public function isValidPassword( public function updatePasswordHashTypeOnChange(): bool { - if (!isset($this->configResolver)) { - // If the ConfigResolver is not set, default to false - return false; - } - return $this->configResolver->getParameter('password_hash.update_type_on_change'); } } diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index bf0ff75a83..ffbd05c645 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -62,7 +62,6 @@ services: Ibexa\Core\Repository\User\PasswordHashService: arguments: - $hashType: 7 $configResolver: '@ibexa.config.resolver' Ibexa\Contracts\Core\Repository\PasswordHashService: diff --git a/tests/lib/Repository/User/PasswordHashServiceTest.php b/tests/lib/Repository/User/PasswordHashServiceTest.php index 5b8a6ae01f..731cecd70d 100644 --- a/tests/lib/Repository/User/PasswordHashServiceTest.php +++ b/tests/lib/Repository/User/PasswordHashServiceTest.php @@ -9,8 +9,10 @@ namespace Ibexa\Tests\Core\Repository\User; use Ibexa\Contracts\Core\Repository\Values\User\User; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType; use Ibexa\Core\Repository\User\PasswordHashService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; final class PasswordHashServiceTest extends TestCase @@ -21,7 +23,25 @@ final class PasswordHashServiceTest extends TestCase protected function setUp(): void { - $this->passwordHashService = new PasswordHashService(); + $this->passwordHashService = new PasswordHashService($this->getConfigResolverMock()); + } + + private function getConfigResolverMock(): ConfigResolverInterface & MockObject + { + $configResolver = $this + ->createMock(ConfigResolverInterface::class); + + $configResolver + ->method('getParameter') + ->with('password_hash.default_type') + ->willReturn(User::PASSWORD_HASH_PHP_DEFAULT); + + $configResolver + ->method('getParameter') + ->with('password_hash.update_type_on_change') + ->willReturn(false); + + return $configResolver; } public function testGetSupportedHashTypes(): void From b0b111a09b8bd824738e3e9a1e9c5399221b8431 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:33:04 +0200 Subject: [PATCH 11/28] Use actual const in yaml config example --- .../DependencyInjection/Configuration/Parser/PasswordHash.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index 6b78225a97..7ef138b7f6 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -37,7 +37,7 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->children() ->integerNode('default_type') ->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.') - ->example('7') + ->example('User::PASSWORD_HASH_PHP_DEFAULT') ->end() ->booleanNode('update_type_on_change') ->info('Whether the password hash type should be changed when the password is changed if it differs from the default type.') From b9074bf50108a9a0175d1b48c10723b4e20fba82 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 09:42:46 +0200 Subject: [PATCH 12/28] Review feedback --- src/contracts/Repository/PasswordHashService.php | 3 --- src/lib/Repository/UserService.php | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/contracts/Repository/PasswordHashService.php b/src/contracts/Repository/PasswordHashService.php index 030f572102..d570812be7 100644 --- a/src/contracts/Repository/PasswordHashService.php +++ b/src/contracts/Repository/PasswordHashService.php @@ -46,8 +46,5 @@ public function createPasswordHash(string $plainPassword, ?int $hashType = null) */ public function isValidPassword(string $plainPassword, string $passwordHash, ?int $hashType = null): bool; - /** - * Returns true if password hash type should be updated when the user changes password. - */ public function updatePasswordHashTypeOnChange(): bool; } diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index b74da02299..100efb168f 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -782,7 +782,8 @@ public function updateUserPassword( $defaultPasswordHashAlgorithm = $this->passwordHashService->getDefaultHashType(); $userCurrentPasswordHashAlgorithm = (int) $loadedUser->hashAlgorithm; - if ($this->passwordHashService->updatePasswordHashTypeOnChange()) { + $updatePasswordHashTypeOnChange = $this->passwordHashService->updatePasswordHashTypeOnChange(); + if ($updatePasswordHashTypeOnChange) { $passwordHashAlgorithm = $defaultPasswordHashAlgorithm; } else { $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; @@ -800,14 +801,14 @@ public function updateUserPassword( } if ( - $this->passwordHashService->updatePasswordHashTypeOnChange() && + $updatePasswordHashTypeOnChange && $passwordHashAlgorithm !== $userCurrentPasswordHashAlgorithm ) { // If we're trying to upgrade the password hash algorithm but the upgrade fails, // we fall back to the user's current password hash algorithm. $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; } elseif ( - !$this->passwordHashService->updatePasswordHashTypeOnChange() && + !$updatePasswordHashTypeOnChange && $passwordHashAlgorithm !== $defaultPasswordHashAlgorithm ) { // If we're not trying to upgrade the password hash algorithm and the user's current From b375fe10bcc991e13038a31dd8184bf6b572e241 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:36:39 +0200 Subject: [PATCH 13/28] Review feedback --- src/lib/Repository/UserService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index 100efb168f..a369bc2be9 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -783,10 +783,9 @@ public function updateUserPassword( $defaultPasswordHashAlgorithm = $this->passwordHashService->getDefaultHashType(); $userCurrentPasswordHashAlgorithm = (int) $loadedUser->hashAlgorithm; $updatePasswordHashTypeOnChange = $this->passwordHashService->updatePasswordHashTypeOnChange(); + $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; if ($updatePasswordHashTypeOnChange) { $passwordHashAlgorithm = $defaultPasswordHashAlgorithm; - } else { - $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; } $passwordHash = null; From ebb1e73b28137e1896950ca00fb760d16294c04c Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:43:33 +0200 Subject: [PATCH 14/28] Review feedback: FQCN in config hint --- .../DependencyInjection/Configuration/Parser/PasswordHash.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index 7ef138b7f6..16490acdf2 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -37,7 +37,7 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->children() ->integerNode('default_type') ->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.') - ->example('User::PASSWORD_HASH_PHP_DEFAULT') + ->example('!php/const:Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_PHP_DEFAULT') ->end() ->booleanNode('update_type_on_change') ->info('Whether the password hash type should be changed when the password is changed if it differs from the default type.') From ad0d728c56c1505d77510f5f9c513a89540df900 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:44:29 +0200 Subject: [PATCH 15/28] Compile time check for Argon2 support in PHP --- .../Configuration/Parser/PasswordHash.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index 16490acdf2..ad2b39830c 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -38,6 +38,20 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->integerNode('default_type') ->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.') ->example('!php/const:Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_PHP_DEFAULT') + ->validate() + ->ifTrue(static function ($value) { + $hashType = (int) $value; + + if ($hashType === \Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_ARGON2I) { + return !defined('PASSWORD_ARGON2I'); + } elseif ($hashType === \Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_ARGON2ID) { + return !defined('PASSWORD_ARGON2ID'); + } + + return !in_array($hashType, \Ibexa\Contracts\Core\Repository\Values\User\User::SUPPORTED_PASSWORD_HASHES, true); + }) + ->thenInvalid('Invalid password hash type "%s".') + ->end() ->end() ->booleanNode('update_type_on_change') ->info('Whether the password hash type should be changed when the password is changed if it differs from the default type.') From 47b9956f3d910a066b01c33c9b66189880ed5042 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:25:08 +0200 Subject: [PATCH 16/28] Skip logger check, CS --- .../Configuration/Parser/PasswordHash.php | 9 +++++---- src/lib/Repository/UserService.php | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index ad2b39830c..5df2a0c829 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -10,6 +10,7 @@ use Ibexa\Bundle\Core\DependencyInjection\Configuration\AbstractParser; use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ContextualizerInterface; +use Ibexa\Contracts\Core\Repository\Values\User\User; use Symfony\Component\Config\Definition\Builder\NodeBuilder; /** @@ -39,16 +40,16 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.') ->example('!php/const:Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_PHP_DEFAULT') ->validate() - ->ifTrue(static function ($value) { + ->ifTrue(static function ($value): bool { $hashType = (int) $value; - if ($hashType === \Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_ARGON2I) { + if ($hashType === User::PASSWORD_HASH_ARGON2I) { return !defined('PASSWORD_ARGON2I'); - } elseif ($hashType === \Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_ARGON2ID) { + } elseif ($hashType === User::PASSWORD_HASH_ARGON2ID) { return !defined('PASSWORD_ARGON2ID'); } - return !in_array($hashType, \Ibexa\Contracts\Core\Repository\Values\User\User::SUPPORTED_PASSWORD_HASHES, true); + return !in_array($hashType, User::SUPPORTED_PASSWORD_HASHES, true); }) ->thenInvalid('Invalid password hash type "%s".') ->end() diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index a369bc2be9..85d7f1431d 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -58,6 +58,7 @@ use Ibexa\Core\Repository\Values\User\UserGroupCreateStruct; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Psr\Log\NullLogger; /** * This service provides methods for managing users and user groups. @@ -125,6 +126,7 @@ public function __construct( $this->passwordHashService = $passwordHashGenerator; $this->passwordValidator = $passwordValidator; $this->configResolver = $configResolver; + $this->logger = new NullLogger(); } /** @@ -793,11 +795,9 @@ public function updateUserPassword( try { $passwordHash = $this->passwordHashService->createPasswordHash($newPassword, $passwordHashAlgorithm); } catch (UnsupportedPasswordHashType|PasswordHashTypeNotCompiled $e) { - if (isset($this->logger)) { - $this->logger->log(LogLevel::WARNING, $e->getMessage(), [ - 'exception' => $e, - ]); - } + $this->logger->log(LogLevel::WARNING, $e->getMessage(), [ + 'exception' => $e, + ]); if ( $updatePasswordHashTypeOnChange && From 0849c9ab701a5d0c529e86e9da7252fd6a598975 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:49:22 +0200 Subject: [PATCH 17/28] phpstan logger ignore --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index c448982be5..8f6322c64e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,7 +9,7 @@ parameters: treatPhpDocTypesAsCertain: false ignoreErrors: - - message: "#^Cannot call method warning\\(\\) on Psr\\\\Log\\\\LoggerInterface\\|null\\.$#" + message: "#^Cannot call method (log|debug|info|notice|warning|error|critical|alert|emergency)\\(\\) on Psr\\\\Log\\\\LoggerInterface\\|null\\.$#" paths: - src - tests From 7785736f2a7d6bbb2b59e19b6874297ae453b813 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:01:18 +0200 Subject: [PATCH 18/28] Use LoggerAwareTrait --- src/lib/Repository/UserService.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index 85d7f1431d..fe48b6edc8 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -56,7 +56,7 @@ use Ibexa\Core\Repository\Values\User\UserCreateStruct; use Ibexa\Core\Repository\Values\User\UserGroup; use Ibexa\Core\Repository\Values\User\UserGroupCreateStruct; -use Psr\Log\LoggerInterface; +use Psr\Log\LoggerAwareTrait; use Psr\Log\LogLevel; use Psr\Log\NullLogger; @@ -65,6 +65,8 @@ */ class UserService implements UserServiceInterface { + use LoggerAwareTrait; + private const USER_FIELD_TYPE_NAME = 'ibexa_user'; /** @var \Ibexa\Contracts\Core\Repository\Repository */ @@ -79,9 +81,6 @@ class UserService implements UserServiceInterface /** @var array */ protected $settings; - /** @var \Psr\Log\LoggerInterface|null */ - protected $logger; - /** @var \Ibexa\Contracts\Core\Repository\PermissionResolver */ private $permissionResolver; @@ -93,11 +92,6 @@ class UserService implements UserServiceInterface private ConfigResolverInterface $configResolver; - public function setLogger(?LoggerInterface $logger = null) - { - $this->logger = $logger; - } - /** * Setups service with reference to repository object that created it & corresponding handler. */ @@ -126,7 +120,7 @@ public function __construct( $this->passwordHashService = $passwordHashGenerator; $this->passwordValidator = $passwordValidator; $this->configResolver = $configResolver; - $this->logger = new NullLogger(); + $this->logger = $logger ?? new NullLogger(); } /** From 37b3bf69a2676dfd025db1d6dd8ebde66771fd68 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:10:27 +0200 Subject: [PATCH 19/28] Rename method --- src/contracts/Repository/PasswordHashService.php | 2 +- src/lib/Repository/User/PasswordHashService.php | 2 +- src/lib/Repository/UserService.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/contracts/Repository/PasswordHashService.php b/src/contracts/Repository/PasswordHashService.php index d570812be7..115b2fc6b3 100644 --- a/src/contracts/Repository/PasswordHashService.php +++ b/src/contracts/Repository/PasswordHashService.php @@ -46,5 +46,5 @@ public function createPasswordHash(string $plainPassword, ?int $hashType = null) */ public function isValidPassword(string $plainPassword, string $passwordHash, ?int $hashType = null): bool; - public function updatePasswordHashTypeOnChange(): bool; + public function shouldPasswordHashTypeBeUpdatedOnChange(): bool; } diff --git a/src/lib/Repository/User/PasswordHashService.php b/src/lib/Repository/User/PasswordHashService.php index 9c10a26d6f..7ca20928b5 100644 --- a/src/lib/Repository/User/PasswordHashService.php +++ b/src/lib/Repository/User/PasswordHashService.php @@ -103,7 +103,7 @@ public function isValidPassword( } } - public function updatePasswordHashTypeOnChange(): bool + public function shouldPasswordHashTypeBeUpdatedOnChange(): bool { return $this->configResolver->getParameter('password_hash.update_type_on_change'); } diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index fe48b6edc8..efd17f9e14 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -778,7 +778,7 @@ public function updateUserPassword( $defaultPasswordHashAlgorithm = $this->passwordHashService->getDefaultHashType(); $userCurrentPasswordHashAlgorithm = (int) $loadedUser->hashAlgorithm; - $updatePasswordHashTypeOnChange = $this->passwordHashService->updatePasswordHashTypeOnChange(); + $updatePasswordHashTypeOnChange = $this->passwordHashService->shouldPasswordHashTypeBeUpdatedOnChange(); $passwordHashAlgorithm = $userCurrentPasswordHashAlgorithm; if ($updatePasswordHashTypeOnChange) { $passwordHashAlgorithm = $defaultPasswordHashAlgorithm; From c50ce8d8abb9e7c8a9e9ac9973558631822dccb1 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:15:15 +0200 Subject: [PATCH 20/28] Fix logger set in ctor --- src/lib/Repository/UserService.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index efd17f9e14..e4376ff573 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -120,7 +120,10 @@ public function __construct( $this->passwordHashService = $passwordHashGenerator; $this->passwordValidator = $passwordValidator; $this->configResolver = $configResolver; - $this->logger = $logger ?? new NullLogger(); + + if (!isset($this->logger)) { + $this->logger = new NullLogger(); + } } /** From c8dc5646fe7998580a692eb855a114e358ce1e8c Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:45:51 +0200 Subject: [PATCH 21/28] Regenerated phpstan baseline --- phpstan-baseline.neon | 60 +++++++++++++------------------------------ 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e3aa86563c..c4360fb140 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -156,6 +156,12 @@ parameters: count: 1 path: src/bundle/Core/Command/ReindexCommand.php + - + message: '#^Parameter \#2 \$subject of function preg_match expects string, string\|false given\.$#' + identifier: argument.type + count: 1 + path: src/bundle/Core/Command/ReindexCommand.php + - message: '#^Parameter \#2 \$subject of function preg_match_all expects string, string\|false given\.$#' identifier: argument.type @@ -3846,18 +3852,6 @@ parameters: count: 2 path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php - - - - message: '#^Cannot call method info\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php - - message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\Handler\\AbstractURLHandler\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -3912,12 +3906,6 @@ parameters: count: 1 path: src/bundle/Core/URLChecker/Handler/MailToHandler.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/bundle/Core/URLChecker/URLChecker.php - - message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\URLChecker\:\:check\(\) has no return type specified\.$#' identifier: missingType.return @@ -10332,12 +10320,6 @@ parameters: count: 1 path: src/lib/Persistence/Cache/Handler.php - - - message: '#^Cannot call method debug\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Persistence/Cache/Identifier/CacheIdentifierGenerator.php - - message: '#^Method Ibexa\\Core\\Persistence\\Cache\\Identifier\\CacheIdentifierGenerator\:\:__construct\(\) has parameter \$keyPatterns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -15306,6 +15288,12 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Notification/Gateway.php + - + message: '#^Function Ibexa\\PolyfillPhp82\\iterator_to_array not found\.$#' + identifier: function.notFound + count: 1 + path: src/lib/Persistence/Legacy/Notification/Gateway/DoctrineDatabase.php + - message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Notification\\Gateway\\DoctrineDatabase\:\:__construct\(\) has parameter \$criterionHandlers with generic interface Ibexa\\Contracts\\Core\\Repository\\Values\\Notification\\CriterionHandlerInterface but does not specify its types\: T$#' identifier: missingType.generics @@ -15336,6 +15324,12 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Notification/Gateway/DoctrineDatabase.php + - + message: '#^Used function Ibexa\\PolyfillPhp82\\iterator_to_array not found\.$#' + identifier: function.notFound + count: 1 + path: src/lib/Persistence/Legacy/Notification/Gateway/DoctrineDatabase.php + - message: '#^Dead catch \- Doctrine\\DBAL\\Exception is never thrown in the try block\.$#' identifier: catch.neverThrown @@ -16920,12 +16914,6 @@ parameters: count: 1 path: src/lib/Repository/ContentTypeService.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Repository/Helper/RelationProcessor.php - - message: '#^Method Ibexa\\Core\\Repository\\Helper\\RelationProcessor\:\:appendFieldRelations\(\) has no return type specified\.$#' identifier: missingType.return @@ -17070,12 +17058,6 @@ parameters: count: 1 path: src/lib/Repository/Mapper/ContentDomainMapper.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Repository/Mapper/ContentDomainMapper.php - - message: '#^Method Ibexa\\Core\\Repository\\Mapper\\ContentDomainMapper\:\:buildContentDomainObject\(\) has parameter \$prioritizedLanguages with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -18264,12 +18246,6 @@ parameters: count: 1 path: src/lib/Repository/UserService.php - - - message: '#^Method Ibexa\\Core\\Repository\\UserService\:\:setLogger\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/lib/Repository/UserService.php - - message: '#^PHPDoc tag @param for parameter \$id with type mixed is not subtype of native type int\.$#' identifier: parameter.phpDocType From 69b2595dd735cc0b832f8e764a961c30cf32fa97 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:55:22 +0200 Subject: [PATCH 22/28] Revert "Regenerated phpstan baseline" This reverts commit 2d86b6fce31f8b5cd4e838664151bbcb25dbb22c. --- phpstan-baseline.neon | 60 ++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c4360fb140..e3aa86563c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -156,12 +156,6 @@ parameters: count: 1 path: src/bundle/Core/Command/ReindexCommand.php - - - message: '#^Parameter \#2 \$subject of function preg_match expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/bundle/Core/Command/ReindexCommand.php - - message: '#^Parameter \#2 \$subject of function preg_match_all expects string, string\|false given\.$#' identifier: argument.type @@ -3852,6 +3846,18 @@ parameters: count: 2 path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php + - + message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php + + - + message: '#^Cannot call method info\(\) on Psr\\Log\\LoggerInterface\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php + - message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\Handler\\AbstractURLHandler\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -3906,6 +3912,12 @@ parameters: count: 1 path: src/bundle/Core/URLChecker/Handler/MailToHandler.php + - + message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/bundle/Core/URLChecker/URLChecker.php + - message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\URLChecker\:\:check\(\) has no return type specified\.$#' identifier: missingType.return @@ -10320,6 +10332,12 @@ parameters: count: 1 path: src/lib/Persistence/Cache/Handler.php + - + message: '#^Cannot call method debug\(\) on Psr\\Log\\LoggerInterface\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/lib/Persistence/Cache/Identifier/CacheIdentifierGenerator.php + - message: '#^Method Ibexa\\Core\\Persistence\\Cache\\Identifier\\CacheIdentifierGenerator\:\:__construct\(\) has parameter \$keyPatterns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -15288,12 +15306,6 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Notification/Gateway.php - - - message: '#^Function Ibexa\\PolyfillPhp82\\iterator_to_array not found\.$#' - identifier: function.notFound - count: 1 - path: src/lib/Persistence/Legacy/Notification/Gateway/DoctrineDatabase.php - - message: '#^Method Ibexa\\Core\\Persistence\\Legacy\\Notification\\Gateway\\DoctrineDatabase\:\:__construct\(\) has parameter \$criterionHandlers with generic interface Ibexa\\Contracts\\Core\\Repository\\Values\\Notification\\CriterionHandlerInterface but does not specify its types\: T$#' identifier: missingType.generics @@ -15324,12 +15336,6 @@ parameters: count: 1 path: src/lib/Persistence/Legacy/Notification/Gateway/DoctrineDatabase.php - - - message: '#^Used function Ibexa\\PolyfillPhp82\\iterator_to_array not found\.$#' - identifier: function.notFound - count: 1 - path: src/lib/Persistence/Legacy/Notification/Gateway/DoctrineDatabase.php - - message: '#^Dead catch \- Doctrine\\DBAL\\Exception is never thrown in the try block\.$#' identifier: catch.neverThrown @@ -16914,6 +16920,12 @@ parameters: count: 1 path: src/lib/Repository/ContentTypeService.php + - + message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/lib/Repository/Helper/RelationProcessor.php + - message: '#^Method Ibexa\\Core\\Repository\\Helper\\RelationProcessor\:\:appendFieldRelations\(\) has no return type specified\.$#' identifier: missingType.return @@ -17058,6 +17070,12 @@ parameters: count: 1 path: src/lib/Repository/Mapper/ContentDomainMapper.php + - + message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' + identifier: method.nonObject + count: 1 + path: src/lib/Repository/Mapper/ContentDomainMapper.php + - message: '#^Method Ibexa\\Core\\Repository\\Mapper\\ContentDomainMapper\:\:buildContentDomainObject\(\) has parameter \$prioritizedLanguages with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -18246,6 +18264,12 @@ parameters: count: 1 path: src/lib/Repository/UserService.php + - + message: '#^Method Ibexa\\Core\\Repository\\UserService\:\:setLogger\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: src/lib/Repository/UserService.php + - message: '#^PHPDoc tag @param for parameter \$id with type mixed is not subtype of native type int\.$#' identifier: parameter.phpDocType From dccab56c75da1ef497877e1434da72f4b861dad0 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:28:25 +0200 Subject: [PATCH 23/28] Add logger to ctor params --- src/lib/Repository/UserService.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index e4376ff573..d49e6a87bf 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -57,6 +57,7 @@ use Ibexa\Core\Repository\Values\User\UserGroup; use Ibexa\Core\Repository\Values\User\UserGroupCreateStruct; use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Psr\Log\NullLogger; @@ -103,7 +104,8 @@ public function __construct( PasswordHashService $passwordHashGenerator, PasswordValidatorInterface $passwordValidator, ConfigResolverInterface $configResolver, - array $settings = [] + array $settings = [], + ?LoggerInterface $logger = null ) { $this->repository = $repository; $this->permissionResolver = $permissionResolver; @@ -120,10 +122,7 @@ public function __construct( $this->passwordHashService = $passwordHashGenerator; $this->passwordValidator = $passwordValidator; $this->configResolver = $configResolver; - - if (!isset($this->logger)) { - $this->logger = new NullLogger(); - } + $this->logger = $logger ?? new NullLogger(); } /** From 2a4477457ee8a780707e9ae627ab0816b2b30be0 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:32:33 +0200 Subject: [PATCH 24/28] Updated phpstan baseline --- phpstan-baseline.neon | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e3aa86563c..ed42142697 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3846,18 +3846,6 @@ parameters: count: 2 path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php - - - - message: '#^Cannot call method info\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/bundle/Core/URLChecker/Handler/AbstractURLHandler.php - - message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\Handler\\AbstractURLHandler\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -3912,12 +3900,6 @@ parameters: count: 1 path: src/bundle/Core/URLChecker/Handler/MailToHandler.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/bundle/Core/URLChecker/URLChecker.php - - message: '#^Method Ibexa\\Bundle\\Core\\URLChecker\\URLChecker\:\:check\(\) has no return type specified\.$#' identifier: missingType.return @@ -10332,12 +10314,6 @@ parameters: count: 1 path: src/lib/Persistence/Cache/Handler.php - - - message: '#^Cannot call method debug\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Persistence/Cache/Identifier/CacheIdentifierGenerator.php - - message: '#^Method Ibexa\\Core\\Persistence\\Cache\\Identifier\\CacheIdentifierGenerator\:\:__construct\(\) has parameter \$keyPatterns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -16920,12 +16896,6 @@ parameters: count: 1 path: src/lib/Repository/ContentTypeService.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Repository/Helper/RelationProcessor.php - - message: '#^Method Ibexa\\Core\\Repository\\Helper\\RelationProcessor\:\:appendFieldRelations\(\) has no return type specified\.$#' identifier: missingType.return @@ -17070,12 +17040,6 @@ parameters: count: 1 path: src/lib/Repository/Mapper/ContentDomainMapper.php - - - message: '#^Cannot call method error\(\) on Psr\\Log\\LoggerInterface\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Repository/Mapper/ContentDomainMapper.php - - message: '#^Method Ibexa\\Core\\Repository\\Mapper\\ContentDomainMapper\:\:buildContentDomainObject\(\) has parameter \$prioritizedLanguages with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -18264,12 +18228,6 @@ parameters: count: 1 path: src/lib/Repository/UserService.php - - - message: '#^Method Ibexa\\Core\\Repository\\UserService\:\:setLogger\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/lib/Repository/UserService.php - - message: '#^PHPDoc tag @param for parameter \$id with type mixed is not subtype of native type int\.$#' identifier: parameter.phpDocType From f7c2aba937da89e52ee9eaf851b57beec7812b03 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:24:09 +0200 Subject: [PATCH 25/28] !php/const in docblock example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Wójs --- .../DependencyInjection/Configuration/Parser/PasswordHash.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index 5df2a0c829..761e9b15b5 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -24,7 +24,7 @@ * system: * default: # configuration per siteaccess or siteaccess group * password_hash: - * default_type: 7 + * default_type: !php/const \Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_ARGON2I * update_type_on_change: false * ``` */ From 567d150b881b53dc58edad73ec5066ed29ce5559 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Wed, 6 Aug 2025 14:47:55 +0200 Subject: [PATCH 26/28] Improved config validation error message --- .../DependencyInjection/Configuration/Parser/PasswordHash.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php index 761e9b15b5..ca3ce69b53 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php @@ -51,7 +51,7 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void return !in_array($hashType, User::SUPPORTED_PASSWORD_HASHES, true); }) - ->thenInvalid('Invalid password hash type "%s".') + ->thenInvalid('Invalid password hash type "%s". If you tried to use Argon2, make sure it\'s compiled in PHP.') ->end() ->end() ->booleanNode('update_type_on_change') From 23b1d0e0e2f45a7fe39b442e3c17b4f766148a41 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:36:02 +0200 Subject: [PATCH 27/28] Made repository aware (incomplete) --- .../Parser/{ => Repository}/PasswordHash.php | 5 ++-- src/bundle/Core/IbexaCoreBundle.php | 6 +++- .../Resources/config/default_settings.yml | 4 --- .../Repository/User/PasswordHashService.php | 16 ++++++---- .../Resources/settings/fieldtype_services.yml | 2 +- src/lib/Resources/settings/settings.yml | 4 --- .../Resources/settings/integration_legacy.yml | 6 ++-- .../User/PasswordHashServiceTest.php | 30 ++++++------------- 8 files changed, 31 insertions(+), 42 deletions(-) rename src/bundle/Core/DependencyInjection/Configuration/Parser/{ => Repository}/PasswordHash.php (94%) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php similarity index 94% rename from src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php rename to src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php index ca3ce69b53..2d44c6d305 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php @@ -6,9 +6,10 @@ */ declare(strict_types=1); -namespace Ibexa\Bundle\Core\DependencyInjection\Configuration\Parser; +namespace Ibexa\Bundle\Core\DependencyInjection\Configuration\Parser\Repository; use Ibexa\Bundle\Core\DependencyInjection\Configuration\AbstractParser; +use Ibexa\Bundle\Core\DependencyInjection\Configuration\RepositoryConfigParserInterface; use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ContextualizerInterface; use Ibexa\Contracts\Core\Repository\Values\User\User; use Symfony\Component\Config\Definition\Builder\NodeBuilder; @@ -28,7 +29,7 @@ * update_type_on_change: false * ``` */ -final class PasswordHash extends AbstractParser +final class PasswordHash extends AbstractParser implements RepositoryConfigParserInterface { public function addSemanticConfig(NodeBuilder $nodeBuilder): void { diff --git a/src/bundle/Core/IbexaCoreBundle.php b/src/bundle/Core/IbexaCoreBundle.php index 02e0a74263..78577d695f 100644 --- a/src/bundle/Core/IbexaCoreBundle.php +++ b/src/bundle/Core/IbexaCoreBundle.php @@ -96,6 +96,10 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TranslationCollectorPass()); $container->addCompilerPass(new SlugConverterConfigurationPass()); + /** @var \Ibexa\Bundle\Core\DependencyInjection\IbexaCoreExtension $kernel */ + $kernel = $container->getExtension('ibexa'); + $kernel->addRepositoryConfigParser(new RepositoryConfigParser\PasswordHash()); + $container->registerForAutoconfiguration(VariableProvider::class)->addTag('ezplatform.view.variable_provider'); } @@ -123,13 +127,13 @@ public function getContainerExtension(): ExtensionInterface new ConfigParser\UrlChecker(), new ConfigParser\TwigVariablesParser(), new ConfigParser\UserContentTypeIdentifier(), - new ConfigParser\PasswordHash(), ], [ new RepositoryConfigParser\Storage(), new RepositoryConfigParser\Search(), new RepositoryConfigParser\FieldGroups(), new RepositoryConfigParser\Options(), + new RepositoryConfigParser\PasswordHash(), ] ); } diff --git a/src/bundle/Core/Resources/config/default_settings.yml b/src/bundle/Core/Resources/config/default_settings.yml index 620a4f6326..19c1bc175a 100644 --- a/src/bundle/Core/Resources/config/default_settings.yml +++ b/src/bundle/Core/Resources/config/default_settings.yml @@ -99,10 +99,6 @@ parameters: ibexa.site_access.config.default.users_group_root_subtree_path: '/1/5' ibexa.site_access.config.default.api_keys: {} # Google Maps APIs v3 key (https://developers.google.com/maps/documentation/javascript/get-api-key) - # Password hash - ibexa.site_access.config.default.password_hash.default_type: 7 # Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User - ibexa.site_access.config.default.password_hash.update_type_on_change: false # Whether the password hash type should be changed when the password is changed if it differs from the default hash type - # IO ibexa.site_access.config.default.io.metadata_handler: "default" ibexa.site_access.config.default.io.binarydata_handler: "default" diff --git a/src/lib/Repository/User/PasswordHashService.php b/src/lib/Repository/User/PasswordHashService.php index 7ca20928b5..9b06403a4b 100644 --- a/src/lib/Repository/User/PasswordHashService.php +++ b/src/lib/Repository/User/PasswordHashService.php @@ -8,9 +8,9 @@ namespace Ibexa\Core\Repository\User; +use Ibexa\Contracts\Core\Container\ApiLoader\RepositoryConfigurationProviderInterface; use Ibexa\Contracts\Core\Repository\PasswordHashService as PasswordHashServiceInterface; use Ibexa\Contracts\Core\Repository\Values\User\User; -use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled; use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType; @@ -19,11 +19,11 @@ */ final class PasswordHashService implements PasswordHashServiceInterface { - private ConfigResolverInterface $configResolver; + private RepositoryConfigurationProviderInterface $repositoryConfigurationProvider; - public function __construct(ConfigResolverInterface $configResolver) + public function __construct(RepositoryConfigurationProviderInterface $repositoryConfigurationProvider) { - $this->configResolver = $configResolver; + $this->repositoryConfigurationProvider = $repositoryConfigurationProvider; } public function getSupportedHashTypes(): array @@ -38,7 +38,9 @@ public function isHashTypeSupported(int $hashType): bool public function getDefaultHashType(): int { - return $this->configResolver->getParameter('password_hash.default_type'); + $config = $this->repositoryConfigurationProvider->getRepositoryConfig(); + + return $config['password_hash']['default_type']; } public function createPasswordHash( @@ -105,6 +107,8 @@ public function isValidPassword( public function shouldPasswordHashTypeBeUpdatedOnChange(): bool { - return $this->configResolver->getParameter('password_hash.update_type_on_change'); + $config = $this->repositoryConfigurationProvider->getRepositoryConfig(); + + return $config['password_hash']['update_type_on_change']; } } diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index ffbd05c645..29da9154a4 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -62,7 +62,7 @@ services: Ibexa\Core\Repository\User\PasswordHashService: arguments: - $configResolver: '@ibexa.config.resolver' + $repositoryConfigurationProvider: '@Ibexa\Core\Base\Container\ApiLoader\RepositoryConfigurationProvider' Ibexa\Contracts\Core\Repository\PasswordHashService: alias: Ibexa\Core\Repository\User\PasswordHashService diff --git a/src/lib/Resources/settings/settings.yml b/src/lib/Resources/settings/settings.yml index f5356a1063..cf27f43870 100644 --- a/src/lib/Resources/settings/settings.yml +++ b/src/lib/Resources/settings/settings.yml @@ -11,10 +11,6 @@ parameters: ibexa.site_access.config.default.io.permissions.files: 0o644 ibexa.site_access.config.default.io.permissions.directories: 0o755 - # Password hash - ibexa.site_access.config.password_hash.default_type: 7 # Password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User - ibexa.site_access.config.password_hash.update_type_on_change: false # Whether the password hash type should be changed when the password is changed if it differs from the default hash type - services: ibexa.api.persistence_handler: #To disable cache, switch alias to Ibexa\Contracts\Core\Persistence\Handler diff --git a/tests/integration/Core/Resources/settings/integration_legacy.yml b/tests/integration/Core/Resources/settings/integration_legacy.yml index 1bdda12ed5..ee9b9cee33 100644 --- a/tests/integration/Core/Resources/settings/integration_legacy.yml +++ b/tests/integration/Core/Resources/settings/integration_legacy.yml @@ -14,9 +14,6 @@ parameters: ibexa.site_access.config.default.user_content_type_identifier: ['user'] - ibexa.site_access.config.default.password_hash.default_type: 7 - ibexa.site_access.config.default.password_hash.update_type_on_change: false - ibexa.repositories: default: storage: ~ @@ -29,6 +26,9 @@ parameters: options: default_version_archive_limit: 5 remove_archived_versions_on_publish: true + password_hash: + default_type: 7 + update_type_on_change: false ibexa.site_access.config.default.repository: default ibexa.site_access.config.default.languages: '%languages%' diff --git a/tests/lib/Repository/User/PasswordHashServiceTest.php b/tests/lib/Repository/User/PasswordHashServiceTest.php index 731cecd70d..8864ee8eee 100644 --- a/tests/lib/Repository/User/PasswordHashServiceTest.php +++ b/tests/lib/Repository/User/PasswordHashServiceTest.php @@ -10,12 +10,12 @@ use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; +use Ibexa\Core\Base\Container\ApiLoader\RepositoryConfigurationProvider; use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType; use Ibexa\Core\Repository\User\PasswordHashService; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; +use Ibexa\Tests\Bundle\Core\ApiLoader\BaseRepositoryConfigurationProviderTestCase; -final class PasswordHashServiceTest extends TestCase +final class PasswordHashServiceTest extends BaseRepositoryConfigurationProviderTestCase { private const int NON_EXISTING_PASSWORD_HASH = PHP_INT_MAX; @@ -23,25 +23,13 @@ final class PasswordHashServiceTest extends TestCase protected function setUp(): void { - $this->passwordHashService = new PasswordHashService($this->getConfigResolverMock()); - } - - private function getConfigResolverMock(): ConfigResolverInterface & MockObject - { - $configResolver = $this - ->createMock(ConfigResolverInterface::class); - - $configResolver - ->method('getParameter') - ->with('password_hash.default_type') - ->willReturn(User::PASSWORD_HASH_PHP_DEFAULT); - - $configResolver - ->method('getParameter') - ->with('password_hash.update_type_on_change') - ->willReturn(false); + $repositories = [ + 'legacy' => $this->buildNormalizedSingleRepositoryConfig('legacy'), + ]; - return $configResolver; + $configResolver = $this->createMock(ConfigResolverInterface::class); + $repositoryConfigurationProvider = new RepositoryConfigurationProvider($configResolver, $repositories); + $this->passwordHashService = new PasswordHashService($repositoryConfigurationProvider); } public function testGetSupportedHashTypes(): void From fca04e09c5d6e1d1536bc3f08c7d61bc80a049e1 Mon Sep 17 00:00:00 2001 From: Gunnstein Lye <289744+glye@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:10:20 +0200 Subject: [PATCH 28/28] Pass repo settings directly to the hasher --- .../Parser/Repository/PasswordHash.php | 24 ++------------- .../Repository/PasswordHashService.php | 14 +++++++++ .../Container/ApiLoader/RepositoryFactory.php | 6 ++++ .../Repository/User/PasswordHashService.php | 30 ++++++++++++------- .../Resources/settings/fieldtype_services.yml | 4 +-- .../User/PasswordHashServiceTest.php | 10 +------ 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php b/src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php index 2d44c6d305..a18ea2127a 100644 --- a/src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php +++ b/src/bundle/Core/DependencyInjection/Configuration/Parser/Repository/PasswordHash.php @@ -8,9 +8,7 @@ namespace Ibexa\Bundle\Core\DependencyInjection\Configuration\Parser\Repository; -use Ibexa\Bundle\Core\DependencyInjection\Configuration\AbstractParser; use Ibexa\Bundle\Core\DependencyInjection\Configuration\RepositoryConfigParserInterface; -use Ibexa\Bundle\Core\DependencyInjection\Configuration\SiteAccessAware\ContextualizerInterface; use Ibexa\Contracts\Core\Repository\Values\User\User; use Symfony\Component\Config\Definition\Builder\NodeBuilder; @@ -29,7 +27,7 @@ * update_type_on_change: false * ``` */ -final class PasswordHash extends AbstractParser implements RepositoryConfigParserInterface +final class PasswordHash implements RepositoryConfigParserInterface { public function addSemanticConfig(NodeBuilder $nodeBuilder): void { @@ -40,6 +38,7 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->integerNode('default_type') ->info('Default password hash type, see the constants in Ibexa\Contracts\Core\Repository\Values\User\User.') ->example('!php/const:Ibexa\Contracts\Core\Repository\Values\User\User::PASSWORD_HASH_PHP_DEFAULT') + ->defaultValue(User::PASSWORD_HASH_PHP_DEFAULT) ->validate() ->ifTrue(static function ($value): bool { $hashType = (int) $value; @@ -58,26 +57,9 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->booleanNode('update_type_on_change') ->info('Whether the password hash type should be changed when the password is changed if it differs from the default type.') ->example('false') + ->defaultFalse() ->end() ->end() ->end(); } - - /** - * @param array $scopeSettings - */ - public function mapConfig(array &$scopeSettings, $currentScope, ContextualizerInterface $contextualizer): void - { - if (!isset($scopeSettings['password_hash'])) { - return; - } - - $settings = $scopeSettings['password_hash']; - if (isset($settings['default_type'])) { - $contextualizer->setContextualParameter('password_hash.default_type', $currentScope, $settings['default_type']); - } - if (isset($settings['update_type_on_change'])) { - $contextualizer->setContextualParameter('password_hash.update_type_on_change', $currentScope, $settings['update_type_on_change']); - } - } } diff --git a/src/contracts/Repository/PasswordHashService.php b/src/contracts/Repository/PasswordHashService.php index 115b2fc6b3..d1775fd314 100644 --- a/src/contracts/Repository/PasswordHashService.php +++ b/src/contracts/Repository/PasswordHashService.php @@ -10,6 +10,20 @@ interface PasswordHashService { + /** + * Sets the default password hash type. + * + * @param int $defaultHashType The default password hash type, one of Ibexa\Contracts\Core\Repository\Values\User\User::SUPPORTED_PASSWORD_HASHES. + */ + public function setDefaultHashType(int $defaultHashType): void; + + /** + * Sets whether the password hash type should be updated when the password is changed. + * + * @param bool $updateTypeOnChange Whether to update the password hash type on change. + */ + public function setUpdateTypeOnChange(bool $updateTypeOnChange): void; + /** * Returns default password hash type. * diff --git a/src/lib/Base/Container/ApiLoader/RepositoryFactory.php b/src/lib/Base/Container/ApiLoader/RepositoryFactory.php index db341c7e9b..6e1385f4b2 100644 --- a/src/lib/Base/Container/ApiLoader/RepositoryFactory.php +++ b/src/lib/Base/Container/ApiLoader/RepositoryFactory.php @@ -18,6 +18,7 @@ use Ibexa\Contracts\Core\Repository\PermissionService; use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\Validator\ContentValidator; +use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Core\Search\Handler as SearchHandler; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\Base\Exceptions\InvalidArgumentException; @@ -98,6 +99,11 @@ public function buildRepository( ): Repository { $config = $this->repositoryConfigurationProvider->getRepositoryConfig(); + if (isset($config['password_hash'])) { + $passwordHashService->setDefaultHashType($config['password_hash']['default_type'] ?? User::PASSWORD_HASH_PHP_DEFAULT); + $passwordHashService->setUpdateTypeOnChange($config['password_hash']['update_type_on_change'] ?? false); + } + return new CoreRepository( $persistenceHandler, $searchHandler, diff --git a/src/lib/Repository/User/PasswordHashService.php b/src/lib/Repository/User/PasswordHashService.php index 9b06403a4b..cb7a49ca71 100644 --- a/src/lib/Repository/User/PasswordHashService.php +++ b/src/lib/Repository/User/PasswordHashService.php @@ -8,7 +8,6 @@ namespace Ibexa\Core\Repository\User; -use Ibexa\Contracts\Core\Container\ApiLoader\RepositoryConfigurationProviderInterface; use Ibexa\Contracts\Core\Repository\PasswordHashService as PasswordHashServiceInterface; use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Core\Repository\User\Exception\PasswordHashTypeNotCompiled; @@ -19,11 +18,26 @@ */ final class PasswordHashService implements PasswordHashServiceInterface { - private RepositoryConfigurationProviderInterface $repositoryConfigurationProvider; + private int $defaultHashType; - public function __construct(RepositoryConfigurationProviderInterface $repositoryConfigurationProvider) + private bool $updateTypeOnChange; + + public function __construct( + int $defaultHashType = User::PASSWORD_HASH_PHP_DEFAULT, + bool $updateTypeOnChange = false + ) { + $this->defaultHashType = $defaultHashType; + $this->updateTypeOnChange = $updateTypeOnChange; + } + + public function setDefaultHashType(int $defaultHashType): void + { + $this->defaultHashType = $defaultHashType; + } + + public function setUpdateTypeOnChange(bool $updateTypeOnChange): void { - $this->repositoryConfigurationProvider = $repositoryConfigurationProvider; + $this->updateTypeOnChange = $updateTypeOnChange; } public function getSupportedHashTypes(): array @@ -38,9 +52,7 @@ public function isHashTypeSupported(int $hashType): bool public function getDefaultHashType(): int { - $config = $this->repositoryConfigurationProvider->getRepositoryConfig(); - - return $config['password_hash']['default_type']; + return $this->defaultHashType; } public function createPasswordHash( @@ -107,8 +119,6 @@ public function isValidPassword( public function shouldPasswordHashTypeBeUpdatedOnChange(): bool { - $config = $this->repositoryConfigurationProvider->getRepositoryConfig(); - - return $config['password_hash']['update_type_on_change']; + return $this->updateTypeOnChange; } } diff --git a/src/lib/Resources/settings/fieldtype_services.yml b/src/lib/Resources/settings/fieldtype_services.yml index 29da9154a4..c31dd758a6 100644 --- a/src/lib/Resources/settings/fieldtype_services.yml +++ b/src/lib/Resources/settings/fieldtype_services.yml @@ -60,9 +60,7 @@ services: Ibexa\Core\FieldType\FieldTypeRegistry: ~ - Ibexa\Core\Repository\User\PasswordHashService: - arguments: - $repositoryConfigurationProvider: '@Ibexa\Core\Base\Container\ApiLoader\RepositoryConfigurationProvider' + Ibexa\Core\Repository\User\PasswordHashService: ~ Ibexa\Contracts\Core\Repository\PasswordHashService: alias: Ibexa\Core\Repository\User\PasswordHashService diff --git a/tests/lib/Repository/User/PasswordHashServiceTest.php b/tests/lib/Repository/User/PasswordHashServiceTest.php index 8864ee8eee..606a5670d3 100644 --- a/tests/lib/Repository/User/PasswordHashServiceTest.php +++ b/tests/lib/Repository/User/PasswordHashServiceTest.php @@ -9,8 +9,6 @@ namespace Ibexa\Tests\Core\Repository\User; use Ibexa\Contracts\Core\Repository\Values\User\User; -use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; -use Ibexa\Core\Base\Container\ApiLoader\RepositoryConfigurationProvider; use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType; use Ibexa\Core\Repository\User\PasswordHashService; use Ibexa\Tests\Bundle\Core\ApiLoader\BaseRepositoryConfigurationProviderTestCase; @@ -23,13 +21,7 @@ final class PasswordHashServiceTest extends BaseRepositoryConfigurationProviderT protected function setUp(): void { - $repositories = [ - 'legacy' => $this->buildNormalizedSingleRepositoryConfig('legacy'), - ]; - - $configResolver = $this->createMock(ConfigResolverInterface::class); - $repositoryConfigurationProvider = new RepositoryConfigurationProvider($configResolver, $repositories); - $this->passwordHashService = new PasswordHashService($repositoryConfigurationProvider); + $this->passwordHashService = new PasswordHashService(); } public function testGetSupportedHashTypes(): void