diff --git a/src/Symfony/Component/Security/Http/Controller/UserValueResolver.php b/src/Symfony/Component/Security/Http/Controller/UserValueResolver.php index 7774e7b4a2554..c55dcbd7b8980 100644 --- a/src/Symfony/Component/Security/Http/Controller/UserValueResolver.php +++ b/src/Symfony/Component/Security/Http/Controller/UserValueResolver.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -41,17 +42,41 @@ public function supports(Request $request, ArgumentMetadata $argument): bool $token = $this->tokenStorage->getToken(); if (!$token instanceof TokenInterface) { + $this->maybeThrowAccessDeniedException($argument); + return false; } $user = $token->getUser(); // in case it's not an object we cannot do anything with it; E.g. "anon." - return $user instanceof UserInterface; + if (!$user instanceof UserInterface) { + $this->maybeThrowAccessDeniedException($argument); + + return false; + } + + return true; } public function resolve(Request $request, ArgumentMetadata $argument): iterable { yield $this->tokenStorage->getToken()->getUser(); } + + private function maybeThrowAccessDeniedException(ArgumentMetadata $argument): void + { + if ($argument->hasDefaultValue() || (null !== $argument->getType() && $argument->isNullable())) { + return; + } + + // Although not really the responsibility of an ArgumentValueResolverInterface, we need to stop here + // because otherwise another resolver (like ServiceValueResolver) can try to load the User class + // from the service container and fail with an exception that is counter-intuitive: + // + // Example: Cannot autowire argument $user of "App\Controller::myAction()": it references class "App\User" but no such service exists. + // + // By throwing an AccessDeniedException we can redirect the user to a login page. + throw new AccessDeniedException('No token found in the security context.'); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php b/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php index bfc182c89c8b1..fedc8e864847c 100644 --- a/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php @@ -18,20 +18,52 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; class UserValueResolverTest extends TestCase { - public function testResolveNoToken() + public function testResolveNoTokenWhenArgumentIsNullable() { $tokenStorage = new TokenStorage(); $resolver = new UserValueResolver($tokenStorage); - $metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null); + $metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, true); $this->assertFalse($resolver->supports(Request::create('/'), $metadata)); } + public function testResolveNoTokenWhenArgumentHasDefaultValue() + { + $tokenStorage = new TokenStorage(); + $resolver = new UserValueResolver($tokenStorage); + $metadata = new ArgumentMetadata('foo', UserInterface::class, false, true, null, true); + + $this->assertFalse($resolver->supports(Request::create('/'), $metadata)); + } + + public function testResolveNoTokenWhenArgumentIsNotNullable() + { + $tokenStorage = new TokenStorage(); + $resolver = new UserValueResolver($tokenStorage); + $metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, false); + + $this->expectException(AccessDeniedException::class); + + $resolver->supports(Request::create('/'), $metadata); + } + + public function testResolveNoTokenWhenArgumentDoesNotHaveDefaultValue() + { + $tokenStorage = new TokenStorage(); + $resolver = new UserValueResolver($tokenStorage); + $metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, false); + + $this->expectException(AccessDeniedException::class); + + $resolver->supports(Request::create('/'), $metadata); + } + public function testResolveNoUser() { $mock = $this->createMock(UserInterface::class);