diff --git a/CHANGELOG b/CHANGELOG index 20cb4d0..05a5966 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ 3.9.0 (unreleased): * Feature: (SF6.2+) Supports automatic value resolving in controllers (with or without MapEntity), see [https://github.com/symfony/symfony/blob/7.2/src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php](Doctrine Bridge) * Feature: supports serializer options in attributes + * Feature: Support UserProvider 3.8.2 (2025-01-09): * Fix: better detection for Uuid subclasses in autowiring diff --git a/composer.json b/composer.json index f2821db..4012097 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "atoum/stubs": "^2.2", "brick/geo": ">=0.5 <=1.0", "symfony/expression-language": "^6.3 || ^7.2", + "symfony/security-bundle": "^6.0 || ^7.0", "symfony/uid": "^6.0 || ^7.0" }, "autoload": { diff --git a/src/TingBundle/DependencyInjection/EntityFactory.php b/src/TingBundle/DependencyInjection/EntityFactory.php new file mode 100644 index 0000000..6ab1088 --- /dev/null +++ b/src/TingBundle/DependencyInjection/EntityFactory.php @@ -0,0 +1,39 @@ +setDefinition($id, new ChildDefinition('ting.security.user_provider')) + ->addArgument($config['class']) + ->addArgument($config['property']) + ; + } + + public function getKey(): string + { + return 'ting'; + } + + public function addConfiguration(NodeDefinition $builder): void + { + $builder + ->children() + ->scalarNode('class') + ->isRequired() + ->info('The full entity class name of your user class.') + ->cannotBeEmpty() + ->end() + ->scalarNode('property')->defaultNull()->end() + ->end() + ; + } +} diff --git a/src/TingBundle/Resources/config/services.xml b/src/TingBundle/Resources/config/services.xml index 00c0da4..e4335e1 100644 --- a/src/TingBundle/Resources/config/services.xml +++ b/src/TingBundle/Resources/config/services.xml @@ -106,6 +106,11 @@ + + + + + diff --git a/src/TingBundle/Security/EntityUserProvider.php b/src/TingBundle/Security/EntityUserProvider.php new file mode 100644 index 0000000..3c72461 --- /dev/null +++ b/src/TingBundle/Security/EntityUserProvider.php @@ -0,0 +1,137 @@ + + */ +class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface +{ + public function __construct( + private readonly MetadataRepository $metadataRepository, + private readonly RepositoryFactory $repositoryFactory, + private readonly string $class, + private readonly ?string $property = null, + ) { + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + $repository = $this->getRepository(); + if (null !== $this->property) { + $user = $repository->getOneBy([$this->property => $identifier]); + } else { + if (!$repository instanceof UserLoaderInterface) { + throw new \InvalidArgumentException(\sprintf('You must either make the "%s" entity Ting Repository ("%s") implement "CCMBenchmark\TingBundle\Security\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->class, get_debug_type($repository))); + } + + $user = $repository->loadUserByIdentifier($identifier); + } + + if (null === $user) { + $e = new UserNotFoundException(\sprintf('User "%s" not found.', $identifier)); + $e->setUserIdentifier($identifier); + + throw $e; + } + + return $user; + } + + public function refreshUser(UserInterface $user): UserInterface + { + if (!$user instanceof $this->class) { + throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user))); + } + + $repository = $this->getRepository(); + if ($repository instanceof UserProviderInterface) { + $refreshedUser = $repository->refreshUser($user); + } else { + // The user must be reloaded via the primary key as all other data + // might have changed without proper persistence in the database. + // That's the case when the user has been changed by a form with + // validation errors. + if (!$id = $this->getIdentifierValues($user)) { + throw new \InvalidArgumentException('You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Ting.'); + } + + $refreshedUser = $repository->get($id); + if (null === $refreshedUser) { + $e = new UserNotFoundException('User with id '.json_encode($id).' not found.'); + $e->setUserIdentifier(json_encode($id)); + + throw $e; + } + } + + return $refreshedUser; + } + + public function supportsClass(string $class): bool + { + return $class === $this->class || is_subclass_of($class, $this->class); + } + + /** + * @final + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof $this->class) { + throw new UnsupportedUserException(\sprintf('Instances of "%s" are not supported.', get_debug_type($user))); + } + + $repository = $this->getRepository(); + if ($repository instanceof PasswordUpgraderInterface) { + $repository->upgradePassword($user, $newHashedPassword); + } + } + + private function getMetadata(): Metadata + { + $metadata = null; + $this->metadataRepository->findMetadataForEntity($this->class, function (Metadata $innerMetadata) use (&$metadata) { + $metadata = $innerMetadata; + }, fn () => null); + + if ($metadata === null) { + throw new \InvalidArgumentException(\sprintf('No metadata found for entity "%s".', $this->class)); + } + + return $metadata; + } + + private function getRepository(): Repository + { + return $this->repositoryFactory->get($this->getMetadata()->getRepository()); + } + + private function getIdentifierValues($user): ?array + { + $metadata = $this->getMetadata(); + $primaries = $metadata->getPrimaries(); + if ($primaries === []) { + return null; + } + $identifierValues = []; + foreach ($primaries as $primary) { + $identifierValues[$primary] = $metadata->getEntityPropertyByFieldName($user, $primary); + } + + return $identifierValues; + } +} diff --git a/src/TingBundle/Security/UserLoaderInterface.php b/src/TingBundle/Security/UserLoaderInterface.php new file mode 100644 index 0000000..4064174 --- /dev/null +++ b/src/TingBundle/Security/UserLoaderInterface.php @@ -0,0 +1,15 @@ +hasExtension('security')) { + $container->getExtension('security')->addUserProviderFactory(new EntityFactory()); + } + } }