From e37091541c2cd3a3478d9a9f7b66806557f7a02d Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Mon, 20 Jul 2020 20:36:06 +0200 Subject: [PATCH] Use NullToken while checking authorization This allows to e.g. have some objects that can be viewed by anyone (even unauthenticated users). --- UPGRADE-5.2.md | 6 + src/Symfony/Component/Security/CHANGELOG.md | 3 +- .../AuthenticationTrustResolver.php | 3 +- .../Core/Authentication/Token/NullToken.php | 105 ++++++++++++++++++ .../Authorization/AuthorizationChecker.php | 11 +- .../AuthorizationCheckerTest.php | 9 +- .../Security/Http/Firewall/AccessListener.php | 15 +-- .../Tests/Firewall/AccessListenerTest.php | 29 +++-- .../Component/Security/Http/composer.json | 2 +- 9 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 src/Symfony/Component/Security/Core/Authentication/Token/NullToken.php diff --git a/UPGRADE-5.2.md b/UPGRADE-5.2.md index 0eaced68a90ef..112866537f47f 100644 --- a/UPGRADE-5.2.md +++ b/UPGRADE-5.2.md @@ -43,3 +43,9 @@ Validator * }) */ ``` + +Security +-------- + + * [BC break] In the experimental authenticator-based system, * `TokenInterface::getUser()` + returns `null` in case of unauthenticated session. diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index c0981d698c8d8..37171f723fca8 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -4,7 +4,8 @@ CHANGELOG 5.2.0 ----- - * Added attributes on ``Passport`` + * Added attributes on `Passport` + * Changed `AuthorizationChecker` to call the access decision manager in unauthenticated sessions with a `NullToken` 5.1.0 ----- diff --git a/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php b/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php index cbc411fad730b..249d8d1cf15fc 100644 --- a/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php +++ b/src/Symfony/Component/Security/Core/Authentication/AuthenticationTrustResolver.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Authentication; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -31,7 +32,7 @@ public function isAnonymous(TokenInterface $token = null) return false; } - return $token instanceof AnonymousToken; + return $token instanceof AnonymousToken || $token instanceof NullToken; } /** diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/NullToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/NullToken.php new file mode 100644 index 0000000000000..9cc2ac4afe7e5 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Token/NullToken.php @@ -0,0 +1,105 @@ + + * + * 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; + +/** + * @author Wouter de Jong + */ +class NullToken implements TokenInterface +{ + public function __toString(): string + { + return ''; + } + + public function getRoleNames(): array + { + return []; + } + + public function getCredentials() + { + return ''; + } + + public function getUser() + { + return null; + } + + public function setUser($user) + { + throw new \BadMethodCallException('Cannot set user on a NullToken.'); + } + + public function getUsername() + { + return ''; + } + + public function isAuthenticated() + { + return true; + } + + public function setAuthenticated(bool $isAuthenticated) + { + throw new \BadMethodCallException('Cannot change authentication state of NullToken.'); + } + + public function eraseCredentials() + { + } + + public function getAttributes() + { + return []; + } + + public function setAttributes(array $attributes) + { + throw new \BadMethodCallException('Cannot set attributes of NullToken.'); + } + + public function hasAttribute(string $name) + { + return false; + } + + public function getAttribute(string $name) + { + return null; + } + + public function setAttribute(string $name, $value) + { + throw new \BadMethodCallException('Cannot add attribute to NullToken.'); + } + + public function __serialize(): array + { + return []; + } + + public function __unserialize(array $data): void + { + } + + public function serialize() + { + return ''; + } + + public function unserialize($serialized) + { + } +} diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php index ac24795d99827..c51551a0d5807 100644 --- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php +++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Authorization; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; @@ -52,11 +53,11 @@ final public function isGranted($attribute, $subject = null): bool throw new AuthenticationCredentialsNotFoundException('The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL.'); } - return false; - } - - if ($this->alwaysAuthenticate || !$token->isAuthenticated()) { - $this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token)); + $token = new NullToken(); + } else { + if ($this->alwaysAuthenticate || !$token->isAuthenticated()) { + $this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token)); + } } return $this->accessDecisionManager->decide($token, [$attribute], $subject); diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php index 0c066aeee3b65..12e78ad46065f 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Tests\Authorization; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; @@ -77,7 +78,13 @@ public function testVoteWithoutAuthenticationTokenAndExceptionOnNoTokenIsFalse() { $authorizationChecker = new AuthorizationChecker($this->tokenStorage, $this->authenticationManager, $this->accessDecisionManager, false, false); - $this->assertFalse($authorizationChecker->isGranted('ROLE_FOO')); + $this->accessDecisionManager + ->expects($this->once()) + ->method('decide') + ->with($this->isInstanceOf(NullToken::class)) + ->willReturn(true); + + $this->assertTrue($authorizationChecker->isGranted('ANONYMOUS')); } /** diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php index b218e1086c62a..14f62d38b051d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; @@ -89,19 +90,7 @@ public function authenticate(RequestEvent $event) throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.'); } - if ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes) { - trigger_deprecation('symfony/security-http', '5.1', 'Using "IS_AUTHENTICATED_ANONYMOUSLY" in your access_control rules when using the authenticator Security system is deprecated, use "PUBLIC_ACCESS" instead.'); - - return; - } - - if ([self::PUBLIC_ACCESS] !== $attributes) { - throw $this->createAccessDeniedException($request, $attributes); - } - } - - if ([self::PUBLIC_ACCESS] === $attributes) { - return; + $token = new NullToken(); } if (!$token->isAuthenticated()) { diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index 154addc7c4095..e99a12b35b051 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; @@ -245,9 +246,15 @@ public function testHandleWhenTheSecurityTokenStorageHasNoTokenAndExceptionOnTok ->willReturn([['foo' => 'bar'], null]) ; + $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class); + $accessDecisionManager->expects($this->once()) + ->method('decide') + ->with($this->isInstanceOf(NullToken::class)) + ->willReturn(false); + $listener = new AccessListener( $tokenStorage, - $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(), + $accessDecisionManager, $accessMap, $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock(), false @@ -268,17 +275,21 @@ public function testHandleWhenPublicAccessIsAllowedAndExceptionOnTokenIsFalse() ->willReturn([[AccessListener::PUBLIC_ACCESS], null]) ; + $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class); + $accessDecisionManager->expects($this->once()) + ->method('decide') + ->with($this->isInstanceOf(NullToken::class), [AccessListener::PUBLIC_ACCESS]) + ->willReturn(true); + $listener = new AccessListener( $tokenStorage, - $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(), + $accessDecisionManager, $accessMap, $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock(), false ); $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); - - $this->expectNotToPerformAssertions(); } public function testHandleWhenPublicAccessWhileAuthenticated() @@ -295,17 +306,21 @@ public function testHandleWhenPublicAccessWhileAuthenticated() ->willReturn([[AccessListener::PUBLIC_ACCESS], null]) ; + $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class); + $accessDecisionManager->expects($this->once()) + ->method('decide') + ->with($this->equalTo($token), [AccessListener::PUBLIC_ACCESS]) + ->willReturn(true); + $listener = new AccessListener( $tokenStorage, - $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(), + $accessDecisionManager, $accessMap, $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock(), false ); $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); - - $this->expectNotToPerformAssertions(); } public function testHandleMWithultipleAttributesShouldBeHandledAsAnd() diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 7dfb787d4aa07..cb924c6f1e792 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.1", - "symfony/security-core": "^5.1", + "symfony/security-core": "^5.2", "symfony/http-foundation": "^4.4.7|^5.0.7", "symfony/http-kernel": "^4.4|^5.0", "symfony/polyfill-php80": "^1.15",