8000 feature #23026 [SecurityBundle] Add user impersonation info and exit … · symfony/symfony@2c438c5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2c438c5

Browse files
committed
feature #23026 [SecurityBundle] Add user impersonation info and exit action to the profiler (yceruto)
This PR was squashed before being merged into the 3.4 branch (closes #23026). Discussion ---------- [SecurityBundle] Add user impersonation info and exit action to the profiler | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #23094 | License | MIT Toolbar item result: ![toolbar](https://cloud.githubusercontent.com/assets/2028198/26724555/1b60320a-4768-11e7-8433-da935f7068e9.png) I'm no sure if more information should be displayed from source token, wdyt? Security profile panel result: ![security_token_profile_panel](https://cloud.githubusercontent.com/assets/2028198/26705860/f7a64054-4706-11e7-9eef-6cd6b7365738.png) Commits ------- a3253f6 [SecurityBundle] Add user impersonation info and exit action to the profiler
2 parents 16fbe3a + a3253f6 commit 2c438c5

File tree

11 files changed

+162
-40
lines changed

11 files changed

+162
-40
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
2121
use Symfony\Component\Security\Core\Role\RoleInterface;
2222
use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener;
23+
use Symfony\Component\Security\Core\Role\SwitchUserRole;
24+
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
2325
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
2426
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
2527
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
@@ -73,6 +75,9 @@ public function collect(Request $request, Response $response, \Exception $except
7375
$this->data = array(
7476
'enabled' => false,
7577
'authenticated' => false,
78+
'impersonated' => false,
79+
'impersonator_user' => null,
80+
'impersonation_exit_path' => null,
7681
'token' => null,
7782
'token_class' => null,
7883
'logout_url' => null,
@@ -85,6 +90,9 @@ public function collect(Request $request, Response $response, \Exception $except
8590
$this->data = array(
8691
'enabled' => true,
8792
'authenticated' => false,
93+
'impersonated' => false,
94+
'impersonator_user' => null,
95+
'impersonation_exit_path' => null,
8896
'token' => null,
8997
'token_class' => null,
9098
'logout_url' => null,
@@ -97,6 +105,14 @@ public function collect(Request $request, Response $response, \Exception $except
97105
$inheritedRoles = array();
98106
$assignedRoles = $token->getRoles();
99107

108+
$impersonatorUser = null;
109+
foreach ($assignedRoles as $role) {
110+
if ($role instanceof SwitchUserRole) {
111+
$impersonatorUser = $role->getSource()->getUsername();
112+
break;
113+
}
114+
}
115+
100116
if (null !== $this->roleHierarchy) {
101117
$allRoles = $this->roleHierarchy->getReachableRoles($assignedRoles);
102118
foreach ($allRoles as $role) {
@@ -126,6 +142,9 @@ public function collect(Request $request, Response $response, \Exception $except
126142
$this->data = array(
127143
'enabled' => true,
128144
'authenticated' => $token->isAuthenticated(),
145+
'impersonated' => null !== $impersonatorUser,
146+
'impersonator_user' => $impersonatorUser,
147+
'impersonation_exit_path' => null,
129148
'token' => $token,
130149
'token_class' => $this->hasVarDumper ? new ClassStub(get_class($token)) : get_class($token),
131150
'logout_url' => $logoutUrl,
@@ -169,6 +188,15 @@ public function collect(Request $request, Response $response, \Exception $except
169188
'user_checker' => $firewallConfig->getUserChecker(),
170189
'listeners' => $firewallConfig->getListeners(),
171190
);
191+
192+
// generate exit impersonation path from current request
193+
if ($this->data['impersonated'] && null !== $switchUserConfig = $firewallConfig->getSwitchUser()) {
194+
$exitPath = $request->getRequestUri();
195+
$exitPath .= null === $request->getQueryString() ? '?' : '&';
196+
$exitPath .= sprintf('%s=%s', urlencode($switchUserConfig['parameter']), SwitchUserListener::EXIT_VALUE);
197+
198+
$this->data['impersonation_exit_path'] = $exitPath;
199+
}
172200
}
173201
}
174202

@@ -245,6 +273,21 @@ public function isAuthenticated()
245273
return $this->data['authenticated'];
246274
}
247275

276+
public function isImpersonated()
277+
{
278+
return $this->data['impersonated'];
279+
}
280+
281+
public function getImpersonatorUser()
282+
{
283+
return $this->data['impersonator_user'];
284+
}
285+
286+
public function getImpersonationExitPath()
287+
{
288+
return $this->data['impersonation_exit_path'];
289+
}
290+
248291
/**
249292
* Get the class name of the security token.
250293
*

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
448448
}
449449

450450
$config->replaceArgument(10, $listenerKeys);
451+
$config->replaceArgument(11, isset($firewall['switch_user']) ? $firewall['switch_user'] : null);
451452

452453
return array($matcher, $listeners, $exceptionListener);
453454
}

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 %}
51+
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 %}
4858

49-
{% if collector.token and collector.logoutUrl %}
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 parameters if configured, null otherwise
149+
*/
150+
public function getSwitchUser()
151+
{
152+
return $this->switchUser;
153+
}
143154
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Component\Security\Core\Role\Role;
2525
use Symfony\Component\Security\Core\Role\RoleHierarchy;
2626
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
27+
use Symfony\Component\Security\Core\Role\SwitchUserRole;
2728
use Symfony\Component\Security\Http\FirewallMapInterface;
2829
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
2930

@@ -37,6 +38,9 @@ public function testCollectWhenSecurityIsDisabled()
3738
$this->assertSame('security', $collector->getName());
3839
$this->assertFalse($collector->isEnabled());
3940
$this->assertFalse($collector->isAuthenticated());
41+
$this->assertFalse($collector->isImpersonated());
42+
$this->assertNull($collector->getImpersonatorUser());
43+
$this->assertNull($collector->getImpersonationExitPath());
4044
$this->assertNull($collector->getTokenClass());
4145
$this->assertFalse($collector->supportsRoleHierarchy());
4246
$this->assertCount(0, $collector->getRoles());
@@ -53,6 +57,9 @@ public function testCollectWhenAuthenticationTokenIsNull()
5357

5458
$this->assertTrue($collector->isEnabled());
5559
$this->assertFalse($collector->isAuthenticated());
60+
$this->assertFalse($collector->isImpersonated());
61+
$this->assertNull($collector->getImpersonatorUser());
62+
$this->assertNull($collector->getImpersonationExitPath());
5663
$this->assertNull($collector->getTokenClass());
5764
$this->assertTrue($collector->supportsRoleHierarchy());
5865
$this->assertCount(0, $collector->getRoles());
@@ -73,13 +80,43 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm
7380

7481
$this->assertTrue($collector->isEnabled());
7582
$this->assertTrue($collector->isAuthenticated());
83+
$this->assertFalse($collector->isImpersonated());
84+
$this->assertNull($collector->getImpersonatorUser());
85+
$this->assertNull($collector->getImpersonationExitPath());
7686
$this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue());
7787
$this->assertTrue($collector->supportsRoleHierarchy());
7888
$this->assertSame($normalizedRoles, $collector->getRoles()->getValue(true));
7989
$this->assertSame($inheritedRoles, $collector->getInheritedRoles()->getValue(true));
8090
$this->assertSame('hhamon', $collector->getUser());
8191
}
8292

93+
public function testCollectImpersonatedToken()
94+
{
95+
$adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', array('ROLE_ADMIN'));
96+
97+
$userRoles = array(
98+
'ROLE_USER',
99+
new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $adminToken),
100+
);
101+
102+
$tokenStorage = new TokenStorage();
103+
$tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles));
104+
105+
$collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy());
106+
$collector->collect($this->getRequest(), $this->getResponse());
107+
$collector->lateCollect();
108+
109+
$this->assertTrue($collector->isEnabled());
110+
$this->assertTrue($collector->isAuthenticated());
111+
$this->assertTrue($collector->isImpersonated());
112+
$this->assertSame('yceruto', $collector->getImpersonatorUser());
113+
$this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue());
114+
$this->assertTrue($collector->supportsRoleHierarchy());
115+
$this->assertSame(array('ROLE_USER', 'ROLE_PREVIOUS_ADMIN'), $collector->getRoles()->getValue(true));
116+
$this->assertSame(array(), $collector->getInheritedRoles()->getValue(true));
117+
$this->assertSame('hhamon', $collector->getUser());
118+
}
119+
83120
public function testGetFirewall()
84121
{
85122
$firewallConfig = new FirewallConfig('dummy', 'security.request_matcher.dummy', 'security.user_checker.dummy');

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ public function testFirewalls()
107107
'remember_me',
108108
'anonymous',
109109
),
110+
array(
111+
'parameter' => '_switch_user',
112+
'role' => 'ROLE_ALLOWED_TO_SWITCH',
113+
),
110114
),
111115
array(
112116
'host',
@@ -123,6 +127,7 @@ public function testFirewalls()
123127
'http_basic',
124128
'anonymous',
125129
),
130+
null,
126131
),
127132
array(
128133
'with_user_checker',
@@ -139,6 +144,7 @@ public function testFirewalls()
139144
'http_basic',
140145
'anonymous',
141146
),
147+
null,
142148
),
143149
), $configs);
144150

src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
1313

14+
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
15+
1416
class SwitchUserTest extends WebTestCase
1517
{
1618
/**
@@ -42,7 +44,7 @@ public function testSwitchedUserExit()
4244
$client = $this->createAuthenticatedClient('user_can_switch');
4345

4446
$client->request('GET', '/profile?_switch_user=user_cannot_switch_1');
45-
$client->request('GET', '/profile?_switch_user=_exit');
47+
$client->request('GET', '/profile?_switch_user='.SwitchUserListener::EXIT_VALUE);
4648

4749
$this->assertEquals(200, $client->getResponse()->getStatusCode());
4850
$this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser());

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

+4Lines 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)
0