diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index d25a5be606ec9..7549c718f6204 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -20,6 +20,8 @@ use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Security\Core\Role\RoleInterface; use Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener; +use Symfony\Component\Security\Core\Role\SwitchUserRole; +use Symfony\Component\Security\Http\Firewall\SwitchUserListener; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; @@ -73,6 +75,9 @@ public function collect(Request $request, Response $response, \Exception $except $this->data = array( 'enabled' => false, 'authenticated' => false, + 'impersonated' => false, + 'impersonator_user' => null, + 'impersonation_exit_path' => null, 'token' => null, 'token_class' => null, 'logout_url' => null, @@ -85,6 +90,9 @@ public function collect(Request $request, Response $response, \Exception $except $this->data = array( 'enabled' => true, 'authenticated' => false, + 'impersonated' => false, + 'impersonator_user' => null, + 'impersonation_exit_path' => null, 'token' => null, 'token_class' => null, 'logout_url' => null, @@ -97,6 +105,14 @@ public function collect(Request $request, Response $response, \Exception $except $inheritedRoles = array(); $assignedRoles = $token->getRoles(); + $impersonatorUser = null; + foreach ($assignedRoles as $role) { + if ($role instanceof SwitchUserRole) { + $impersonatorUser = $role->getSource()->getUsername(); + break; + } + } + if (null !== $this->roleHierarchy) { $allRoles = $this->roleHierarchy->getReachableRoles($assignedRoles); foreach ($allRoles as $role) { @@ -126,6 +142,9 @@ public function collect(Request $request, Response $response, \Exception $except $this->data = array( 'enabled' => true, 'authenticated' => $token->isAuthenticated(), + 'impersonated' => null !== $impersonatorUser, + 'impersonator_user' => $impersonatorUser, + 'impersonation_exit_path' => null, 'token' => $token, 'token_class' => $this->hasVarDumper ? new ClassStub(get_class($token)) : get_class($token), 'logout_url' => $logoutUrl, @@ -169,6 +188,15 @@ public function collect(Request $request, Response $response, \Exception $except 'user_checker' => $firewallConfig->getUserChecker(), 'listeners' => $firewallConfig->getListeners(), ); + + // generate exit impersonation path from current request + if ($this->data['impersonated'] && null !== $switchUserConfig = $firewallConfig->getSwitchUser()) { + $exitPath = $request->getRequestUri(); + $exitPath .= null === $request->getQueryString() ? '?' : '&'; + $exitPath .= sprintf('%s=%s', urlencode($switchUserConfig['parameter']), SwitchUserListener::EXIT_VALUE); + + $this->data['impersonation_exit_path'] = $exitPath; + } } } @@ -245,6 +273,21 @@ public function isAuthenticated() return $this->data['authenticated']; } + public function isImpersonated() + { + return $this->data['impersonated']; + } + + public function getImpersonatorUser() + { + return $this->data['impersonator_user']; + } + + public function getImpersonationExitPath() + { + return $this->data['impersonation_exit_path']; + } + /** * Get the class name of the security token. * diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 6209d763cd81a..82aa222a759f7 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -448,6 +448,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a } $config->replaceArgument(10, $listenerKeys); + $config->replaceArgument(11, isset($firewall['switch_user']) ? $firewall['switch_user'] : null); return array($matcher, $listeners, $exceptionListener); } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 79435ff5d4619..9b31ae84439d7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -136,6 +136,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 46804a4d2ed2e..eb40758489d02 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -16,47 +16,63 @@ {% endset %} {% set text %} - {% if collector.enabled %} - {% if collector.token %} + {% if collector.impersonated %} +
- Logged in as - {{ collector.user }} + Impersonator + {{ collector.impersonatorUser }}
+
+ {% endif %} -
- Authenticated - {{ is_authenticated ? 'Yes' : 'No' }} -
+
+ {% if collector.enabled %} + {% if collector.token %} +
+ Logged in as + {{ collector.user }} +
-
- Token class - {{ collector.tokenClass|abbr_class }} -
- {% else %} -
- Authenticated - No -
- {% endif %} +
+ Authenticated + {{ is_authenticated ? 'Yes' : 'No' }} +
- {% if collector.firewall %} -
- Firewall name - {{ collector.firewall.name }} -
- {% endif %} +
+ Token class + {{ collector.tokenClass|abbr_class }} +
+ {% else %} +
+ Authenticated + No +
+ {% endif %} + + {% if collector.firewall %} +
+ Firewall name + {{ collector.firewall.name }} +
+ {% endif %} - {% if collector.token and collector.logoutUrl %} + {% if collector.token and collector.logoutUrl %} +
+ Actions + + Logout + {% if collector.impersonated and collector.impersonationExitPath %} + | Exit impersonation + {% endif %} + +
+ {% endif %} + {% else %}
- Actions - Logout + The security is disabled.
{% endif %} - {% else %} -
- The security is disabled. -
- {% endif %} +
{% endset %} {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: color_code }) }} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php index 3cfb11404ccf8..62317f625c271 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallConfig.php @@ -27,6 +27,7 @@ final class FirewallConfig private $accessDeniedHandler; private $accessDeniedUrl; private $listeners; + private $switchUser; /** * @param string $name @@ -40,8 +41,9 @@ final class FirewallConfig * @param string|null $accessDeniedHandler * @param string|null $accessDeniedUrl * @param string[] $listeners + * @param array|null $switchUser */ - public function __construct($name, $userChecker, $requestMatcher = null, $securityEnabled = true, $stateless = false, $provider = null, $context = null, $entryPoint = null, $accessDeniedHandler = null, $accessDeniedUrl = null, $listeners = array()) + 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) { $this->name = $name; $this->userChecker = $userChecker; @@ -54,6 +56,7 @@ public function __construct($name, $userChecker, $requestMatcher = null, $securi $this->accessDeniedHandler = $accessDeniedHandler; $this->accessDeniedUrl = $accessDeniedUrl; $this->listeners = $listeners; + $this->switchUser = $switchUser; } public function getName() @@ -140,4 +143,12 @@ public function getListeners() { return $this->listeners; } + + /** + * @return array|null The switch_user parameters if configured, null otherwise + */ + public function getSwitchUser() + { + return $this->switchUser; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 1c75641daf697..58809ef56f8da 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\RoleHierarchy; use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Core\Role\SwitchUserRole; use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; @@ -37,6 +38,9 @@ public function testCollectWhenSecurityIsDisabled() $this->assertSame('security', $collector->getName()); $this->assertFalse($collector->isEnabled()); $this->assertFalse($collector->isAuthenticated()); + $this->assertFalse($collector->isImpersonated()); + $this->assertNull($collector->getImpersonatorUser()); + $this->assertNull($collector->getImpersonationExitPath()); $this->assertNull($collector->getTokenClass()); $this->assertFalse($collector->supportsRoleHierarchy()); $this->assertCount(0, $collector->getRoles()); @@ -53,6 +57,9 @@ public function testCollectWhenAuthenticationTokenIsNull() $this->assertTrue($collector->isEnabled()); $this->assertFalse($collector->isAuthenticated()); + $this->assertFalse($collector->isImpersonated()); + $this->assertNull($collector->getImpersonatorUser()); + $this->assertNull($collector->getImpersonationExitPath()); $this->assertNull($collector->getTokenClass()); $this->assertTrue($collector->supportsRoleHierarchy()); $this->assertCount(0, $collector->getRoles()); @@ -73,6 +80,9 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm $this->assertTrue($collector->isEnabled()); $this->assertTrue($collector->isAuthenticated()); + $this->assertFalse($collector->isImpersonated()); + $this->assertNull($collector->getImpersonatorUser()); + $this->assertNull($collector->getImpersonationExitPath()); $this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue()); $this->assertTrue($collector->supportsRoleHierarchy()); $this->assertSame($normalizedRoles, $collector->getRoles()->getValue(true)); @@ -80,6 +90,33 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm $this->assertSame('hhamon', $collector->getUser()); } + public function testCollectImpersonatedToken() + { + $adminToken = new UsernamePasswordToken('yceruto', 'P4$$w0rD', 'provider', array('ROLE_ADMIN')); + + $userRoles = array( + 'ROLE_USER', + new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $adminToken), + ); + + $tokenStorage = new TokenStorage(); + $tokenStorage->setToken(new UsernamePasswordToken('hhamon', 'P4$$w0rD', 'provider', $userRoles)); + + $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy()); + $collector->collect($this->getRequest(), $this->getResponse()); + $collector->lateCollect(); + + $this->assertTrue($collector->isEnabled()); + $this->assertTrue($collector->isAuthenticated()); + $this->assertTrue($collector->isImpersonated()); + $this->assertSame('yceruto', $collector->getImpersonatorUser()); + $this->assertSame('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $collector->getTokenClass()->getValue()); + $this->assertTrue($collector->supportsRoleHierarchy()); + $this->assertSame(array('ROLE_USER', 'ROLE_PREVIOUS_ADMIN'), $collector->getRoles()->getValue(true)); + $this->assertSame(array(), $collector->getInheritedRoles()->getValue(true)); + $this->assertSame('hhamon', $collector->getUser()); + } + public function testGetFirewall() { $firewallConfig = new FirewallConfig('dummy', 'security.request_matcher.dummy', 'security.user_checker.dummy'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 220e39b09a723..c2598b51ff927 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -107,6 +107,10 @@ public function testFirewalls() 'remember_me', 'anonymous', ), + array( + 'parameter' => '_switch_user', + 'role' => 'ROLE_ALLOWED_TO_SWITCH', + ), ), array( 'host', @@ -123,6 +127,7 @@ public function testFirewalls() 'http_basic', 'anonymous', ), + null, ), array( 'with_user_checker', @@ -139,6 +144,7 @@ public function testFirewalls() 'http_basic', 'anonymous', ), + null, ), ), $configs); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php index f12e95671be2c..697829b1cb75a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; +use Symfony\Component\Security\Http\Firewall\SwitchUserListener; + class SwitchUserTest extends WebTestCase { /** @@ -42,7 +44,7 @@ public function testSwitchedUserExit() $client = $this->createAuthenticatedClient('user_can_switch'); $client->request('GET', '/profile?_switch_user=user_cannot_switch_1'); - $client->request('GET', '/profile?_switch_user=_exit'); + $client->request('GET', '/profile?_switch_user='.SwitchUserListener::EXIT_VALUE); $this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser()); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php index a0b4a79c50cdc..4abf33e276694 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallConfigTest.php @@ -29,6 +29,7 @@ public function testGetters() 'access_denied_url' => 'foo_access_denied_url', 'access_denied_handler' => 'foo_access_denied_handler', 'user_checker' => 'foo_user_checker', + 'switch_user' => array('provider' => null, 'parameter' => '_switch_user', 'role' => 'ROLE_ALLOWED_TO_SWITCH'), ); $config = new FirewallConfig( @@ -42,7 +43,8 @@ public function testGetters() $options['entry_point'], $options['access_denied_handler'], $options['access_denied_url'], - $listeners + $listeners, + $options['switch_user'] ); $this->assertSame('foo_firewall', $config->getName()); @@ -57,5 +59,6 @@ public function testGetters() $this->assertSame($options['user_checker'], $config->getUserChecker()); $this->assertTrue($config->allowsAnonymous()); $this->assertSame($listeners, $config->getListeners()); + $this->assertSame($options['switch_user'], $config->getSwitchUser()); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index e9c3e4068d530..b4f9b3e0782c0 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -38,6 +38,8 @@ */ class SwitchUserListener implements ListenerInterface { + const EXIT_VALUE = '_exit'; + private $tokenStorage; private $provider; private $userChecker; @@ -80,7 +82,7 @@ public function handle(GetResponseEvent $event) return; } - if ('_exit' === $request->get($this->usernameParameter)) { + if (self::EXIT_VALUE === $request->get($this->usernameParameter)) { $this->tokenStorage->setToken($this->attemptExitUser($request)); } else { try { diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index 43013520c36ba..3174265c08d08 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -73,7 +73,7 @@ public function testExitUserThrowsAuthenticationExceptionIfOriginalTokenCannotBe $token = new UsernamePasswordToken('username', '', 'key', array('ROLE_FOO')); $this->tokenStorage->setToken($token); - $this->request->query->set('_switch_user', '_exit'); + $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); @@ -84,7 +84,7 @@ public function testExitUserUpdatesToken() $originalToken = new UsernamePasswordToken('username', '', 'key', array()); $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', array(new SwitchUserRole('ROLE_PREVIOUS', $originalToken)))); - $this->request->query->set('_switch_user', '_exit'); + $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); @@ -108,7 +108,7 @@ public function testExitUserDispatchesEventWithRefreshedUser() ->willReturn($refreshedUser); $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', array(new SwitchUserRole('ROLE_PREVIOUS', $originalToken)))); - $this->request->query->set('_switch_user', '_exit'); + $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); $dispatcher @@ -132,7 +132,7 @@ public function testExitUserDoesNotDispatchEventWithStringUser() ->method('refreshUser'); $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); $this->tokenStorage->setToken(new UsernamePasswordToken('username', '', 'key', array(new SwitchUserRole('ROLE_PREVIOUS', $originalToken)))); - $this->request->query->set('_switch_user', '_exit'); + $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); $dispatcher