10000 Changing behavior so that roles are only refreshed if the token requests · symfony/symfony@2675664 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2675664

Browse files
committed
Changing behavior so that roles are only refreshed if the token requests
it By default, AbstractToken (and its sub-classes), have a mechanism to check if any extra tokens were added, beyond the user tokens. If there are none, then the roles are refreshed. If there are some, then they are not.
1 parent bd72d8f commit 2675664

File tree

13 files changed

+227
-9
lines changed

13 files changed

+227
-9
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
13+
14+
use Symfony\Component\HttpFoundation\RequestStack;
15+
use Symfony\Component\Security\Core\User\UserInterface;
16+
use Symfony\Component\Security\Core\User\UserProviderInterface;
17+
18+
class RefreshableRolesTest extends WebTestCase
19+
{
20+
public function testRolesAreRefreshed()
21+
{
22+
// log them in!
23+
$client = $this->createAuthenticatedClient('cool_user');
24+
25+
// refresh the page, roles have not changed yet
26+
$client->request('GET', '/profile');
27+
$rolesData = $client->getProfile()->getCollector('security')->getRoles();
28+
$this->assertCount(1, $rolesData);
29+
$this->assertEquals('ROLE_ORIGINAL', $rolesData[0]);
30+
31+
// this will cause the refreshed user to have these new roles
32+
$client->request('GET', '/profile?new_role=ROLE_NEW');
33+
$rolesData = $client->getProfile()->getCollector('security')->getRoles();
34+
$this->assertCount(1, $rolesData);
35+
$this->assertEquals('ROLE_NEW', $rolesData[0]);
36+
37+
// the change should be persistent
38+
$client->request('GET', '/profile');
39+
$rolesData = $client->getProfile()->getCollector('security')->getRoles();
40+
$this->assertCount(1, $rolesData);
41+
$this->assertEquals('ROLE_NEW', $rolesData[0]);
42+
}
43+
44+
private function createAuthenticatedClient($username)
45+
{
46+
$client = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'refreshable_roles.yml'));
47+
$client->followRedirects(true);
48+
49+
$form = $client->request('GET', '/login')->selectButton('login')->form();
50+
$form['_username'] = $username;
51+
$form['_password'] = 'test';
52+
$client->submit($form);
53+
54+
return $client;
55+
}
56+
}
57+
58+
class RefreshableRolesUserProvider implements UserProviderInterface
59+
{
60+
private $requestStack;
61+
62+
public function __construct(RequestStack $requestStack)
63+
{
64+
$this->requestStack = $requestStack;
65+
}
66+
67+
public function loadUserByUsername($username)
68+
{
69+
return new RefreshableUser($username, array('ROLE_ORIGINAL'));
70+
}
71+
72+
public function refreshUser(UserInterface $user)
73+
{
74+
$request = $this->requestStack->getCurrentRequest();
75+
// a sneaky way of faking the stored user's roles being changed
76+
if ($request->query->has('new_role')) {
77+
$user->setRoles(array($request->query->get('new_role')));
78+
}
79+
80+
return $user;
81+
}
82+
83+
public function supportsClass($class)
84+
{
85+
return $class === RefreshableUser::class;
86+
}
87+
}
88+
89+
class RefreshableUser implements UserInterface
90+
{
91+
private $username;
92+
private $roles;
93+
94+
public function __construct($username, array $roles)
95+
{
96+
$this->username = $username;
97+
$this->roles = $roles;
98+
}
99+
100+
public function getUsername()
101+
{
102+
return $this->username;
103+
}
104+
105+
public function getRoles()
106+
{
107+
return $this->roles;
108+
}
109+
110+
public function setRoles($roles)
111+
{
112+
$this->roles = $roles;
113+
}
114+
115+
public function getPassword()
116+
{
117+
return &# 10000 39;test';
118+
}
119+
public function getSalt()
120+
{
121+
}
122+
public function eraseCredentials()
123+
{
124+
}
125+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
imports:
2+
- { resource: ./../config/default.yml }
3+
4+
services:
5+
refreshable_roles_user_provider:
6+
class: Symfony\Bundle\SecurityBundle\Tests\Functional\RefreshableRolesUserProvider
7+
arguments: ['@request_stack']
8+
9+
security:
10+
encoders:
11+
Symfony\Bundle\SecurityBundle\Tests\Functional\RefreshableUser: plaintext
12+
13+
providers:
14+
all_users:
15+
id: refreshable_roles_user_provider
16+
17+
firewalls:
18+
# This firewall doesn't make sense in combination with the rest of the
19+
# configuration file, but it's here for testing purposes (do not use
20+
# this file in a real world scenario though)
21+
login_form:
22+
pattern: ^/login$
23+
security: false
24+
25+
default:
26+
form_login:
27+
check_path: /login_check
28+
default_target_path: /profile
29+
anonymous: ~

src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ abstract class AbstractToken implements TokenInterface, RefreshableRolesTokenInt
2929
private $roles = array();
3030
private $authenticated = false;
3131
private $attributes = array();
32+
private $shouldUpdateRoles = false;
3233

3334
/**
3435
* Constructor.
@@ -235,6 +236,14 @@ public function updateRoles(array $roles)
235236
}
236237
}
237238

239+
/**
240+
* {@inheritdoc}
241+
*/
242+
public function shouldUpdateRoles()
243+
{
244+
return $this->shouldUpdateRoles;
245+
}
246+
238247
/**
239248
* {@inheritdoc}
240249
*/
@@ -251,6 +260,18 @@ public function __toString()
251260
return sprintf('%s(user="%s", authenticated=%s, roles="%s")', $class, $this->getUsername(), json_encode($this->authenticated), implode(', ', $roles));
252261
}
253262

263+
/**
264+
* Call this from a sub-class if you want the token's roles
265+
* to be updated from UserInterface::getRoles() on each
266+
* page refresh (when using session-based authentication)
267+
*
268+
* @param bool $shouldUpdateRoles
269+
*/
270+
protected function setShouldUpdateRoles($shouldUpdateRoles)
271+
{
272+
$this->shouldUpdateRoles = $shouldUpdateRoles;
273+
}
274+
254275
private function hasUserChanged(UserInterface $user)
255276
{
256277
if (!($this->user instanceof UserInterface)) {

src/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Security\Core\Authentication\Token;
1313

1414
use Symfony\Component\Security\Core\Role\Role;
15+
use Symfony\Component\Security\Core\User\UserInterface;
1516

1617
/**
1718
* AnonymousToken represents an anonymous token.
@@ -36,6 +37,8 @@ public function __construct($secret, $user, array $roles = array())
3637
$this->secret = $secret;
3738
$this->setUser($user);
3839
$this->setAuthenticated(true);
40+
41+
$this->setShouldUpdateRoles($user instanceof UserInterface && $user->getRoles() === $roles);
3942
}
4043

4144
/**

src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Security\Core\Authentication\Token;
1313

14+
use Symfony\Component\Security\Core\User\UserInterface;
15+
1416
/**
1517
* PreAuthenticatedToken implements a pre-authenticated token.
1618
*
@@ -44,6 +46,8 @@ public function __construct($user, $credentials, $providerKey, array $roles = ar
4446
if ($roles) {
4547
$this->setAuthenticated(true);
4648
}
49+
50+
$this->setShouldUpdateRoles($user instanceof UserInterface && $user->getRoles() === $roles);
4751
}
4852

4953
/**

src/Symfony/Component/Security/Core/Authentication/Token/RefreshableRolesTokenInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,15 @@ interface RefreshableRolesTokenInterface
2222
* @param array $roles An array of roles
2323
*/
2424
public function updateRoles(array $roles);
25+
26+
/**
27+
* Returns whether or not roles *should* be updated on this token.
28+
*
29+
* This can be useful if your token is adding custom roles,
30+
* and so you purposely do not want the roles in the token to
31+
* be automatically reset.
32+
*
33+
* @return bool
34+
*/
35+
public function shouldUpdateRoles();
2536
}

src/Symfony/Component/Security/Core/Authentication/Token/RememberMeToken.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function __construct(UserInterface $user, $providerKey, $secret)
4949

5050
$this->setUser($user);
5151
parent::setAuthenticated(true);
52+
$this->setShouldUpdateRoles(true);
5253
}
5354

5455
/**

src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Security\Core\Authentication\Token;
1313

14+
use Symfony\Component\Security\Core\User\UserInterface;
15+
1416
/**
1517
* UsernamePasswordToken implements a username and password token.
1618
*
@@ -44,6 +46,8 @@ public function __construct($user, $credentials, $providerKey, array $roles = ar
4446
$this->providerKey = $providerKey;
4547

4648
parent::setAuthenticated(count($roles) > 0);
49+
50+
$this->setShouldUpdateRoles($user instanceof UserInterface && $user->getRoles() === $roles);
4751
}
4852

4953
/**

src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function testAuthenticate()
5656
{
5757
$user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
5858
$user
59-
->expects($this->once())
59+
->expects($this->atLeastOnce())
6060
->method('getRoles')
6161
->will($this->returnValue(array()))
6262
;

src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public function testAuthenticateWhenPostCheckAuthenticationFailsWithHideFalse()
159159
public function testAuthenticate()
160160
{
161161
$user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
162-
$user->expects($this->once())
162+
$user->expects($this->atLeastOnce())
163163
->method('getRoles')
164164
->will($this->returnValue(array('ROLE_FOO')))
165165
;
@@ -193,7 +193,7 @@ public function testAuthenticate()
193193
public function testAuthenticateWithPreservingRoleSwitchUserRole()
194194
{
195195
$user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock();
196-
$user->expects($this->once())
196+
$user->expects($this->atLeastOnce())
197197
->method('getRoles')
198198
->will($this->returnValue(array('ROLE_FOO')))
199199
;

src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public function __construct(UserInterface $user, $providerKey, array $roles)
4848
// this token is meant to be used after authentication success, so it is always authenticated
4949
// you could set it as non authenticated later if you need to
5050
parent::setAuthenticated(true);
51+
52+
$this->setShouldUpdateRoles($user instanceof UserInterface && $user->getRoles() === $roles);
5153
}
5254

5355
/**

src/Symfony/Component/Security/Http/Firewall/ContextListener.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ class ContextListener implements ListenerInterface
4040
private $tokenStorage;
4141
private $contextKey;
4242
private $sessionKey;
43+
private $refreshableRolesSessionKey;
4344
private $logger;
4445
private $userProviders;
4546
private $dispatcher;
4647
private $registered;
4748
private $trustResolver;
4849
private $logoutOnUserChange = false;
50+
private $refreshedToken;
4951

5052
/**
5153
* @param TokenStorageInterface $tokenStorage
@@ -65,6 +67,8 @@ public function __construct(TokenStorageInterface $tokenStorage, $userProviders,
6567
$this->userProviders = $userProviders;
6668
$this->contextKey = $contextKey;
6769
$this->sessionKey = '_security_'.$contextKey;
70+
$this->refreshableRolesSessionKey = $this->sessionKey.'_refreshable_roles';
71+
6872
$this->logger = $logger;
6973
$this->dispatcher = $dispatcher;
7074
$this->trustResolver = $trustResolver ?: new AuthenticationTrustResolver(AnonymousToken::class, RememberMeToken::class);
@@ -120,14 +124,16 @@ public function handle(GetResponseEvent $event)
120124
$token = null;
121125
}
122126

123-
if ($token instanceof RefreshableRolesTokenInterface && $token->getUser() instanceof UserInterface) {
127+
// see if this token wants the roles refreshed from the User
128+
if ($session->get($this->refreshableRolesSessionKey) && $token->getUser() instanceof UserInterface) {
124129
if (null !== $this->logger) {
125130
$this->logger->debug('Refreshing token roles from the User object');
126131
}
127132

128133
$token->updateRoles($token->getUser()->getRoles());
129134
}
130135

136+
$this->refreshedToken = $token;
131137
$this->tokenStorage->setToken($token);
132138
}
133139

@@ -155,13 +161,20 @@ public function onKernelResponse(FilterResponseEvent $event)
155161
if ((null === $token = $this->tokenStorage->getToken()) || $this->trustResolver->isAnonymous($token)) {
156162
if ($request->hasPreviousSession()) {
157163
$session->remove($this->sessionKey);
164+
$session->remove($this->refreshableRolesSessionKey);
158165
}
159166
} else {
160167
$session->set($this->sessionKey, serialize($token));
161168

162169
if (null !== $this->logger) {
163170
$this->logger->debug('Stored the security token in the session.', array('key' => $this->sessionKey));
164171
}
172+
173+
// if the token changed, then re-set the refreshable key
174+
if ($token !== $this->refreshedToken) {
175+
$shouldUpdateRoles = $token instanceof RefreshableRolesTokenInterface && $token->shouldUpdateRoles();
176+
$session->set($this->refreshableRolesSessionKey, $shouldUpdateRoles);
177+
}
165178
}
166179
}
167180

0 commit comments

Comments
 (0)
0