Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
39 changes: 39 additions & 0 deletions src/TingBundle/DependencyInjection/EntityFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace CCMBenchmark\TingBundle\DependencyInjection;

use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class EntityFactory implements UserProviderFactoryInterface
{
public function create(ContainerBuilder $container, string $id, array $config): void
{
$container
->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()
;
}
}
5 changes: 5 additions & 0 deletions src/TingBundle/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
<argument type="service" id="ting" />
<tag name="validator.constraint_validator" />
</service>

<service id="ting.security.user_provider" abstract="true" class="CCMBenchmark\TingBundle\Security\EntityUserProvider">
<argument type="service" id="ting.metadatarepository" />
<argument type="service" id="ting" />
</service>

<service id="ting.entity_value_resolver" class="CCMBenchmark\TingBundle\ArgumentResolver\EntityValueResolver">
<tag name="controller.argument_value_resolver" priority="110" />
Expand Down
137 changes: 137 additions & 0 deletions src/TingBundle/Security/EntityUserProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

namespace CCMBenchmark\TingBundle\Security;

use CCMBenchmark\Ting\MetadataRepository;
use CCMBenchmark\Ting\Repository\Metadata;
use CCMBenchmark\Ting\Repository\Repository;
use CCMBenchmark\TingBundle\Repository\RepositoryFactory;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

/**
* @template TUser of UserInterface
*
* @template-implements UserProviderInterface<TUser>
*/
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;
}
}
15 changes: 15 additions & 0 deletions src/TingBundle/Security/UserLoaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace CCMBenchmark\TingBundle\Security;

use Symfony\Component\Security\Core\User\UserInterface;

interface UserLoaderInterface
{
/**
* Loads the user for the given user identifier (e.g. username or email).
*
* This method must return null if the user is not found.
*/
public function loadUserByIdentifier(string $identifier): ?UserInterface;
}
13 changes: 12 additions & 1 deletion src/TingBundle/TingBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,20 @@

namespace CCMBenchmark\TingBundle;

use CCMBenchmark\TingBundle\DependencyInjection\EntityFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class TingBundle extends Bundle
{
public const VERSION = '3.8';
public const VERSION = '3.9.0';

public function build(ContainerBuilder $container): void
{
parent::build($container);

if ($container->hasExtension('security')) {
$container->getExtension('security')->addUserProviderFactory(new EntityFactory());
}
}
}
Loading