diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index 7a2dca2063..a0d5e52b17 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -53,6 +53,13 @@ HttpFoundation Security -------- + * The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles + instead. + * The `RoleHierarchyInterface` is deprecated and will be removed in 5.0. + * The `getReachableRoles()` method of the `RoleHierarchy` class is deprecated and will be removed in 5.0. + Use the `getReachableRoleNames()` method instead. + * The `getRoles()` method of the `TokenInterface` is deprecated. Tokens must implement the `getRoleNames()` + method instead and return roles as strings. * The `AbstractToken::serialize()`, `AbstractToken::unserialize()`, `AuthenticationException::serialize()` and `AuthenticationException::unserialize()` methods are now final, use `getState()` and `setState()` instead. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index f61211c1cc..a959f2d06a 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -226,6 +226,11 @@ Process Security -------- + * The `Role` and `SwitchUserRole` classes have been removed. + * The `RoleHierarchyInterface` has been removed. + * The `getReachableRoles()` method of the `RoleHierarchy` class has been removed. + * The `getRoles()` method has been removed from the `TokenInterface`. It has been replaced by the new + `getRoleNames()` method. * The `ContextListener::setLogoutOnUserChange()` method has been removed. * The `Symfony\Component\Security\Core\User\AdvancedUserInterface` has been removed. * The `ExpressionVoter::addExpressionLanguageProvider()` method has been removed. diff --git a/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php index 7bf03a036a..7613d01361 100644 --- a/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/TokenProcessor.php @@ -31,10 +31,16 @@ class TokenProcessor { $records['extra']['token'] = null; if (null !== $token = $this->tokenStorage->getToken()) { + if (method_exists($token, 'getRoleNames')) { + $roles = $token->getRoleNames(); + } else { + $roles = array_map(function ($role) { return $role->getRole(); }, $token->getRoles(false)); + } + $records['extra']['token'] = [ 'username' => $token->getUsername(), 'authenticated' => $token->isAuthenticated(), - 'roles' => array_map(function ($role) { return $role->getRole(); }, $token->getRoles()), + 'roles' => $roles, ]; } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php index 1b01348639..ef3f6cc9f5 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php @@ -36,7 +36,6 @@ class TokenProcessorTest extends TestCase $this->assertArrayHasKey('token', $record['extra']); $this->assertEquals($token->getUsername(), $record['extra']['token']['username']); $this->assertEquals($token->isAuthenticated(), $record['extra']['token']['authenticated']); - $roles = array_map(function ($role) { return $role->getRole(); }, $token->getRoles()); - $this->assertEquals($roles, $record['extra']['token']['roles']); + $this->assertEquals(['ROLE_USER'], $record['extra']['token']['roles']); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 3e55022f16..d33d227ff1 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -18,10 +18,12 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter; use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Http\Firewall\SwitchUserListener; @@ -91,18 +93,32 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn ]; } else { $inheritedRoles = []; - $assignedRoles = $token->getRoles(); + + if (method_exists($token, 'getRoleNames')) { + $assignedRoles = $token->getRoleNames(); + } else { + $assignedRoles = array_map(function (Role $role) { return $role->getRole(); }, $token->getRoles(false)); + } $impersonatorUser = null; - foreach ($assignedRoles as $role) { - if ($role instanceof SwitchUserRole) { - $impersonatorUser = $role->getSource()->getUsername(); - break; + if ($token instanceof SwitchUserToken) { + $impersonatorUser = $token->getOriginalToken()->getUsername(); + } else { + foreach ($token->getRoles(false) as $role) { + if ($role instanceof SwitchUserRole) { + $impersonatorUser = $role->getSource()->getUsername(); + break; + } } } if (null !== $this->roleHierarchy) { - $allRoles = $this->roleHierarchy->getReachableRoles($assignedRoles); + if ($this->roleHierarchy instanceof RoleHierarchy) { + $allRoles = $this->roleHierarchy->getReachableRoleNames($assignedRoles); + } else { + $allRoles = array_map(function (Role $role) { return (string) $role; }, $this->roleHierarchy->getReachableRoles($token->getRoles(false))); + } + foreach ($allRoles as $role) { if (!\in_array($role, $assignedRoles, true)) { $inheritedRoles[] = $role; @@ -129,8 +145,8 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn 'token_class' => $this->hasVarDumper ? new ClassStub(\get_class($token)) : \get_class($token), 'logout_url' => $logoutUrl, 'user' => $token->getUsername(), - 'roles' => array_map(function (Role $role) { return $role->getRole(); }, $assignedRoles), - 'inherited_roles' => array_unique(array_map(function (Role $role) { return $role->getRole(); }, $inheritedRoles)), + 'roles' => $assignedRoles, + 'inherited_roles' => array_unique($inheritedRoles), 'supports_role_hierarchy' => null !== $this->roleHierarchy, ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 9fbdae0bdd..08931ac718 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -98,6 +98,7 @@ %security.role_hierarchy.roles% + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index e95b354d99..ac264d2937 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -18,9 +18,12 @@ use Symfony\Bundle\SecurityBundle\Security\FirewallConfig; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; @@ -38,7 +41,7 @@ class SecurityDataCollectorTest extends TestCase public function testCollectWhenSecurityIsDisabled() { $collector = new SecurityDataCollector(); - $collector->collect($this->getRequest(), $this->getResponse()); + $collector->collect(new Request(), new Response()); $this->assertSame('security', $collector->getName()); $this->assertFalse($collector->isEnabled()); @@ -58,7 +61,7 @@ class SecurityDataCollectorTest extends TestCase { $tokenStorage = new TokenStorage(); $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); - $collector->collect($this->getRequest(), $this->getResponse()); + $collector->collect(new Request(), new Response()); $this->assertTrue($collector->isEnabled()); $this->assertFalse($collector->isAuthenticated()); @@ -80,7 +83,7 @@ class SecurityDataCollectorTest extends TestCase $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $roles)); $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); - $collector->collect($this->getRequest(), $this->getResponse()); + $collector->collect(new Request(), new Response()); $collector->lateCollect(); $this->assertTrue($collector->isEnabled()); @@ -95,6 +98,9 @@ class SecurityDataCollectorTest extends TestCase $this->assertSame('hhamon', $collector->getUser()); } + /** + * @group legacy + */ public function testCollectImpersonatedToken() { $adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', ['ROLE_ADMIN']); @@ -108,7 +114,7 @@ class SecurityDataCollectorTest extends TestCase $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles)); $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); - $collector->collect($this->getRequest(), $this->getResponse()); + $collector->collect(new Request(), new Response()); $collector->lateCollect(); $this->assertTrue($collector->isEnabled()); @@ -122,10 +128,32 @@ class SecurityDataCollectorTest extends TestCase $this->assertSame('hhamon', $collector->getUser()); } + public function testCollectSwitchUserToken() + { + $adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', ['ROLE_ADMIN']); + + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken(new SwitchUserToken('hhamon', 'P4$$w0rD', 'provider', ['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $adminToken)); + + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector->collect(new Request(), new Response()); + $collector->lateCollect(); + + $this->assertTrue($collector->isEnabled()); + $this->assertTrue($collector->isAuthenticated()); + $this->assertTrue($collector->isImpersonated()); + $this->assertSame('yceruto', $collector->getImpersonatorUser()); + $this->assertSame(SwitchUserToken::class, $collector->getTokenClass()->getValue()); + $this->assertTrue($collector->supportsRoleHierarchy()); + $this->assertSame(['ROLE_USER', 'ROLE_PREVIOUS_ADMIN'], $collector->getRoles()->getValue(true)); + $this->assertSame([], $collector->getInheritedRoles()->getValue(true)); + $this->assertSame('hhamon', $collector->getUser()); + } + public function testGetFirewall() { $firewallConfig = new FirewallConfig('dummy', 'security.request_matcher.dummy', 'security.user_checker.dummy'); - $request = $this->getRequest(); + $request = new Request(); $firewallMap = $this ->getMockBuilder(FirewallMap::class) @@ -138,7 +166,7 @@ class SecurityDataCollectorTest extends TestCase ->willReturn($firewallConfig); $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator())); - $collector->collect($request, $this->getResponse()); + $collector->collect($request, new Response()); $collector->lateCollect(); $collected = $collector->getFirewall(); @@ -158,8 +186,8 @@ class SecurityDataCollectorTest extends TestCase public function testGetFirewallReturnsNull() { - $request = $this->getRequest(); - $response = $this->getResponse(); + $request = new Request(); + $response = new Response(); // Don't inject any firewall map $collector = new SecurityDataCollector(); @@ -192,9 +220,9 @@ class SecurityDataCollectorTest extends TestCase */ public function testGetListeners() { - $request = $this->getRequest(); + $request = new Request(); $event = new GetResponseEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); - $event->setResponse($response = $this->getResponse()); + $event->setResponse($response = new Response()); $listener = $this->getMockBuilder(ListenerInterface::class)->getMock(); $listener ->expects($this->once()) @@ -345,7 +373,7 @@ class SecurityDataCollectorTest extends TestCase ->willReturn($decisionLog); $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager); - $dataCollector->collect($this->getRequest(), $this->getResponse()); + $dataCollector->collect(new Request(), new Response()); $this->assertEquals($dataCollector->getAccessDecisionLog(), $expectedDecisionLog, 'Wrong value returned by getAccessDecisionLog'); @@ -367,7 +395,7 @@ class SecurityDataCollectorTest extends TestCase [], ], [ - [new Role('ROLE_USER')], + [new Role('ROLE_USER', false)], ['ROLE_USER'], [], ], @@ -378,7 +406,7 @@ class SecurityDataCollectorTest extends TestCase ['ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'], ], [ - [new Role('ROLE_ADMIN')], + [new Role('ROLE_ADMIN', false)], ['ROLE_ADMIN'], ['ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'], ], @@ -397,20 +425,4 @@ class SecurityDataCollectorTest extends TestCase 'ROLE_OPERATOR' => ['ROLE_USER'], ]); } - - private function getRequest() - { - return $this - ->getMockBuilder('Symfony\Component\HttpFoundation\Request') - ->disableOriginalConstructor() - ->getMock(); - } - - private function getResponse() - { - return $this - ->getMockBuilder('Symfony\Component\HttpFoundation\Response') - ->disableOriginalConstructor() - ->getMock(); - } } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 23b40ea144..074e859317 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -21,7 +21,7 @@ "symfony/config": "^4.2", "symfony/dependency-injection": "^4.2", "symfony/http-kernel": "^4.1", - "symfony/security-core": "~4.2", + "symfony/security-core": "~4.3", "symfony/security-csrf": "~4.2", "symfony/security-guard": "~4.2", "symfony/security-http": "~4.2" diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index 23d3ea43be..011d802ea1 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -4,6 +4,13 @@ CHANGELOG 4.3.0 ----- +* The `Role` and `SwitchUserRole` classes are deprecated and will be removed in 5.0. Use strings for roles + instead. +* The `RoleHierarchyInterface` is deprecated and will be removed in 5.0. +* The `getReachableRoles()` method of the `RoleHierarchy` class is deprecated and will be removed in 5.0. + Use the `getReachableRoleNames()` method instead. +* The `getRoles()` method of the `TokenInterface` is deprecated. Tokens must implement the `getRoleNames()` + method instead and return roles as strings. * Made the `serialize()` and `unserialize()` methods of `AbstractToken` and `AuthenticationException` final, use `getState()`/`setState()` instead diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php index 5f9128fe7b..7a35bb056a 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Security\Core\Authentication\Provider; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -87,7 +88,12 @@ abstract class UserAuthenticationProvider implements AuthenticationProviderInter throw $e; } - $authenticatedToken = new UsernamePasswordToken($user, $token->getCredentials(), $this->providerKey, $this->getRoles($user, $token)); + if ($token instanceof SwitchUserToken) { + $authenticatedToken = new SwitchUserToken($user, $token->getCredentials(), $this->providerKey, $this->getRoles($user, $token), $token->getOriginalToken()); + } else { + $authenticatedToken = new UsernamePasswordToken($user, $token->getCredentials(), $this->providerKey, $this->getRoles($user, $token)); + } + $authenticatedToken->setAttributes($token->getAttributes()); return $authenticatedToken; @@ -110,7 +116,7 @@ abstract class UserAuthenticationProvider implements AuthenticationProviderInter { $roles = $user->getRoles(); - foreach ($token->getRoles() as $role) { + foreach ($token->getRoles(false) as $role) { if ($role instanceof SwitchUserRole) { $roles[] = $role; diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php index f879cf8a87..700634e7dc 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php @@ -26,11 +26,12 @@ abstract class AbstractToken implements TokenInterface { private $user; private $roles = []; + private $roleNames = []; private $authenticated = false; private $attributes = []; /** - * @param (Role|string)[] $roles An array of roles + * @param string[] $roles An array of roles * * @throws \InvalidArgumentException */ @@ -38,20 +39,30 @@ abstract class AbstractToken implements TokenInterface { foreach ($roles as $role) { if (\is_string($role)) { - $role = new Role($role); + $role = new Role($role, false); } elseif (!$role instanceof Role) { throw new \InvalidArgumentException(sprintf('$roles must be an array of strings, or Role instances, but got %s.', \gettype($role))); } $this->roles[] = $role; + $this->roleNames[] = (string) $role; } } + public function getRoleNames(): array + { + return $this->roleNames; + } + /** * {@inheritdoc} */ public function getRoles() { + if (0 === \func_num_args() || func_get_arg(0)) { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 4.3. Use the getRoleNames() method instead.', __METHOD__), E_USER_DEPRECATED); + } + return $this->roles; } @@ -172,7 +183,7 @@ abstract class AbstractToken implements TokenInterface */ protected function getState(): array { - return [$this->user, $this->authenticated, $this->roles, $this->attributes]; + return [$this->user, $this->authenticated, $this->roles, $this->attributes, $this->roleNames]; } /** @@ -193,7 +204,7 @@ abstract class AbstractToken implements TokenInterface */ protected function setState(array $data) { - [$this->user, $this->authenticated, $this->roles, $this->attributes] = $data; + [$this->user, $this->authenticated, $this->roles, $this->attributes, $this->roleNames] = $data; } /** @@ -225,7 +236,7 @@ abstract class AbstractToken implements TokenInterface */ public function hasAttribute($name) { - return array_key_exists($name, $this->attributes); + return \array_key_exists($name, $this->attributes); } /** @@ -239,7 +250,7 @@ abstract class AbstractToken implements TokenInterface */ public function getAttribute($name) { - if (!array_key_exists($name, $this->attributes)) { + if (!\array_key_exists($name, $this->attributes)) { throw new \InvalidArgumentException(sprintf('This token has no "%s" attribute.', $name)); } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.php index 18b2481330..c233c2bd16 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Role\Role; - /** * AnonymousToken represents an anonymous token. * @@ -25,7 +23,7 @@ class AnonymousToken extends AbstractToken /** * @param string $secret A secret used to make sure the token is created by the app and not by a malicious client * @param string|object $user The user can be a UserInterface instance, or an object implementing a __toString method or the username as a regular string - * @param Role[] $roles An array of roles + * @param string[] $roles An array of roles */ public function __construct(string $secret, $user, array $roles = []) { diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php index 733f29159a..eb407e50c9 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Role\Role; - /** * PreAuthenticatedToken implements a pre-authenticated token. * @@ -24,10 +22,10 @@ class PreAuthenticatedToken extends AbstractToken private $providerKey; /** - * @param string|object $user The user can be a UserInterface instance, or an object implementing a __toString method or the username as a regular string - * @param mixed $credentials The user credentials - * @param string $providerKey The provider key - * @param (Role|string)[] $roles An array of roles + * @param string|object $user The user can be a UserInterface instance, or an object implementing a __toString method or the username as a regular string + * @param mixed $credentials The user credentials + * @param string $providerKey The provider key + * @param string[] $roles An array of roles */ public function __construct($user, $credentials, string $providerKey, array $roles = []) { diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php index f1f5391ba7..97534b8f70 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php @@ -39,6 +39,10 @@ class TokenStorage implements TokenStorageInterface, ResetInterface */ public function setToken(TokenInterface $token = null) { + if (null !== $token && !method_exists($token, 'getRoleNames')) { + @trigger_error(sprintf('Not implementing the getRoleNames() method in %s which implements %s is deprecated since Symfony 4.3.', \get_class($token), TokenInterface::class), E_USER_DEPRECATED); + } + $this->token = $token; } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php new file mode 100644 index 0000000000..8e73876306 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php @@ -0,0 +1,60 @@ + + * + * 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; + +/** + * Token representing a user who temporarily impersonates another one. + * + * @author Christian Flothmann + */ +class SwitchUserToken extends UsernamePasswordToken +{ + private $originalToken; + + /** + * @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method + * @param mixed $credentials This usually is the password of the user + * @param string $providerKey The provider key + * @param string[] $roles An array of roles + * @param TokenInterface $originalToken The token of the user who switched to the current user + * + * @throws \InvalidArgumentException + */ + public function __construct($user, $credentials, string $providerKey, array $roles = [], TokenInterface $originalToken) + { + parent::__construct($user, $credentials, $providerKey, $roles); + + $this->originalToken = $originalToken; + } + + public function getOriginalToken(): TokenInterface + { + return $this->originalToken; + } + + /** + * {@inheritdoc} + */ + protected function getState(): array + { + return [$this->originalToken, parent::getState()]; + } + + /** + * {@inheritdoc} + */ + protected function setState(array $data) + { + [$this->originalToken, $parentData] = $data; + parent::setState($parentData); + } +} diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.php b/src/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.php index bcf6fbdd94..4d8c2522ab 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/TokenInterface.php @@ -18,6 +18,8 @@ use Symfony\Component\Security\Core\Role\Role; * * @author Fabien Potencier * @author Johannes M. Schmitt + * + * @method string[] getRoleNames() The associated roles - not implementing it is deprecated since Symfony 4.3 */ interface TokenInterface extends \Serializable { @@ -34,6 +36,8 @@ interface TokenInterface extends \Serializable * Returns the user roles. * * @return Role[] An array of Role instances + * + * @deprecated since Symfony 4.3, use the getRoleNames() method instead */ public function getRoles(); diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php index d731906eb7..79e5780f09 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Core\Authentication\Token; -use Symfony\Component\Security\Core\Role\Role; - /** * UsernamePasswordToken implements a username and password token. * @@ -24,10 +22,10 @@ class UsernamePasswordToken extends AbstractToken private $providerKey; /** - * @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method - * @param mixed $credentials This usually is the password of the user - * @param string $providerKey The provider key - * @param (Role|string)[] $roles An array of roles + * @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method + * @param mixed $credentials This usually is the password of the user + * @param string $providerKey The provider key + * @param string[] $roles An array of roles * * @throws \InvalidArgumentException */ diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php index d9530e071c..c04c2ab322 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php @@ -18,6 +18,8 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverIn use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; /** @@ -90,18 +92,34 @@ class ExpressionVoter implements VoterInterface private function getVariables(TokenInterface $token, $subject) { - if (null !== $this->roleHierarchy) { - $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); + if ($this->roleHierarchy instanceof RoleHierarchy) { + if (method_exists($token, 'getRoleNames')) { + $rolesFromToken = $token->getRoleNames(); + } else { + @trigger_error(sprintf('Not implementing the getRoleNames() method in %s which implements %s is deprecated since Symfony 4.3.', \get_class($token), TokenInterface::class), E_USER_DEPRECATED); + + $rolesFromToken = $token->getRoles(false); + } + + $roles = $this->roleHierarchy->getReachableRoleNames($rolesFromToken); + } elseif (null !== $this->roleHierarchy) { + $roles = $this->roleHierarchy->getReachableRoles($token->getRoles(false)); + } elseif (method_exists($token, 'getRoleNames')) { + $roles = $token->getRoleNames(); } else { - $roles = $token->getRoles(); + @trigger_error(sprintf('Not implementing the getRoleNames() method in %s which implements %s is deprecated since Symfony 4.3.', \get_class($token), TokenInterface::class), E_USER_DEPRECATED); + + $roles = $token->getRoles(false); } + $roles = array_map(function ($role) { return $role instanceof Role ? $role->getRole() : $role; }, $roles); + $variables = [ 'token' => $token, 'user' => $token->getUser(), 'object' => $subject, 'subject' => $subject, - 'roles' => array_map(function ($role) { return $role->getRole(); }, $roles), + 'roles' => $roles, 'trust_resolver' => $this->trustResolver, 'auth_checker' => $this->authChecker, ]; diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleHierarchyVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleHierarchyVoter.php index 06dfb4228e..4a1de12cf7 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleHierarchyVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleHierarchyVoter.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Authorization\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; /** @@ -26,6 +27,10 @@ class RoleHierarchyVoter extends RoleVoter public function __construct(RoleHierarchyInterface $roleHierarchy, string $prefix = 'ROLE_') { + if (!$roleHierarchy instanceof RoleHierarchy) { + @trigger_error(sprintf('Passing a role hierarchy to "%s" that is not an instance of "%s" is deprecated since Symfony 4.3 and support for it will be dropped in Symfony 5.0 ("%s" given).', __CLASS__, RoleHierarchy::class, \get_class($roleHierarchy)), E_USER_DEPRECATED); + } + $this->roleHierarchy = $roleHierarchy; parent::__construct($prefix); @@ -36,6 +41,18 @@ class RoleHierarchyVoter extends RoleVoter */ protected function extractRoles(TokenInterface $token) { - return $this->roleHierarchy->getReachableRoles($token->getRoles()); + if ($this->roleHierarchy instanceof RoleHierarchy) { + if (method_exists($token, 'getRoleNames')) { + $roles = $token->getRoleNames(); + } else { + @trigger_error(sprintf('Not implementing the getRoleNames() method in %s which implements %s is deprecated since Symfony 4.3.', \get_class($token), TokenInterface::class), E_USER_DEPRECATED); + + $roles = $token->getRoles(false); + } + + return $this->roleHierarchy->getReachableRoleNames($roles); + } + + return $this->roleHierarchy->getReachableRoles($token->getRoles(false)); } } diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php index fc2e6883e4..deb542255c 100644 --- a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php +++ b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php @@ -47,7 +47,7 @@ class RoleVoter implements VoterInterface $result = VoterInterface::ACCESS_DENIED; foreach ($roles as $role) { - if ($attribute === $role->getRole()) { + if ($attribute === $role) { return VoterInterface::ACCESS_GRANTED; } } @@ -58,6 +58,12 @@ class RoleVoter implements VoterInterface protected function extractRoles(TokenInterface $token) { - return $token->getRoles(); + if (method_exists($token, 'getRoleNames')) { + return $token->getRoleNames(); + } + + @trigger_error(sprintf('Not implementing the getRoleNames() method in %s which implements %s is deprecated since Symfony 4.3.', \get_class($token), TokenInterface::class), E_USER_DEPRECATED); + + return array_map(function (Role $role) { return $role->getRole(); }, $token->getRoles(false)); } } diff --git a/src/Symfony/Component/Security/Core/Role/Role.php b/src/Symfony/Component/Security/Core/Role/Role.php index 208fe2f6b7..df6ccf7636 100644 --- a/src/Symfony/Component/Security/Core/Role/Role.php +++ b/src/Symfony/Component/Security/Core/Role/Role.php @@ -15,6 +15,8 @@ namespace Symfony\Component\Security\Core\Role; * Role is a simple implementation representing a role identified by a string. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.3, to be removed in 5.0. Use strings as roles instead. */ class Role { @@ -22,6 +24,10 @@ class Role public function __construct(string $role) { + if (\func_num_args() < 2 || func_get_arg(1)) { + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3 and will be removed in 5.0. Use strings as roles instead.', __CLASS__), E_USER_DEPRECATED); + } + $this->role = $role; } @@ -34,4 +40,9 @@ class Role { return $this->role; } + + public function __toString(): string + { + return $this->role; + } } diff --git a/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php b/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php index f4a2e5b622..d725000d1b 100644 --- a/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php +++ b/src/Symfony/Component/Security/Core/Role/RoleHierarchy.php @@ -36,6 +36,8 @@ class RoleHierarchy implements RoleHierarchyInterface */ public function getReachableRoles(array $roles) { + @trigger_error(sprintf('The %s() method is deprecated since Symfony 4.3 and will be removed in 5.0. Use roles as strings and the getReachableRoleNames() method instead.', __METHOD__), E_USER_DEPRECATED); + $reachableRoles = $roles; foreach ($roles as $role) { if (!isset($this->map[$role->getRole()])) { @@ -50,6 +52,37 @@ class RoleHierarchy implements RoleHierarchyInterface return $reachableRoles; } + /** + * @param string[] $roles + * + * @return string[] + */ + public function getReachableRoleNames(array $roles): array + { + $reachableRoles = $roles = array_map( + function ($role) { + if ($role instanceof Role) { + return $role->getRole(); + } + + return $role; + }, + $roles + ); + + foreach ($roles as $role) { + if (!isset($this->map[$role])) { + continue; + } + + foreach ($this->map[$role] as $r) { + $reachableRoles[] = $r; + } + } + + return $reachableRoles; + } + protected function buildRoleMap() { $this->map = []; diff --git a/src/Symfony/Component/Security/Core/Role/RoleHierarchyInterface.php b/src/Symfony/Component/Security/Core/Role/RoleHierarchyInterface.php index 1a86db9901..3a5112f7e5 100644 --- a/src/Symfony/Component/Security/Core/Role/RoleHierarchyInterface.php +++ b/src/Symfony/Component/Security/Core/Role/RoleHierarchyInterface.php @@ -15,6 +15,8 @@ namespace Symfony\Component\Security\Core\Role; * RoleHierarchyInterface is the interface for a role hierarchy. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.3, to be removed in 5.0. */ interface RoleHierarchyInterface { diff --git a/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php b/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php index d26c7067a2..85d7ddb1a5 100644 --- a/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php +++ b/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php @@ -18,9 +18,12 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; * another one. * * @author Fabien Potencier + * + * @deprecated since version 4.3, to be removed in 5.0. Use strings as roles instead. */ class SwitchUserRole extends Role { + private $deprecationTriggered = false; private $source; /** @@ -29,7 +32,13 @@ class SwitchUserRole extends Role */ public function __construct(string $role, TokenInterface $source) { - parent::__construct($role); + if ($triggerDeprecation = \func_num_args() < 3 || func_get_arg(2)) { + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3 and will be removed in 5.0. Use strings as roles instead.', __CLASS__), E_USER_DEPRECATED); + + $this->deprecationTriggered = true; + } + + parent::__construct($role, $triggerDeprecation); $this->source = $source; } @@ -41,6 +50,12 @@ class SwitchUserRole extends Role */ public function getSource() { + if (!$this->deprecationTriggered && (\func_num_args() < 1 || func_get_arg(0))) { + @trigger_error(sprintf('The "%s" class is deprecated since version 4.3 and will be removed in 5.0. Use strings as roles instead.', __CLASS__), E_USER_DEPRECATED); + + $this->deprecationTriggered = true; + } + return $this->source; } } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php index a101208d68..d8d18ddeb9 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php @@ -70,7 +70,7 @@ class PreAuthenticatedAuthenticationProviderTest extends TestCase $this->assertInstanceOf('Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken', $token); $this->assertEquals('pass', $token->getCredentials()); $this->assertEquals('key', $token->getProviderKey()); - $this->assertEquals([], $token->getRoles()); + $this->assertEquals([], $token->getRoleNames()); $this->assertEquals(['foo' => 'bar'], $token->getAttributes(), '->authenticate() copies token attributes'); $this->assertSame($user, $token->getUser()); } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php index 26287693d6..37d9a42a96 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/RememberMeAuthenticationProviderTest.php @@ -14,7 +14,6 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Provider\RememberMeAuthenticationProvider; use Symfony\Component\Security\Core\Exception\DisabledException; -use Symfony\Component\Security\Core\Role\Role; class RememberMeAuthenticationProviderTest extends TestCase { @@ -78,7 +77,7 @@ class RememberMeAuthenticationProviderTest extends TestCase $this->assertInstanceOf('Symfony\Component\Security\Core\Authentication\Token\RememberMeToken', $authToken); $this->assertSame($user, $authToken->getUser()); - $this->assertEquals([new Role('ROLE_FOO')], $authToken->getRoles()); + $this->assertEquals(['ROLE_FOO'], $authToken->getRoleNames()); $this->assertEquals('', $authToken->getCredentials()); } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php index 7c306d2f1f..e62ac3f9f5 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php @@ -12,11 +12,11 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Exception\AccountExpiredException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\CredentialsExpiredException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\SwitchUserRole; class UserAuthenticationProviderTest extends TestCase @@ -189,11 +189,14 @@ class UserAuthenticationProviderTest extends TestCase $this->assertInstanceOf('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $authToken); $this->assertSame($user, $authToken->getUser()); - $this->assertEquals([new Role('ROLE_FOO')], $authToken->getRoles()); + $this->assertEquals(['ROLE_FOO'], $authToken->getRoleNames()); $this->assertEquals('foo', $authToken->getCredentials()); $this->assertEquals(['foo' => 'bar'], $authToken->getAttributes(), '->authenticate() copies token attributes'); } + /** + * @group legacy + */ public function testAuthenticateWithPreservingRoleSwitchUserRole() { $user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); @@ -224,12 +227,40 @@ class UserAuthenticationProviderTest extends TestCase $this->assertInstanceOf('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $authToken); $this->assertSame($user, $authToken->getUser()); - $this->assertContains(new Role('ROLE_FOO'), $authToken->getRoles(), '', false, false); + $this->assertContains('ROLE_FOO', $authToken->getRoleNames(), '', false, false); $this->assertContains($switchUserRole, $authToken->getRoles(), '', false, false); $this->assertEquals('foo', $authToken->getCredentials()); $this->assertEquals(['foo' => 'bar'], $authToken->getAttributes(), '->authenticate() copies token attributes'); } + public function testAuthenticatePreservesOriginalToken() + { + $user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); + $user->expects($this->once()) + ->method('getRoles') + ->will($this->returnValue(['ROLE_FOO'])) + ; + + $provider = $this->getProvider(); + $provider->expects($this->once()) + ->method('retrieveUser') + ->will($this->returnValue($user)) + ; + + $originalToken = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = new SwitchUserToken($this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(), 'foo', 'key', [], $originalToken); + $token->setAttributes(['foo' => 'bar']); + + $authToken = $provider->authenticate($token); + + $this->assertInstanceOf(SwitchUserToken::class, $authToken); + $this->assertSame($originalToken, $authToken->getOriginalToken()); + $this->assertSame($user, $authToken->getUser()); + $this->assertContains('ROLE_FOO', $authToken->getRoleNames(), '', false, false); + $this->assertEquals('foo', $authToken->getCredentials()); + $this->assertEquals(['foo' => 'bar'], $authToken->getAttributes(), '->authenticate() copies token attributes'); + } + protected function getSupportedToken() { $mock = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken')->setMethods(['getCredentials', 'getProviderKey', 'getRoles'])->disableOriginalConstructor()->getMock(); diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php index 066ca6892c..e87d25a789 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AbstractTokenTest.php @@ -14,61 +14,14 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Role\Role; -use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Core\User\User; - -class TestUser -{ - protected $name; - - public function __construct($name) - { - $this->name = $name; - } - - public function __toString() - { - return $this->name; - } -} - -class ConcreteToken extends AbstractToken -{ - private $credentials = 'credentials_value'; - - public function __construct($user, array $roles = []) - { - parent::__construct($roles); - - $this->setUser($user); - } - - /** - * {@inheritdoc} - */ - public function serialize() - { - $serialized = [$this->credentials, parent::serialize(true)]; - - return $this->doSerialize($serialized, \func_num_args() ? \func_get_arg(0) : null); - } - - public function unserialize($serialized) - { - list($this->credentials, $parentStr) = unserialize($serialized); - parent::unserialize($parentStr); - } - - public function getCredentials() - { - } -} +use Symfony\Component\Security\Core\User\UserInterface; class AbstractTokenTest extends TestCase { public function testGetUsername() { - $token = $this->getToken(['ROLE_FOO']); + $token = new ConcreteToken(['ROLE_FOO']); $token->setUser('fabien'); $this->assertEquals('fabien', $token->getUsername()); @@ -83,7 +36,7 @@ class AbstractTokenTest extends TestCase public function testEraseCredentials() { - $token = $this->getToken(['ROLE_FOO']); + $token = new ConcreteToken(['ROLE_FOO']); $user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); $user->expects($this->once())->method('eraseCredentials'); @@ -94,19 +47,22 @@ class AbstractTokenTest extends TestCase public function testSerialize() { - $token = $this->getToken(['ROLE_FOO', new Role('ROLE_BAR')]); + $token = new ConcreteToken(['ROLE_FOO', new Role('ROLE_BAR', false)]); $token->setAttributes(['foo' => 'bar']); $uToken = unserialize(serialize($token)); - $this->assertEquals($token->getRoles(), $uToken->getRoles()); + $this->assertEquals($token->getRoleNames(), $uToken->getRoleNames()); $this->assertEquals($token->getAttributes(), $uToken->getAttributes()); } + /** + * @group legacy + */ public function testSerializeWithRoleObjects() { $user = new User('name', 'password', [new Role('ROLE_FOO'), new Role('ROLE_BAR')]); - $token = new ConcreteToken($user, $user->getRoles()); + $token = new ConcreteToken($user->getRoles(), $user); $serialized = serialize($token); $unserialized = unserialize($serialized); @@ -116,35 +72,42 @@ class AbstractTokenTest extends TestCase $this->assertEquals($roles, $user->getRoles()); } - public function testSerializeParent() - { - $user = new TestUser('fabien'); - $token = new ConcreteToken($user, ['ROLE_FOO']); - - $parentToken = new ConcreteToken($user, [new SwitchUserRole('ROLE_PREVIOUS', $token)]); - $uToken = unserialize(serialize($parentToken)); - - $this->assertEquals( - current($parentToken->getRoles())->getSource()->getUser(), - current($uToken->getRoles())->getSource()->getUser() - ); - } - public function testConstructor() { - $token = $this->getToken(['ROLE_FOO']); + $token = new ConcreteToken(['ROLE_FOO']); + $this->assertEquals(['ROLE_FOO'], $token->getRoleNames()); + } + + /** + * @group legacy + */ + public function testConstructorWithRoleObjects() + { + $token = new ConcreteToken([new Role('ROLE_FOO')]); + $this->assertEquals(['ROLE_FOO'], $token->getRoleNames()); + + $token = new ConcreteToken([new Role('ROLE_FOO'), 'ROLE_BAR']); + $this->assertEquals(['ROLE_FOO', 'ROLE_BAR'], $token->getRoleNames()); + } + + /** + * @group legacy + */ + public function testGetRoles() + { + $token = new ConcreteToken(['ROLE_FOO']); $this->assertEquals([new Role('ROLE_FOO')], $token->getRoles()); - $token = $this->getToken([new Role('ROLE_FOO')]); + $token = new ConcreteToken([new Role('ROLE_FOO')]); $this->assertEquals([new Role('ROLE_FOO')], $token->getRoles()); - $token = $this->getToken([new Role('ROLE_FOO'), 'ROLE_BAR']); + $token = new ConcreteToken([new Role('ROLE_FOO'), 'ROLE_BAR']); $this->assertEquals([new Role('ROLE_FOO'), new Role('ROLE_BAR')], $token->getRoles()); } public function testAuthenticatedFlag() { - $token = $this->getToken(); + $token = new ConcreteToken(); $this->assertFalse($token->isAuthenticated()); $token->setAuthenticated(true); @@ -157,7 +120,7 @@ class AbstractTokenTest extends TestCase public function testAttributes() { $attributes = ['foo' => 'bar']; - $token = $this->getToken(); + $token = new ConcreteToken(); $token->setAttributes($attributes); $this->assertEquals($attributes, $token->getAttributes(), '->getAttributes() returns the token attributes'); @@ -181,7 +144,7 @@ class AbstractTokenTest extends TestCase */ public function testSetUser($user) { - $token = $this->getToken(); + $token = new ConcreteToken(); $token->setUser($user); $this->assertSame($user, $token->getUser()); } @@ -202,7 +165,7 @@ class AbstractTokenTest extends TestCase */ public function testSetUserSetsAuthenticatedToFalseWhenUserChanges($firstUser, $secondUser) { - $token = $this->getToken(); + $token = new ConcreteToken(); $token->setAuthenticated(true); $this->assertTrue($token->isAuthenticated()); @@ -236,7 +199,7 @@ class AbstractTokenTest extends TestCase */ public function testSetUserSetsAuthenticatedToFalseWhenUserChangesAdvancedUser($firstUser, $secondUser) { - $token = $this->getToken(); + $token = new ConcreteToken(); $token->setAuthenticated(true); $this->assertTrue($token->isAuthenticated()); @@ -275,7 +238,7 @@ class AbstractTokenTest extends TestCase */ public function testSetUserDoesNotSetAuthenticatedToFalseWhenUserDoesNotChange($user) { - $token = $this->getToken(); + $token = new ConcreteToken(); $token->setAuthenticated(true); $this->assertTrue($token->isAuthenticated()); @@ -285,9 +248,48 @@ class AbstractTokenTest extends TestCase $token->setUser($user); $this->assertTrue($token->isAuthenticated()); } +} - protected function getToken(array $roles = []) +class TestUser +{ + protected $name; + + public function __construct($name) + { + $this->name = $name; + } + + public function __toString() + { + return $this->name; + } +} + +class ConcreteToken extends AbstractToken +{ + private $credentials = 'credentials_value'; + + public function __construct(array $roles = [], UserInterface $user = null) + { + parent::__construct($roles); + + if (null !== $user) { + $this->setUser($user); + } + } + + public function serialize() + { + return serialize([$this->credentials, parent::serialize()]); + } + + public function unserialize($serialized) + { + list($this->credentials, $parentStr) = unserialize($serialized); + parent::unserialize($parentStr); + } + + public function getCredentials() { - return $this->getMockForAbstractClass('Symfony\Component\Security\Core\Authentication\Token\AbstractToken', [$roles]); } } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php index 7024cc5356..1b00fd4e76 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/AnonymousTokenTest.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; -use Symfony\Component\Security\Core\Role\Role; class AnonymousTokenTest extends TestCase { @@ -23,7 +22,7 @@ class AnonymousTokenTest extends TestCase $this->assertTrue($token->isAuthenticated()); $token = new AnonymousToken('foo', 'bar', ['ROLE_FOO']); - $this->assertEquals([new Role('ROLE_FOO')], $token->getRoles()); + $this->assertEquals(['ROLE_FOO'], $token->getRoleNames()); } public function testGetKey() diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php index 7e64aa1a5d..78cda619b2 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/PreAuthenticatedTokenTest.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; -use Symfony\Component\Security\Core\Role\Role; class PreAuthenticatedTokenTest extends TestCase { @@ -24,7 +23,7 @@ class PreAuthenticatedTokenTest extends TestCase $token = new PreAuthenticatedToken('foo', 'bar', 'key', ['ROLE_FOO']); $this->assertTrue($token->isAuthenticated()); - $this->assertEquals([new Role('ROLE_FOO')], $token->getRoles()); + $this->assertEquals(['ROLE_FOO'], $token->getRoleNames()); $this->assertEquals('key', $token->getProviderKey()); } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/RememberMeTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/RememberMeTokenTest.php index 8fa0307108..fea6161d77 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/RememberMeTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/RememberMeTokenTest.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; -use Symfony\Component\Security\Core\Role\Role; class RememberMeTokenTest extends TestCase { @@ -24,7 +23,7 @@ class RememberMeTokenTest extends TestCase $this->assertEquals('fookey', $token->getProviderKey()); $this->assertEquals('foo', $token->getSecret()); - $this->assertEquals([new Role('ROLE_FOO')], $token->getRoles()); + $this->assertEquals(['ROLE_FOO'], $token->getRoleNames()); $this->assertSame($user, $token->getUser()); $this->assertTrue($token->isAuthenticated()); } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php index fd30eea3c5..43261b3bd2 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/TokenStorageTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token\Storage; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; class TokenStorageTest extends TestCase { @@ -20,7 +21,7 @@ class TokenStorageTest extends TestCase { $tokenStorage = new TokenStorage(); $this->assertNull($tokenStorage->getToken()); - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = new UsernamePasswordToken('username', 'password', 'provider'); $tokenStorage->setToken($token); $this->assertSame($token, $tokenStorage->getToken()); } diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php new file mode 100644 index 0000000000..5841250959 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php @@ -0,0 +1,41 @@ + + * + * 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\SwitchUserToken; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; + +class SwitchUserTokenTest extends TestCase +{ + public function testSerialize() + { + $originalToken = new UsernamePasswordToken('user', 'foo', 'provider-key', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']); + $token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken); + + $unserializedToken = unserialize(serialize($token)); + + $this->assertInstanceOf(SwitchUserToken::class, $unserializedToken); + $this->assertSame('admin', $unserializedToken->getUsername()); + $this->assertSame('bar', $unserializedToken->getCredentials()); + $this->assertSame('provider-key', $unserializedToken->getProviderKey()); + $this->assertEquals(['ROLE_USER'], $unserializedToken->getRoleNames()); + + $unserializedOriginalToken = $unserializedToken->getOriginalToken(); + + $this->assertInstanceOf(UsernamePasswordToken::class, $unserializedOriginalToken); + $this->assertSame('user', $unserializedOriginalToken->getUsername()); + $this->assertSame('foo', $unserializedOriginalToken->getCredentials()); + $this->assertSame('provider-key', $unserializedOriginalToken->getProviderKey()); + $this->assertEquals(['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'], $unserializedOriginalToken->getRoleNames()); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php index 87dceea3d8..ab0abaf653 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/UsernamePasswordTokenTest.php @@ -13,7 +13,6 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Role\Role; class UsernamePasswordTokenTest extends TestCase { @@ -23,7 +22,7 @@ class UsernamePasswordTokenTest extends TestCase $this->assertFalse($token->isAuthenticated()); $token = new UsernamePasswordToken('foo', 'bar', 'key', ['ROLE_FOO']); - $this->assertEquals([new Role('ROLE_FOO')], $token->getRoles()); + $this->assertEquals(['ROLE_FOO'], $token->getRoleNames()); $this->assertTrue($token->isAuthenticated()); $this->assertEquals('key', $token->getProviderKey()); } diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php index 6a07c94f20..f2dcb6fbc3 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Core\Tests\Authorization; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationChecker; class AuthorizationCheckerTest extends TestCase @@ -37,10 +38,10 @@ class AuthorizationCheckerTest extends TestCase public function testVoteAuthenticatesTokenIfNecessary() { - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = new UsernamePasswordToken('username', 'password', 'provider'); $this->tokenStorage->setToken($token); - $newToken = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $newToken = new UsernamePasswordToken('username', 'password', 'provider'); $this->authenticationManager ->expects($this->once()) @@ -79,11 +80,7 @@ class AuthorizationCheckerTest extends TestCase */ public function testIsGranted($decide) { - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); - $token - ->expects($this->once()) - ->method('isAuthenticated') - ->will($this->returnValue(true)); + $token = new UsernamePasswordToken('username', 'password', 'provider', ['ROLE_USER']); $this->accessDecisionManager ->expects($this->once()) diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php index 632c6d0ab0..d377718842 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Tests\Authorization\Voter; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; @@ -20,6 +21,7 @@ use Symfony\Component\Security\Core\Role\Role; class ExpressionVoterTest extends TestCase { /** + * @group legacy * @dataProvider getVoteTests */ public function testVote($roles, $attributes, $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true) @@ -29,6 +31,16 @@ class ExpressionVoterTest extends TestCase $this->assertSame($expected, $voter->vote($this->getToken($roles, $tokenExpectsGetRoles), null, $attributes)); } + /** + * @dataProvider getVoteTests + */ + public function testVoteWithTokenThatReturnsRoleNames($roles, $attributes, $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true) + { + $voter = new ExpressionVoter($this->createExpressionLanguage($expressionLanguageExpectsEvaluate), $this->createTrustResolver(), $this->createAuthorizationChecker()); + + $this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles, $tokenExpectsGetRoles), null, $attributes)); + } + public function getVoteTests() { return [ @@ -58,6 +70,19 @@ class ExpressionVoterTest extends TestCase return $token; } + protected function getTokenWithRoleNames(array $roles, $tokenExpectsGetRoles = true) + { + $token = $this->getMockBuilder(AbstractToken::class)->getMock(); + + if ($tokenExpectsGetRoles) { + $token->expects($this->once()) + ->method('getRoleNames') + ->will($this->returnValue($roles)); + } + + return $token; + } + protected function createExpressionLanguage($expressionLanguageExpectsEvaluate = true) { $mock = $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\ExpressionLanguage')->getMock(); diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php index 14705fbf96..ec21779a68 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Security\Core\Role\RoleHierarchy; class RoleHierarchyVoterTest extends RoleVoterTest { /** + * @group legacy * @dataProvider getVoteTests */ public function testVote($roles, $attributes, $expected) @@ -27,6 +28,16 @@ class RoleHierarchyVoterTest extends RoleVoterTest $this->assertSame($expected, $voter->vote($this->getToken($roles), null, $attributes)); } + /** + * @dataProvider getVoteTests + */ + public function testVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected) + { + $voter = new RoleHierarchyVoter(new RoleHierarchy(['ROLE_FOO' => ['ROLE_FOOBAR']])); + + $this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes)); + } + public function getVoteTests() { return array_merge(parent::getVoteTests(), [ @@ -35,6 +46,18 @@ class RoleHierarchyVoterTest extends RoleVoterTest } /** + * @group legacy + * @dataProvider getLegacyVoteOnRoleObjectsTests + */ + public function testVoteOnRoleObjects($roles, $attributes, $expected) + { + $voter = new RoleHierarchyVoter(new RoleHierarchy(['ROLE_FOO' => ['ROLE_FOOBAR']])); + + $this->assertSame($expected, $voter->vote($this->getToken($roles), null, $attributes)); + } + + /** + * @group legacy * @dataProvider getVoteWithEmptyHierarchyTests */ public function testVoteWithEmptyHierarchy($roles, $attributes, $expected) @@ -44,6 +67,16 @@ class RoleHierarchyVoterTest extends RoleVoterTest $this->assertSame($expected, $voter->vote($this->getToken($roles), null, $attributes)); } + /** + * @dataProvider getVoteWithEmptyHierarchyTests + */ + public function testVoteWithEmptyHierarchyUsingTokenThatReturnsRoleNames($roles, $attributes, $expected) + { + $voter = new RoleHierarchyVoter(new RoleHierarchy([])); + + $this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes)); + } + public function getVoteWithEmptyHierarchyTests() { return parent::getVoteTests(); diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php index 5fb45e08b3..6a1034417c 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Core\Tests\Authorization\Voter; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Role\Role; @@ -19,6 +20,7 @@ use Symfony\Component\Security\Core\Role\Role; class RoleVoterTest extends TestCase { /** + * @group legacy * @dataProvider getVoteTests */ public function testVote($roles, $attributes, $expected) @@ -28,6 +30,16 @@ class RoleVoterTest extends TestCase $this->assertSame($expected, $voter->vote($this->getToken($roles), null, $attributes)); } + /** + * @dataProvider getVoteTests + */ + public function testVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected) + { + $voter = new RoleVoter(); + + $this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes)); + } + public function getVoteTests() { return [ @@ -41,6 +53,23 @@ class RoleVoterTest extends TestCase // Test mixed Types [[], [[]], VoterInterface::ACCESS_ABSTAIN], [[], [new \stdClass()], VoterInterface::ACCESS_ABSTAIN], + ]; + } + + /** + * @group legacy + * @dataProvider getLegacyVoteOnRoleObjectsTests + */ + public function testVoteOnRoleObjects($roles, $attributes, $expected) + { + $voter = new RoleVoter(); + + $this->assertSame($expected, $voter->vote($this->getToken($roles), null, $attributes)); + } + + public function getLegacyVoteOnRoleObjectsTests() + { + return [ [['ROLE_BAR'], [new Role('ROLE_BAR')], VoterInterface::ACCESS_GRANTED], [['ROLE_BAR'], [new Role('ROLE_FOO')], VoterInterface::ACCESS_DENIED], ]; @@ -58,4 +87,14 @@ class RoleVoterTest extends TestCase return $token; } + + protected function getTokenWithRoleNames(array $roles) + { + $token = $this->getMockBuilder(AbstractToken::class)->getMock(); + $token->expects($this->once()) + ->method('getRoleNames') + ->will($this->returnValue($roles)); + + return $token; + } } diff --git a/src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php b/src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php index 4451f391ad..c33fb953a1 100644 --- a/src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Role/RoleHierarchyTest.php @@ -17,6 +17,9 @@ use Symfony\Component\Security\Core\Role\RoleHierarchy; class RoleHierarchyTest extends TestCase { + /** + * @group legacy + */ public function testGetReachableRoles() { $role = new RoleHierarchy([ @@ -30,4 +33,18 @@ class RoleHierarchyTest extends TestCase $this->assertEquals([new Role('ROLE_FOO'), new Role('ROLE_ADMIN'), new Role('ROLE_USER')], $role->getReachableRoles([new Role('ROLE_FOO'), new Role('ROLE_ADMIN')])); $this->assertEquals([new Role('ROLE_SUPER_ADMIN'), new Role('ROLE_ADMIN'), new Role('ROLE_FOO'), new Role('ROLE_USER')], $role->getReachableRoles([new Role('ROLE_SUPER_ADMIN')])); } + + public function testGetReachableRoleNames() + { + $role = new RoleHierarchy(array( + 'ROLE_ADMIN' => array('ROLE_USER'), + 'ROLE_SUPER_ADMIN' => array('ROLE_ADMIN', 'ROLE_FOO'), + )); + + $this->assertEquals(array('ROLE_USER'), $role->getReachableRoleNames(array('ROLE_USER'))); + $this->assertEquals(array('ROLE_FOO'), $role->getReachableRoleNames(array('ROLE_FOO'))); + $this->assertEquals(array('ROLE_ADMIN', 'ROLE_USER'), $role->getReachableRoleNames(array('ROLE_ADMIN'))); + $this->assertEquals(array('ROLE_FOO', 'ROLE_ADMIN', 'ROLE_USER'), $role->getReachableRoleNames(array('ROLE_FOO', 'ROLE_ADMIN'))); + $this->assertEquals(array('ROLE_SUPER_ADMIN', 'ROLE_ADMIN', 'ROLE_FOO', 'ROLE_USER'), $role->getReachableRoleNames(array('ROLE_SUPER_ADMIN'))); + } } diff --git a/src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php b/src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php index edf779413b..e872a8c36b 100644 --- a/src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Role/RoleTest.php @@ -14,6 +14,9 @@ namespace Symfony\Component\Security\Core\Tests\Role; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Role\Role; +/** + * @group legacy + */ class RoleTest extends TestCase { public function testGetRole() diff --git a/src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php b/src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php index e40471733c..88f6a18abf 100644 --- a/src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Role/SwitchUserRoleTest.php @@ -14,6 +14,9 @@ namespace Symfony\Component\Security\Core\Tests\Role; use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Role\SwitchUserRole; +/** + * @group legacy + */ class SwitchUserRoleTest extends TestCase { public function testGetSource() diff --git a/src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php b/src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php index 9e3646da45..ac792b6a87 100644 --- a/src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php +++ b/src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Security\Guard\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; -use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -28,9 +27,9 @@ class PostAuthenticationGuardToken extends AbstractToken implements GuardTokenIn private $providerKey; /** - * @param UserInterface $user The user! - * @param string $providerKey The provider (firewall) key - * @param (Role|string)[] $roles An array of roles + * @param UserInterface $user The user! + * @param string $providerKey The provider (firewall) key + * @param string[] $roles An array of roles * * @throws \InvalidArgumentException */ diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index c7a6dc96c8..e86dc5f230 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverIn use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; @@ -189,10 +190,14 @@ class ContextListener implements ListenerInterface if (null !== $this->logger) { $context = ['provider' => \get_class($provider), 'username' => $refreshedUser->getUsername()]; - foreach ($token->getRoles() as $role) { - if ($role instanceof SwitchUserRole) { - $context['impersonator_username'] = $role->getSource()->getUsername(); - break; + if ($token instanceof SwitchUserToken) { + $context['impersonator_username'] = $token->getOriginalToken()->getUsername(); + } else { + foreach ($token->getRoles(false) as $role) { + if ($role instanceof SwitchUserRole) { + $context['impersonator_username'] = $role->getSource(false)->getUsername(); + break; + } } } diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index 4aff3b4581..102ab30615 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -17,8 +17,8 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; @@ -122,7 +122,7 @@ class SwitchUserListener implements ListenerInterface $token = $this->tokenStorage->getToken(); $originalToken = $this->getOriginalToken($token); - if (false !== $originalToken) { + if (null !== $originalToken) { if ($token->getUsername() === $username) { return $token; } @@ -146,9 +146,9 @@ class SwitchUserListener implements ListenerInterface $this->userChecker->checkPostAuth($user); $roles = $user->getRoles(); - $roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->tokenStorage->getToken()); + $roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->tokenStorage->getToken(), false); - $token = new UsernamePasswordToken($user, $user->getPassword(), $this->providerKey, $roles); + $token = new SwitchUserToken($user, $user->getPassword(), $this->providerKey, $roles, $token); if (null !== $this->dispatcher) { $switchEvent = new SwitchUserEvent($request, $token->getUser(), $token); @@ -169,7 +169,7 @@ class SwitchUserListener implements ListenerInterface */ private function attemptExitUser(Request $request) { - if (false === $original = $this->getOriginalToken($this->tokenStorage->getToken())) { + if (null === ($currentToken = $this->tokenStorage->getToken()) || null === $original = $this->getOriginalToken($currentToken)) { throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.'); } @@ -183,19 +183,18 @@ class SwitchUserListener implements ListenerInterface return $original; } - /** - * Gets the original Token from a switched one. - * - * @return TokenInterface|false The original TokenInterface instance, false if the current TokenInterface is not switched - */ - private function getOriginalToken(TokenInterface $token) + private function getOriginalToken(TokenInterface $token): ?TokenInterface { - foreach ($token->getRoles() as $role) { + if ($token instanceof SwitchUserToken) { + return $token->getOriginalToken(); + } + + foreach ($token->getRoles(false) as $role) { if ($role instanceof SwitchUserRole) { return $role->getSource(); } } - return false; + return null; } } diff --git a/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php b/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php index 25108e482d..8bcb960aa6 100644 --- a/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolve use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; @@ -35,7 +36,7 @@ class UserValueResolverTest extends TestCase public function testResolveNoUser() { $mock = $this->getMockBuilder(UserInterface::class)->getMock(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $token = new UsernamePasswordToken('username', 'password', 'provider'); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -57,8 +58,7 @@ class UserValueResolverTest extends TestCase public function testResolve() { $user = $this->getMockBuilder(UserInterface::class)->getMock(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); - $token->expects($this->any())->method('getUser')->willReturn($user); + $token = new UsernamePasswordToken($user, 'password', 'provider'); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -72,8 +72,7 @@ class UserValueResolverTest extends TestCase public function testIntegration() { $user = $this->getMockBuilder(UserInterface::class)->getMock(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); - $token->expects($this->any())->method('getUser')->willReturn($user); + $token = new UsernamePasswordToken($user, 'password', 'provider'); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -83,7 +82,7 @@ class UserValueResolverTest extends TestCase public function testIntegrationNoUser() { - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $token = new UsernamePasswordToken('username', 'password', 'provider'); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index f26b72fc14..1468df6612 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Core\User\User; @@ -93,7 +94,7 @@ class SwitchUserListenerTest extends TestCase public function testExitUserUpdatesToken() { $originalToken = new UsernamePasswordToken('username', '', 'key', []); - $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', [new SwitchUserRole('ROLE_PREVIOUS', $originalToken)])); + $this->tokenStorage->setToken(new SwitchUserToken('username', '', 'key', [new SwitchUserRole('ROLE_PREVIOUS', $originalToken, false)], $originalToken)); $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); @@ -107,6 +108,22 @@ class SwitchUserListenerTest extends TestCase $this->assertSame($originalToken, $this->tokenStorage->getToken()); } + /** + * @group legacy + */ + public function testExitUserBasedOnSwitchUserRoleUpdatesToken() + { + $originalToken = new UsernamePasswordToken('username', '', 'key', array()); + $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', array(new SwitchUserRole('ROLE_PREVIOUS', $originalToken, false)), $originalToken)); + + $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); + + $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); + $listener->handle($this->event); + + $this->assertSame($originalToken, $this->tokenStorage->getToken()); + } + public function testExitUserDispatchesEventWithRefreshedUser() { $originalUser = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); @@ -118,7 +135,7 @@ class SwitchUserListenerTest extends TestCase ->with($originalUser) ->willReturn($refreshedUser); $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); - $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', [new SwitchUserRole('ROLE_PREVIOUS', $originalToken)])); + $this->tokenStorage->setToken(new SwitchUserToken('username', '', 'key', [new SwitchUserRole('ROLE_PREVIOUS', $originalToken, false)], $originalToken)); $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); @@ -142,7 +159,7 @@ class SwitchUserListenerTest extends TestCase ->expects($this->never()) ->method('refreshUser'); $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); - $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', [new SwitchUserRole('ROLE_PREVIOUS', $originalToken)])); + $this->tokenStorage->setToken(new SwitchUserToken('username', '', 'key', [new SwitchUserRole('ROLE_PREVIOUS', $originalToken, false)], $originalToken)); $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); diff --git a/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php index 727dde0f81..1216660247 100644 --- a/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php @@ -95,7 +95,7 @@ class LogoutUrlGeneratorTest extends TestCase public function testGuessFromTokenWithoutProviderKeyFallbacksToCurrentFirewall() { - $this->tokenStorage->setToken($this->getMockBuilder(TokenInterface::class)->getMock()); + $this->tokenStorage->setToken(new UsernamePasswordToken('username', 'password', 'provider')); $this->generator->registerListener('secured_area', '/logout', null, null); $this->generator->setCurrentFirewall('secured_area'); diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 7047eba06e..7bfac0fdcf 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.1.3", - "symfony/security-core": "~3.4|~4.0", + "symfony/security-core": "~4.3", "symfony/event-dispatcher": "~3.4|~4.0", "symfony/http-foundation": "~3.4|~4.0", "symfony/http-kernel": "~3.4|~4.0", diff --git a/src/Symfony/Component/Workflow/EventListener/GuardListener.php b/src/Symfony/Component/Workflow/EventListener/GuardListener.php index 3a9b417f67..60cd00ed20 100644 --- a/src/Symfony/Component/Workflow/EventListener/GuardListener.php +++ b/src/Symfony/Component/Workflow/EventListener/GuardListener.php @@ -14,6 +14,8 @@ namespace Symfony\Component\Workflow\EventListener; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Workflow\Event\GuardEvent; @@ -80,19 +82,23 @@ class GuardListener throw new InvalidTokenConfigurationException(sprintf('There are no tokens available for workflow %s.', $event->getWorkflowName())); } - if (null !== $this->roleHierarchy) { - $roles = $this->roleHierarchy->getReachableRoles($token->getRoles()); + if (method_exists($token, 'getRoleNames')) { + $roles = $token->getRoleNames(); } else { - $roles = $token->getRoles(); + $roles = array_map(function (Role $role) { return $role->getRole(); }, $token->getRoles(false)); + } + + if ($this->roleHierarchy instanceof RoleHierarchy) { + $roles = $this->roleHierarchy->getReachableRoleNames($roles); + } elseif (null !== $this->roleHierarchy) { + $roles = $this->roleHierarchy->getReachableRoles($token->getRoles(false)); } $variables = [ 'token' => $token, 'user' => $token->getUser(), 'subject' => $event->getSubject(), - 'roles' => array_map(function ($role) { - return $role->getRole(); - }, $roles), + 'roles' => $roles, // needed for the is_granted expression function 'auth_checker' => $this->authorizationChecker, // needed for the is_* expression function diff --git a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php index 64df594b23..8e7a9cb779 100644 --- a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php +++ b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -35,8 +36,7 @@ class GuardListenerTest extends TestCase ], ]; $expressionLanguage = new ExpressionLanguage(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); - $token->expects($this->any())->method('getRoles')->willReturn([new Role('ROLE_USER')]); + $token = new UsernamePasswordToken('username', 'credentials', 'provider', ['ROLE_USER']); $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); $tokenStorage->expects($this->any())->method('getToken')->willReturn($token); $this->authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();