Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 87d2826

Browse filesBrowse 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 87d2826
Copy full SHA for 87d2826

File tree

13 files changed

+229
-9
lines changed
Filter options

13 files changed

+229
-9
lines changed
+127Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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 RefreshableUser::class === $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 'test';
118+
}
119+
120+
public function getSalt()
121+
{
122+
}
123+
124+
public function eraseCredentials()
125+
{
126+
}
127+
}
+29Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php
+21Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/AnonymousToken.php
+3Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php
+4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/RefreshableRolesTokenInterface.php
+11Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/RememberMeToken.php
+1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php
+4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Tests/Authentication/Provider/PreAuthenticatedAuthenticationProviderTest.php
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Tests/Authentication/Provider/UserAuthenticationProviderTest.php
+2-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Guard/Token/PostAuthenticationGuardToken.php
+2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Firewall/ContextListener.php
+14-1Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.