From b0171459167faad0e72dd0ce11b568b0d794416e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 17 Feb 2025 16:31:27 +0100 Subject: [PATCH] [Security] Merge UserAuthorizationCheckerInterface into AuthorizationCheckerInterface --- UPGRADE-7.3.md | 1 + .../Twig/Extension/SecurityExtension.php | 13 +-- .../Tests/Extension/SecurityExtensionTest.php | 86 +++++++++++++------ .../Resources/config/security.php | 9 -- .../Resources/config/templating_twig.php | 1 - .../Bundle/SecurityBundle/Security.php | 25 +++--- .../Token/UserAuthorizationCheckerToken.php | 31 ------- .../Authorization/AuthorizationChecker.php | 18 ++++ .../AuthorizationCheckerInterface.php | 2 + .../UserAuthorizationChecker.php | 31 ------- .../UserAuthorizationCheckerInterface.php | 30 ------- .../Component/Security/Core/CHANGELOG.md | 3 +- .../UserAuthorizationCheckerTokenTest.php | 26 ------ .../AuthorizationCheckerTest.php | 39 +++++++++ .../UserAuthorizationCheckerTest.php | 70 --------------- .../Voter/AuthenticatedVoterTest.php | 4 +- 16 files changed, 137 insertions(+), 252 deletions(-) delete mode 100644 src/Symfony/Component/Security/Core/Authentication/Token/UserAuthorizationCheckerToken.php delete mode 100644 src/Symfony/Component/Security/Core/Authorization/UserAuthorizationChecker.php delete mode 100644 src/Symfony/Component/Security/Core/Authorization/UserAuthorizationCheckerInterface.php delete mode 100644 src/Symfony/Component/Security/Core/Tests/Authentication/Token/UserAuthorizationCheckerTokenTest.php delete mode 100644 src/Symfony/Component/Security/Core/Tests/Authorization/UserAuthorizationCheckerTest.php diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md index c460396ed480d..c4b1bc15d5c82 100644 --- a/UPGRADE-7.3.md +++ b/UPGRADE-7.3.md @@ -16,6 +16,7 @@ Ldap Security -------- + * Add `AuthorizationCheckerInterface::isGrantedForUser()` to test user authorization without relying on the session * Deprecate `UserInterface::eraseCredentials()` and `TokenInterface::eraseCredentials()`; erase credentials e.g. using `__serialize()` instead diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index 6bd8e764c78aa..ea11283d6780a 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -14,7 +14,6 @@ use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Security\Core\Authorization\AccessDecision; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator; @@ -31,7 +30,6 @@ final class SecurityExtension extends AbstractExtension public function __construct( private ?AuthorizationCheckerInterface $securityChecker = null, private ?ImpersonateUrlGenerator $impersonateUrlGenerator = null, - private ?UserAuthorizationCheckerInterface $userSecurityChecker = null, ) { } @@ -58,8 +56,8 @@ public function isGranted(mixed $role, mixed $object = null, ?string $field = nu public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?string $field = null, ?AccessDecision $accessDecision = null): bool { - if (!$this->userSecurityChecker) { - throw new \LogicException(\sprintf('An instance of "%s" must be provided to use "%s()".', UserAuthorizationCheckerInterface::class, __METHOD__)); + if (null === $this->securityChecker) { + return false; } if (null !== $field) { @@ -71,7 +69,7 @@ public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $s } try { - return $this->userSecurityChecker->isGrantedForUser($user, $attribute, $subject, $accessDecision); + return $this->securityChecker->isGrantedForUser($user, $attribute, $subject, $accessDecision); } catch (AuthenticationCredentialsNotFoundException) { return false; } @@ -117,16 +115,13 @@ public function getFunctions(): array { $functions = [ new TwigFunction('is_granted', $this->isGranted(...)), + new TwigFunction('is_granted_for_user', $this->isGrantedForUser(...)), new TwigFunction('impersonation_exit_url', $this->getImpersonateExitUrl(...)), new TwigFunction('impersonation_exit_path', $this->getImpersonateExitPath(...)), new TwigFunction('impersonation_url', $this->getImpersonateUrl(...)), new TwigFunction('impersonation_path', $this->getImpersonatePath(...)), ]; - if ($this->userSecurityChecker) { - $functions[] = new TwigFunction('is_granted_for_user', $this->isGrantedForUser(...)); - } - return $functions; } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/SecurityExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/SecurityExtensionTest.php index 2afa868f0364e..fa9a72ceb0903 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/SecurityExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/SecurityExtensionTest.php @@ -15,12 +15,23 @@ use Symfony\Bridge\PhpUnit\ClassExistsMock; use Symfony\Bridge\Twig\Extension\SecurityExtension; use Symfony\Component\Security\Acl\Voter\FieldVote; +use Symfony\Component\Security\Core\Authorization\AccessDecision; +use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; class SecurityExtensionTest extends TestCase { + public static function setUpBeforeClass(): void + { + ClassExistsMock::register(SecurityExtension::class); + } + + protected function tearDown(): void + { + ClassExistsMock::withMockedClasses([FieldVote::class => true]); + } + /** * @dataProvider provideObjectFieldAclCases */ @@ -39,17 +50,16 @@ public function testIsGrantedCreatesFieldVoteObjectWhenFieldNotNull($object, $fi public function testIsGrantedThrowsWhenFieldNotNullAndFieldVoteClassDoesNotExist() { - if (!class_exists(UserAuthorizationCheckerInterface::class)) { + if (!method_exists(AuthorizationChecker::class, 'isGrantedForUser')) { $this->markTestSkipped('This test requires symfony/security-core 7.3 or superior.'); } $securityChecker = $this->createMock(AuthorizationCheckerInterface::class); - ClassExistsMock::register(SecurityExtension::class); ClassExistsMock::withMockedClasses([FieldVote::class => false]); $this->expectException(\LogicException::class); - $this->expectExceptionMessageMatches('Passing a $field to the "is_granted()" function requires symfony/acl.'); + $this->expectExceptionMessage('Passing a $field to the "is_granted()" function requires symfony/acl.'); $securityExtension = new SecurityExtension($securityChecker); $securityExtension->isGranted('ROLE', 'object', 'bar'); @@ -60,38 +70,41 @@ public function testIsGrantedThrowsWhenFieldNotNullAndFieldVoteClassDoesNotExist */ public function testIsGrantedForUserCreatesFieldVoteObjectWhenFieldNotNull($object, $field, $expectedSubject) { - if (!class_exists(UserAuthorizationCheckerInterface::class)) { + if (!method_exists(AuthorizationChecker::class, 'isGrantedForUser')) { $this->markTestSkipped('This test requires symfony/security-core 7.3 or superior.'); } $user = $this->createMock(UserInterface::class); - $userSecurityChecker = $this->createMock(UserAuthorizationCheckerInterface::class); - $userSecurityChecker - ->expects($this->once()) - ->method('isGrantedForUser') - ->with($user, 'ROLE', $expectedSubject) - ->willReturn(true); + $securityChecker = new class implements AuthorizationCheckerInterface { + public UserInterface $user; + public mixed $attribute; + public mixed $subject; + + public function isGranted(mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool + { + throw new \BadMethodCallException('This method should not be called.'); + } + + public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool + { + $this->user = $user; + $this->attribute = $attribute; + $this->subject = $subject; + + return true; + } + }; - $securityExtension = new SecurityExtension(null, null, $userSecurityChecker); + $securityExtension = new SecurityExtension($securityChecker); $this->assertTrue($securityExtension->isGrantedForUser($user, 'ROLE', $object, $field)); - } + $this->assertSame($user, $securityChecker->user); + $this->assertSame('ROLE', $securityChecker->attribute); - public function testIsGrantedForUserThrowsWhenFieldNotNullAndFieldVoteClassDoesNotExist() - { - if (!class_exists(UserAuthorizationCheckerInterface::class)) { - $this->markTestSkipped('This test requires symfony/security-core 7.3 or superior.'); + if (null === $field) { + $this->assertSame($object, $securityChecker->subject); + } else { + $this->assertEquals($expectedSubject, $securityChecker->subject); } - - $securityChecker = $this->createMock(UserAuthorizationCheckerInterface::class); - - ClassExistsMock::register(SecurityExtension::class); - ClassExistsMock::withMockedClasses([FieldVote::class => false]); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessageMatches('Passing a $field to the "is_granted_for_user()" function requires symfony/acl.'); - - $securityExtension = new SecurityExtension(null, null, $securityChecker); - $securityExtension->isGrantedForUser($this->createMock(UserInterface::class), 'object', 'bar'); } public static function provideObjectFieldAclCases() @@ -105,4 +118,21 @@ public static function provideObjectFieldAclCases() ['object', 'field', new FieldVote('object', 'field')], ]; } + + public function testIsGrantedForUserThrowsWhenFieldNotNullAndFieldVoteClassDoesNotExist() + { + if (!method_exists(AuthorizationChecker::class, 'isGrantedForUser')) { + $this->markTestSkipped('This test requires symfony/security-core 7.3 or superior.'); + } + + $securityChecker = $this->createMock(AuthorizationCheckerInterface::class); + + ClassExistsMock::withMockedClasses([FieldVote::class => false]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Passing a $field to the "is_granted_for_user()" function requires symfony/acl.'); + + $securityExtension = new SecurityExtension($securityChecker); + $securityExtension->isGrantedForUser($this->createMock(UserInterface::class), 'ROLE', 'object', 'bar'); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php index bd879973b49a3..7411c6dc5ceb2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.php @@ -31,8 +31,6 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; -use Symfony\Component\Security\Core\Authorization\UserAuthorizationChecker; -use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; @@ -69,12 +67,6 @@ ]) ->alias(AuthorizationCheckerInterface::class, 'security.authorization_checker') - ->set('security.user_authorization_checker', UserAuthorizationChecker::class) - ->args([ - service('security.access.decision_manager'), - ]) - ->alias(UserAuthorizationCheckerInterface::class, 'security.user_authorization_checker') - ->set('security.token_storage', UsageTrackingTokenStorage::class) ->args([ service('security.untracked_token_storage'), @@ -93,7 +85,6 @@ service_locator([ 'security.token_storage' => service('security.token_storage'), 'security.authorization_checker' => service('security.authorization_checker'), - 'security.user_authorization_checker' => service('security.user_authorization_checker'), 'security.authenticator.managers_locator' => service('security.authenticator.managers_locator')->ignoreOnInvalid(), 'request_stack' => service('request_stack'), 'security.firewall.map' => service('security.firewall.map'), diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php index 96a7a2833a443..05a74d086e820 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.php @@ -26,7 +26,6 @@ ->args([ service('security.authorization_checker')->ignoreOnInvalid(), service('security.impersonate_url_generator')->ignoreOnInvalid(), - service('security.user_authorization_checker')->ignoreOnInvalid(), ]) ->tag('twig.extension') ; diff --git a/src/Symfony/Bundle/SecurityBundle/Security.php b/src/Symfony/Bundle/SecurityBundle/Security.php index f1999ebb284b6..50b54917d87a6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security.php +++ b/src/Symfony/Bundle/SecurityBundle/Security.php @@ -19,7 +19,6 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AccessDecision; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\LogicException; use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Core\User\UserInterface; @@ -39,7 +38,7 @@ * * @final */ -class Security implements AuthorizationCheckerInterface, UserAuthorizationCheckerInterface +class Security implements AuthorizationCheckerInterface { public function __construct( private readonly ContainerInterface $container, @@ -65,6 +64,17 @@ public function isGranted(mixed $attributes, mixed $subject = null, ?AccessDecis ->isGranted($attributes, $subject, $accessDecision); } + /** + * Checks if the attribute is granted against the user and optionally supplied subject. + * + * This should be used over isGranted() when checking permissions against a user that is not currently logged in or while in a CLI context. + */ + public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool + { + return $this->container->get('security.authorization_checker') + ->isGrantedForUser($user, $attribute, $subject, $accessDecision); + } + public function getToken(): ?TokenInterface { return $this->container->get('security.token_storage')->getToken(); @@ -150,17 +160,6 @@ public function logout(bool $validateCsrfToken = true): ?Response return $logoutEvent->getResponse(); } - /** - * Checks if the attribute is granted against the user and optionally supplied subject. - * - * This should be used over isGranted() when checking permissions against a user that is not currently logged in or while in a CLI context. - */ - public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool - { - return $this->container->get('security.user_authorization_checker') - ->isGrantedForUser($user, $attribute, $subject, $accessDecision); - } - private function getAuthenticator(?string $authenticatorName, string $firewallName): AuthenticatorInterface { if (!isset($this->authenticators[$firewallName])) { diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/UserAuthorizationCheckerToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/UserAuthorizationCheckerToken.php deleted file mode 100644 index 2e84ce7ae3614..0000000000000 --- a/src/Symfony/Component/Security/Core/Authentication/Token/UserAuthorizationCheckerToken.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Authentication\Token; - -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * UserAuthorizationCheckerToken implements a token used for checking authorization. - * - * @author Nate Wiebe - * - * @internal - */ -final class UserAuthorizationCheckerToken extends AbstractToken implements OfflineTokenInterface -{ - public function __construct(UserInterface $user) - { - parent::__construct($user->getRoles()); - - $this->setUser($user); - } -} diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php index 3960f2bea87cc..7ad1a005719ed 100644 --- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php +++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php @@ -11,8 +11,11 @@ namespace Symfony\Component\Security\Core\Authorization; +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Authentication\Token\NullToken; +use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * AuthorizationChecker is the main authorization point of the Security component. @@ -48,4 +51,19 @@ final public function isGranted(mixed $attribute, mixed $subject = null, ?Access array_pop($this->accessDecisionStack); } } + + final public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool + { + $token = new class($user->getRoles()) extends AbstractToken implements OfflineTokenInterface {}; + $token->setUser($user); + + $accessDecision ??= end($this->accessDecisionStack) ?: new AccessDecision(); + $this->accessDecisionStack[] = $accessDecision; + + try { + return $accessDecision->isGranted = $this->accessDecisionManager->decide($token, [$attribute], $subject, $accessDecision); + } finally { + array_pop($this->accessDecisionStack); + } + } } diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php index 7c673dfc8a306..b8eed2a5fd7ae 100644 --- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php +++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php @@ -15,6 +15,8 @@ * The AuthorizationCheckerInterface. * * @author Johannes M. Schmitt + * + * @method bool isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null) */ interface AuthorizationCheckerInterface { diff --git a/src/Symfony/Component/Security/Core/Authorization/UserAuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/UserAuthorizationChecker.php deleted file mode 100644 index f515e5cbdeaea..0000000000000 --- a/src/Symfony/Component/Security/Core/Authorization/UserAuthorizationChecker.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Authorization; - -use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * @author Nate Wiebe - */ -final class UserAuthorizationChecker implements UserAuthorizationCheckerInterface -{ - public function __construct( - private readonly AccessDecisionManagerInterface $accessDecisionManager, - ) { - } - - public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool - { - return $this->accessDecisionManager->decide(new UserAuthorizationCheckerToken($user), [$attribute], $subject, $accessDecision); - } -} diff --git a/src/Symfony/Component/Security/Core/Authorization/UserAuthorizationCheckerInterface.php b/src/Symfony/Component/Security/Core/Authorization/UserAuthorizationCheckerInterface.php deleted file mode 100644 index 15e5b4d43990d..0000000000000 --- a/src/Symfony/Component/Security/Core/Authorization/UserAuthorizationCheckerInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Authorization; - -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * Interface is used to check user authorization without a session. - * - * @author Nate Wiebe - */ -interface UserAuthorizationCheckerInterface -{ - /** - * Checks if the attribute is granted against the user and optionally supplied subject. - * - * @param mixed $attribute A single attribute to vote on (can be of any type, string and instance of Expression are supported by the core) - * @param AccessDecision|null $accessDecision Should be used to explain the decision - */ - public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool; -} diff --git a/src/Symfony/Component/Security/Core/CHANGELOG.md b/src/Symfony/Component/Security/Core/CHANGELOG.md index 331b204cc1ae8..6ad2626c86560 100644 --- a/src/Symfony/Component/Security/Core/CHANGELOG.md +++ b/src/Symfony/Component/Security/Core/CHANGELOG.md @@ -4,8 +4,7 @@ CHANGELOG 7.3 --- - * Add `UserAuthorizationChecker::isGrantedForUser()` to test user authorization without relying on the session. - For example, users not currently logged in, or while processing a message from a message queue. + * Add `AuthorizationCheckerInterface::isGrantedForUser()` to test user authorization without relying on the session * Add `OfflineTokenInterface` to mark tokens that do not represent the currently logged-in user * Deprecate `UserInterface::eraseCredentials()` and `TokenInterface::eraseCredentials()`, erase credentials e.g. using `__serialize()` instead diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UserAuthorizationCheckerTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UserAuthorizationCheckerTokenTest.php deleted file mode 100644 index 2e7e11bde58f6..0000000000000 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UserAuthorizationCheckerTokenTest.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Tests\Authentication\Token; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken; -use Symfony\Component\Security\Core\User\InMemoryUser; - -class UserAuthorizationCheckerTokenTest extends TestCase -{ - public function testConstructor() - { - $token = new UserAuthorizationCheckerToken($user = new InMemoryUser('foo', 'bar', ['ROLE_FOO'])); - $this->assertSame(['ROLE_FOO'], $token->getRoleNames()); - $this->assertSame($user, $token->getUser()); - } -} diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php index 36b048c8976d1..00f0f50e47ca3 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\NullToken; +use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -77,4 +78,42 @@ public function testIsGrantedWithObjectAttribute() $this->tokenStorage->setToken($token); $this->assertTrue($this->authorizationChecker->isGranted($attribute)); } + + /** + * @dataProvider isGrantedForUserProvider + */ + public function testIsGrantedForUser(bool $decide, array $roles) + { + $user = new InMemoryUser('username', 'password', $roles); + + $this->accessDecisionManager + ->expects($this->once()) + ->method('decide') + ->with($this->callback(static fn (OfflineTokenInterface $token) => $token->getUser() === $user), ['ROLE_FOO']) + ->willReturn($decide); + + $this->assertSame($decide, $this->authorizationChecker->isGrantedForUser($user, 'ROLE_FOO')); + } + + public static function isGrantedForUserProvider(): array + { + return [ + [false, ['ROLE_USER']], + [true, ['ROLE_USER', 'ROLE_FOO']], + ]; + } + + public function testIsGrantedForUserWithObjectAttribute() + { + $attribute = new \stdClass(); + + $user = new InMemoryUser('username', 'password', ['ROLE_USER']); + + $this->accessDecisionManager + ->expects($this->once()) + ->method('decide') + ->with($this->isInstanceOf(OfflineTokenInterface::class), [$attribute]) + ->willReturn(true); + $this->assertTrue($this->authorizationChecker->isGrantedForUser($user, $attribute)); + } } diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/UserAuthorizationCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/UserAuthorizationCheckerTest.php deleted file mode 100644 index e9b6bb74bfe6f..0000000000000 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/UserAuthorizationCheckerTest.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Core\Tests\Authorization; - -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken; -use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; -use Symfony\Component\Security\Core\Authorization\UserAuthorizationChecker; -use Symfony\Component\Security\Core\User\InMemoryUser; - -class UserAuthorizationCheckerTest extends TestCase -{ - private AccessDecisionManagerInterface&MockObject $accessDecisionManager; - private UserAuthorizationChecker $authorizationChecker; - - protected function setUp(): void - { - $this->accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class); - - $this->authorizationChecker = new UserAuthorizationChecker($this->accessDecisionManager); - } - - /** - * @dataProvider isGrantedProvider - */ - public function testIsGranted(bool $decide, array $roles) - { - $user = new InMemoryUser('username', 'password', $roles); - - $this->accessDecisionManager - ->expects($this->once()) - ->method('decide') - ->with($this->callback(fn (UserAuthorizationCheckerToken $token): bool => $user === $token->getUser()), $this->identicalTo(['ROLE_FOO'])) - ->willReturn($decide); - - $this->assertSame($decide, $this->authorizationChecker->isGrantedForUser($user, 'ROLE_FOO')); - } - - public static function isGrantedProvider(): array - { - return [ - [false, ['ROLE_USER']], - [true, ['ROLE_USER', 'ROLE_FOO']], - ]; - } - - public function testIsGrantedWithObjectAttribute() - { - $attribute = new \stdClass(); - - $token = new UserAuthorizationCheckerToken(new InMemoryUser('username', 'password', ['ROLE_USER'])); - - $this->accessDecisionManager - ->expects($this->once()) - ->method('decide') - ->with($this->isInstanceOf($token::class), $this->identicalTo([$attribute])) - ->willReturn(true); - $this->assertTrue($this->authorizationChecker->isGrantedForUser($token->getUser(), $attribute)); - } -} diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php index 89f6c35007520..b5e0bf429fcd7 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php @@ -15,9 +15,9 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Authentication\Token\NullToken; +use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; -use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken; use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Exception\InvalidArgumentException; @@ -148,7 +148,7 @@ public function getCredentials() } if ('offline' === $authenticated) { - return new UserAuthorizationCheckerToken($user); + return new class($user->getRoles()) extends AbstractToken implements OfflineTokenInterface {}; } return new NullToken();