Skip to content

Commit a497d2a

Browse files
committed
Added user impersonation info and exit action
1 parent e8a4771 commit a497d2a

File tree

8 files changed

+159
-34
lines changed

8 files changed

+159
-34
lines changed

src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\HttpFoundation\Response;
1919
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
2020
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
21+
use Symfony\Component\Security\Core\Role\SwitchUserRole;
2122
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
2223
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
2324
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
@@ -68,6 +69,9 @@ public function collect(Request $request, Response $response, \Exception $except
6869
$this->data = array(
6970
'enabled' => false,
7071
'authenticated' => false,
72+
'impersonated' => false,
73+
'impersonator_user' => null,
74+
'impersonation_exit_path' => null,
7175
'token' => null,
7276
'token_class' => null,
7377
'logout_url' => null,
@@ -80,6 +84,9 @@ public function collect(Request $request, Response $response, \Exception $except
8084
$this->data = array(
8185
'enabled' => true,
8286
'authenticated' => false,
87+
'impersonated' => false,
88+
'impersonator_user' => null,
89+
'impersonated_exit_path' => null,
8390
'token' => null,
8491
'token_class' => null,
8592
'logout_url' => null,
@@ -92,6 +99,14 @@ public function collect(Request $request, Response $response, \Exception $except
9299
$inheritedRoles = array();
93100
$assignedRoles = $token->getRoles();
94101

102+
$impersonatorUser = null;
103+
foreach ($assignedRoles as $role) {
104+
if ($role instanceof SwitchUserRole) {
105+
$impersonatorUser = $role->getSource()->getUsername();
106+
break;
107+
}
108+
}
109+
95110
if (null !== $this->roleHierarchy) {
96111
$allRoles = $this->roleHierarchy->getReachableRoles($assignedRoles);
97112
foreach ($allRoles as $role) {
@@ -113,6 +128,9 @@ public function collect(Request $request, Response $response, \Exception $except
113128
$this->data = array(
114129
'enabled' => true,
115130
'authenticated' => $token->isAuthenticated(),
131+
'impersonated' => null !== $impersonatorUser,
132+
'impersonator_user' => $impersonatorUser,
133+
'impersonation_exit_path' => null,
116134
'token' => $token,
117135
'token_class' => $this->hasVarDumper ? new ClassStub(get_class($token)) : get_class($token),
118136
'logout_url' => $logoutUrl,
@@ -156,6 +174,15 @@ public function collect(Request $request, Response $response, \Exception $except
156174
'user_checker' => $firewallConfig->getUserChecker(),
157175
'listeners' => $firewallConfig->getListeners(),
158176
);
177+
178+
// generate exit impersonation path from current request
179+
if ($this->data['impersonated'] && null !== $switchUserConfig = $firewallConfig->getSwitchUser()) {
180+
$exitPath = $request->getRequestUri();
181+
$exitPath .= null === $request->getQueryString() ? '?' : '&';
182+
$exitPath .= urlencode($switchUserConfig['parameter']).'=_exit';
183+
184+
$this->data['impersonation_exit_path'] = $exitPath;
185+
}
159186
}
160187
}
161188
}
@@ -226,6 +253,36 @@ public function isAuthenticated()
226253
return $this->data['authenticated'];
227254
}
228255

256+
/**
257+
* Checks if the user is impersonated or not.
258+
*
259+
* @return bool true if the user is impersonated, false otherwise
260+
*/
261+
public function isImpersonated()
262+
{
263+
return $this->data['impersonated'];
264+
}
265+
266+
/**
267+
* Returns the impersonation source user.
268+
*
269+
* @return string The username
270+
*/
271+
public function getImpersonatorUser()
272+
{
273+
return $this->data['impersonator_user'];
274+
}
275+
276+
/**
277+
* Returns the exit impersonation path.
278+
*
279+
* @return string The URI path
280+
*/
281+
public function getImpersonationExitPath()
282+
{
283+
return $this->data['impersonation_exit_path'];
284+
}
285+
229286
/**
230287
* Get the class name of the security token.
231288
*

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,8 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
399399
if (isset($firewall['switch_user'])) {
400400
$listenerKeys[] = 'switch_user';
401401
$listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider));
402+
403+
$config->replaceArgument(11, $firewall['switch_user']);
402404
}
403405

404406
// Access listener

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
<argument /> <!-- access_denied_handler -->
137137
<argument /> <!-- access_denied_url -->
138138
<argument type="collection" /> <!-- listeners -->
139+
<argument /> <!-- switch_user -->
139140
</service>
140141

141142
<service id="security.logout_url_generator" class="Symfony\Component\Security\Http\Logout\LogoutUrlGenerator">

src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,63 @@
1616
{% endset %}
1717

1818
{% set text %}
19-
{% if collector.enabled %}
20-
{% if collector.token %}
19+
{% if collector.impersonated %}
20+
<div class="sf-toolbar-info-group">
2121
<div class="sf-toolbar-info-piece">
22-
<b>Logged in as</b>
23-
<span>{{ collector.user }}</span>
22+
<b>Impersonator</b>
23+
<span>{{ collector.impersonatorUser }}</span>
2424
</div>
25+
</div>
26+
{% endif %}
2527

26-
<div class="sf-toolbar-info-piece">
27-
<b>Authenticated</b>
28-
<span class="sf-toolbar-status sf-toolbar-status-{{ is_authenticated ? 'green' : 'red' }}">{{ is_authenticated ? 'Yes' : 'No' }}</span>
29-
</div>
28+
<div class="sf-toolbar-info-group">
29+
{% if collector.enabled %}
30+
{% if collector.token %}
31+
<div class="sf-toolbar-info-piece">
32+
<b>Logged in as</b>
33+
<span>{{ collector.user }}</span>
34+
</div>
3035

31-
<div class="sf-toolbar-info-piece">
32-
<b>Token class</b>
33-
<span>{{ collector.tokenClass|abbr_class }}</span>
34-
</div>
35-
{% else %}
36-
<div class="sf-toolbar-info-piece">
37-
<b>Authenticated</b>
38-
<span class="sf-toolbar-status sf-toolbar-status-red">No</span>
39-
</div>
40-
{% endif %}
36+
<div class="sf-toolbar-info-piece">
37+
<b>Authenticated</b>
38+
<span class="sf-toolbar-status sf-toolbar-status-{{ is_authenticated ? 'green' : 'red' }}">{{ is_authenticated ? 'Yes' : 'No' }}</span>
39+
</div>
4140

42-
{% if collector.firewall %}
43-
<div class="sf-toolbar-info-piece">
44-
<b>Firewall name</b>
45-
<span>{{ collector.firewall.name }}</span>
46-
</div>
47-
{% endif %}
41+
<div class="sf-toolbar-info-piece">
42+
<b>Token class</b>
43+
<span>{{ collector.tokenClass|abbr_class }}</span>
44+
</div>
45+
{% else %}
46+
<div class="sf-toolbar-info-piece">
47+
<b>Authenticated</b>
48+
<span class="sf-toolbar-status sf-toolbar-status-red">No</span>
49+
</div>
50+
{% endif %}
4851

49-
{% if collector.token and collector.logoutUrl %}
52+
{% if collector.firewall %}
53+
<div class="sf-toolbar-info-piece">
54+
<b>Firewall name</b>
55+
<span>{{ collector.firewall.name }}</span>
56+
</div>
57+
{% endif %}
58+
59+
{% if collector.token and collector.logoutUrl %}
60+
<div class="sf-toolbar-info-piece">
61+
<b>Actions</b>
62+
<span>
63+
<a href="{{ collector.logoutUrl }}">Logout</a>
64+
{% if collector.impersonated and collector.impersonationExitPath %}
65+
| <a href="{{ collector.impersonationExitPath }}">Exit impersonation</a>
66+
{% endif %}
67+
</span>
68+
</div>
69+
{% endif %}
70+
{% else %}
5071
<div class="sf-toolbar-info-piece">
51-
<b>Actions</b>
52-
<span><a href="{{ collector.logoutUrl }}">Logout</a></span>
72+
<span>The security is disabled.</span>
5373
</div>
5474
{% endif %}
55-
{% else %}
56-
<div class="sf-toolbar-info-piece">
57-
<span>The security is disabled.</span>
58-
</div>
59-
{% endif %}
75+
</div>
6076
{% endset %}
6177

6278
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: color_code }) }}

src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ final class FirewallConfig
2727
private $accessDeniedHandler;
2828
private $accessDeniedUrl;
2929
private $listeners;
30+
private $switchUser;
3031

3132
/**
3233
* @param string $name
@@ -40,8 +41,9 @@ final class FirewallConfig
4041
* @param string|null $accessDeniedHandler
4142
* @param string|null $accessDeniedUrl
4243
* @param string[] $listeners
44+
* @param array|null $switchUser
4345
*/
44-
public function __construct($name, $userChecker, $requestMatcher = null, $securityEnabled = true, $stateless = false, $provider = null, $context = null, $entryPoint = null, $accessDeniedHandler = null, $accessDeniedUrl = null, $listeners = array())
46+
public function __construct($name, $userChecker, $requestMatcher = null, $securityEnabled = true, $stateless = false, $provider = null, $context = null, $entryPoint = null, $accessDeniedHandler = null, $accessDeniedUrl = null, $listeners = array(), $switchUser = null)
4547
{
4648
$this->name = $name;
4749
$this->userChecker = $userChecker;
@@ -54,6 +56,7 @@ public function __construct($name, $userChecker, $requestMatcher = null, $securi
5456
$this->accessDeniedHandler = $accessDeniedHandler;
5557
$this->accessDeniedUrl = $accessDeniedUrl;
5658
$this->listeners = $listeners;
59+
$this->switchUser = $switchUser;
5760
}
5861

5962
public function getName()
@@ -140,4 +143,12 @@ public function getListeners()
140143
{
141144
return $this->listeners;
142145
}
146+
147+
/**
148+
* @return array|null The switch_user config if enabled, null otherwise
149+
*/
150+
public function getSwitchUser()
151+
{
152+
return $this->switchUser;
153+
}
143154
}

src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
2020
use Symfony\Component\Security\Core\Role\Role;
2121
use Symfony\Component\Security\Core\Role\RoleHierarchy;
22+
use Symfony\Component\Security\Core\Role\SwitchUserRole;
2223
use Symfony\Component\Security\Http\FirewallMapInterface;
2324

2425
class SecurityDataCollectorTest extends TestCase
@@ -31,6 +32,7 @@ public function testCollectWhenSecurityIsDisabled()
3132
$this->assertSame('security', $collector->getName());
3233
$this->assertFalse($collector->isEnabled());
3334
$this->assertFalse($collector->isAuthenticated());
35+
$this->assertFalse($collector->isImpersonated());
3436
$this->assertNull($collector->getTokenClass());
3537
$this->assertFalse($collector->supportsRoleHierarchy());
3638
$this->assertCount(0, $collector->getRoles());
@@ -47,6 +49,7 @@ public function testCollectWhenAuthenticationTokenIsNull()
4749

4850
$this->assertTrue($collector->isEnabled());
4951
$this->assertFalse($collector->isAuthenticated());
52+
$this->assertFalse($collector->isImpersonated());
5053
$this->assertNull($collector->getTokenClass());
5154
$this->assertTrue($collector->supportsRoleHierarchy());
5255
$this->assertCount(0, $collector->getRoles());
@@ -67,13 +70,41 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm
6770

6871
$this->assertTrue($collector->isEnabled());
6972
$this->assertTrue($collector->isAuthenticated());
73+
$this->assertFalse($collector->isImpersonated());
7074
$this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue());
7175
$this->assertTrue($collector->supportsRoleHierarchy());
7276
$this->assertSame($normalizedRoles, $collector->getRoles()->getValue(true));
7377
$this->assertSame($inheritedRoles, $collector->getInheritedRoles()->getValue(true));
7478
$this->assertSame('hhamon', $collector->getUser());
7579
}
7680

81+
public function testCollectImpersonatedToken()
82+
{
83+
$adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', array('ROLE_ADMIN'));
84+
85+
$userRoles = array(
86+
'ROLE_USER',
87+
new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $adminToken),
88+
);
89+
90+
$tokenStorage = new TokenStorage();
91+
$tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles));
92+
93+
$collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy());
94+
$collector->collect($this->getRequest(), $this->getResponse());
95+
$collector->lateCollect();
96+
97+
$this->assertTrue($collector->isEnabled());
98+
$this->assertTrue($collector->isAuthenticated());
99+
$this->assertTrue($collector->isImpersonated());
100+
$this->assertSame('yceruto', $collector->getImpersonatorUser());
101+
$this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue());
102+
$this->assertTrue($collector->supportsRoleHierarchy());
103+
$this->assertSame(array('ROLE_USER', 'ROLE_PREVIOUS_ADMIN'), $collector->getRoles()->getValue(true));
104+
$this->assertSame(array(), $collector->getInheritedRoles()->getValue(true));
105+
$this->assertSame('hhamon', $collector->getUser());
106+
}
107+
77108
public function testGetFirewall()
78109
{
79110
$firewallConfig = new FirewallConfig('dummy', 'security.request_matcher.dummy', 'security.user_checker.dummy');

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ public function testFirewalls()
9494
'security.user.provider.concrete.default',
9595
null,
9696
'security.authentication.form_entry_point.secure',
97+
array(
98+
'parameter' => '_switch_user',
99+
'role' => 'ROLE_ALLOWED_TO_SWITCH',
100+
),
97101
null,
98102
null,
99103
array(

src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function testGetters()
2929
'access_denied_url' => 'foo_access_denied_url',
3030
'access_denied_handler' => 'foo_access_denied_handler',
3131
'user_checker' => 'foo_user_checker',
32+
'switch_user' => array('provider' => null, 'parameter' => '_switch_user', 'role' => 'ROLE_ALLOWED_TO_SWITCH'),
3233
);
3334

3435
$config = new FirewallConfig(
@@ -42,7 +43,8 @@ public function testGetters()
4243
$options['entry_point'],
4344
$options['access_denied_handler'],
4445
$options['access_denied_url'],
45-
$listeners
46+
$listeners,
47+
$options['switch_user']
4648
);
4749

4850
$this->assertSame('foo_firewall', $config->getName());
@@ -57,5 +59,6 @@ public function testGetters()
5759
$this->assertSame($options['user_checker'], $config->getUserChecker());
5860
$this->assertTrue($config->allowsAnonymous());
5961
$this->assertSame($listeners, $config->getListeners());
62+
$this->assertSame($options['switch_user'], $config->getSwitchUser());
6063
}
6164
}

0 commit comments

Comments
 (0)