[Security] Add user impersonation support for stateless authentication
This commit is contained in:
parent
09afa64909
commit
e7a5803e2e
@ -317,6 +317,9 @@ SecurityBundle
|
|||||||
* Deprecated the HTTP digest authentication: `HttpDigestFactory` will be removed in 4.0.
|
* Deprecated the HTTP digest authentication: `HttpDigestFactory` will be removed in 4.0.
|
||||||
Use another authentication system like `http_basic` instead.
|
Use another authentication system like `http_basic` instead.
|
||||||
|
|
||||||
|
* Deprecated setting the `switch_user.stateless` option to false when the firewall is `stateless`.
|
||||||
|
Setting it to false will have no effect in 4.0.
|
||||||
|
|
||||||
Translation
|
Translation
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
@ -694,6 +694,8 @@ SecurityBundle
|
|||||||
* Removed the HTTP digest authentication system. The `HttpDigestFactory` class
|
* Removed the HTTP digest authentication system. The `HttpDigestFactory` class
|
||||||
has been removed. Use another authentication system like `http_basic` instead.
|
has been removed. Use another authentication system like `http_basic` instead.
|
||||||
|
|
||||||
|
* The `switch_user.stateless` option is now always true if the firewall is stateless.
|
||||||
|
|
||||||
Serializer
|
Serializer
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ CHANGELOG
|
|||||||
* deprecated command `acl:set` along with `SetAclCommand` class
|
* deprecated command `acl:set` along with `SetAclCommand` class
|
||||||
* deprecated command `init:acl` along with `InitAclCommand` class
|
* deprecated command `init:acl` along with `InitAclCommand` class
|
||||||
* Added support for the new Argon2i password encoder
|
* Added support for the new Argon2i password encoder
|
||||||
|
* added `stateless` option to the `switch_user` listener
|
||||||
|
|
||||||
3.3.0
|
3.3.0
|
||||||
-----
|
-----
|
||||||
|
@ -304,6 +304,7 @@ class MainConfiguration implements ConfigurationInterface
|
|||||||
->scalarNode('provider')->end()
|
->scalarNode('provider')->end()
|
||||||
->scalarNode('parameter')->defaultValue('_switch_user')->end()
|
->scalarNode('parameter')->defaultValue('_switch_user')->end()
|
||||||
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
|
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
|
||||||
|
->booleanNode('stateless')->defaultValue(false)->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
@ -456,7 +456,7 @@ class SecurityExtension extends Extension
|
|||||||
// Switch user listener
|
// Switch user listener
|
||||||
if (isset($firewall['switch_user'])) {
|
if (isset($firewall['switch_user'])) {
|
||||||
$listenerKeys[] = 'switch_user';
|
$listenerKeys[] = 'switch_user';
|
||||||
$listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider));
|
$listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless']));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access listener
|
// Access listener
|
||||||
@ -699,10 +699,15 @@ class SecurityExtension extends Extension
|
|||||||
return $exceptionListenerId;
|
return $exceptionListenerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createSwitchUserListener($container, $id, $config, $defaultProvider)
|
private function createSwitchUserListener($container, $id, $config, $defaultProvider, $stateless)
|
||||||
{
|
{
|
||||||
$userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
|
$userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
|
||||||
|
|
||||||
|
// in 4.0, ignore the `switch_user.stateless` key if $stateless is `true`
|
||||||
|
if ($stateless && false === $config['stateless']) {
|
||||||
|
@trigger_error(sprintf('Firewall "%s" is configured as "stateless" but the "switch_user.stateless" key is set to false. Both should have the same value, the firewall\'s "stateless" value will be used as default value for the "switch_user.stateless" key in 4.0.', $id), E_USER_DEPRECATED);
|
||||||
|
}
|
||||||
|
|
||||||
$switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
|
$switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
|
||||||
$listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
|
$listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
|
||||||
$listener->replaceArgument(1, new Reference($userProvider));
|
$listener->replaceArgument(1, new Reference($userProvider));
|
||||||
@ -710,6 +715,7 @@ class SecurityExtension extends Extension
|
|||||||
$listener->replaceArgument(3, $id);
|
$listener->replaceArgument(3, $id);
|
||||||
$listener->replaceArgument(6, $config['parameter']);
|
$listener->replaceArgument(6, $config['parameter']);
|
||||||
$listener->replaceArgument(7, $config['role']);
|
$listener->replaceArgument(7, $config['role']);
|
||||||
|
$listener->replaceArgument(9, $config['stateless']);
|
||||||
|
|
||||||
return $switchUserListenerId;
|
return $switchUserListenerId;
|
||||||
}
|
}
|
||||||
|
@ -241,6 +241,7 @@
|
|||||||
<argument>_switch_user</argument>
|
<argument>_switch_user</argument>
|
||||||
<argument>ROLE_ALLOWED_TO_SWITCH</argument>
|
<argument>ROLE_ALLOWED_TO_SWITCH</argument>
|
||||||
<argument type="service" id="event_dispatcher" on-invalid="null"/>
|
<argument type="service" id="event_dispatcher" on-invalid="null"/>
|
||||||
|
<argument>false</argument> <!-- Stateless -->
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service id="security.access_listener" class="Symfony\Component\Security\Http\Firewall\AccessListener">
|
<service id="security.access_listener" class="Symfony\Component\Security\Http\Firewall\AccessListener">
|
||||||
|
@ -130,6 +130,7 @@ abstract class CompleteConfigurationTest extends TestCase
|
|||||||
array(
|
array(
|
||||||
'parameter' => '_switch_user',
|
'parameter' => '_switch_user',
|
||||||
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
||||||
|
'stateless' => true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
@ -256,6 +257,7 @@ abstract class CompleteConfigurationTest extends TestCase
|
|||||||
array(
|
array(
|
||||||
'parameter' => '_switch_user',
|
'parameter' => '_switch_user',
|
||||||
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
'role' => 'ROLE_ALLOWED_TO_SWITCH',
|
||||||
|
'stateless' => true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
|
@ -65,7 +65,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_basic' => true,
|
'http_basic' => true,
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -67,7 +67,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_digest' => array('secret' => 'TheSecret'),
|
'http_digest' => array('secret' => 'TheSecret'),
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -67,7 +67,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_digest' => array('secret' => 'TheSecret'),
|
'http_digest' => array('secret' => 'TheSecret'),
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -17,7 +17,7 @@ $container->loadFromExtension('security', array(
|
|||||||
'http_basic' => true,
|
'http_basic' => true,
|
||||||
'form_login' => true,
|
'form_login' => true,
|
||||||
'anonymous' => true,
|
'anonymous' => true,
|
||||||
'switch_user' => true,
|
'switch_user' => array('stateless' => true),
|
||||||
'x509' => true,
|
'x509' => true,
|
||||||
'remote_user' => true,
|
'remote_user' => true,
|
||||||
'logout' => true,
|
'logout' => true,
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
<http-basic />
|
<http-basic />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<http-digest secret="TheSecret" />
|
<http-digest secret="TheSecret" />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<http-digest secret="TheSecret" />
|
<http-digest secret="TheSecret" />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<http-basic />
|
<http-basic />
|
||||||
<form-login />
|
<form-login />
|
||||||
<anonymous />
|
<anonymous />
|
||||||
<switch-user />
|
<switch-user stateless="true" />
|
||||||
<x509 />
|
<x509 />
|
||||||
<remote-user />
|
<remote-user />
|
||||||
<user-checker />
|
<user-checker />
|
||||||
|
@ -47,7 +47,8 @@ security:
|
|||||||
http_basic: true
|
http_basic: true
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -50,7 +50,8 @@ security:
|
|||||||
secret: TheSecret
|
secret: TheSecret
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -50,7 +50,8 @@ security:
|
|||||||
secret: TheSecret
|
secret: TheSecret
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -12,7 +12,8 @@ security:
|
|||||||
http_basic: true
|
http_basic: true
|
||||||
form_login: true
|
form_login: true
|
||||||
anonymous: true
|
anonymous: true
|
||||||
switch_user: true
|
switch_user:
|
||||||
|
stateless: true
|
||||||
x509: true
|
x509: true
|
||||||
remote_user: true
|
remote_user: true
|
||||||
logout: true
|
logout: true
|
||||||
|
@ -148,6 +148,32 @@ class SecurityExtensionTest extends TestCase
|
|||||||
$container->compile();
|
$container->compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
* @expectedDeprecation Firewall "some_firewall" is configured as "stateless" but the "switch_user.stateless" key is set to false. Both should have the same value, the firewall's "stateless" value will be used as default value for the "switch_user.stateless" key in 4.0.
|
||||||
|
*/
|
||||||
|
public function testSwitchUserNotStatelessOnStatelessFirewall()
|
||||||
|
{
|
||||||
|
$container = $this->getRawContainer();
|
||||||
|
|
||||||
|
$container->loadFromExtension('security', array(
|
||||||
|
'providers' => array(
|
||||||
|
'default' => array('id' => 'foo'),
|
||||||
|
),
|
||||||
|
|
||||||
|
'firewalls' => array(
|
||||||
|
'some_firewall' => array(
|
||||||
|
'stateless' => true,
|
||||||
|
'http_basic' => null,
|
||||||
|
'switch_user' => array('stateless' => false),
|
||||||
|
'logout_on_user_change' => true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$container->compile();
|
||||||
|
}
|
||||||
|
|
||||||
protected function getRawContainer()
|
protected function getRawContainer()
|
||||||
{
|
{
|
||||||
$container = new ContainerBuilder();
|
$container = new ContainerBuilder();
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
|
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
|
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
|
||||||
|
|
||||||
class SwitchUserTest extends WebTestCase
|
class SwitchUserTest extends WebTestCase
|
||||||
@ -50,6 +51,18 @@ class SwitchUserTest extends WebTestCase
|
|||||||
$this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser());
|
$this->assertEquals('user_can_switch', $client->getProfile()->getCollector('security')->getUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSwitchUserStateless()
|
||||||
|
{
|
||||||
|
$client = $this->createClient(array('test_case' => 'JsonLogin', 'root_config' => 'switchuser_stateless.yml'));
|
||||||
|
$client->request('POST', '/chk', array('_switch_user' => 'dunglas'), array(), array('CONTENT_TYPE' => 'application/json'), '{"user": {"login": "user_can_switch", "password": "test"}}');
|
||||||
|
$response = $client->getResponse();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(JsonResponse::class, $response);
|
||||||
|
$this->assertSame(200, $response->getStatusCode());
|
||||||
|
$this->assertSame(array('message' => 'Welcome @dunglas!'), json_decode($response->getContent(), true));
|
||||||
|
$this->assertSame('dunglas', $client->getProfile()->getCollector('security')->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
public function getTestParameters()
|
public function getTestParameters()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
imports:
|
||||||
|
- { resource: ./config.yml }
|
||||||
|
|
||||||
|
security:
|
||||||
|
providers:
|
||||||
|
in_memory:
|
||||||
|
memory:
|
||||||
|
users:
|
||||||
|
user_can_switch: { password: test, roles: [ROLE_USER, ROLE_ALLOWED_TO_SWITCH] }
|
||||||
|
firewalls:
|
||||||
|
main:
|
||||||
|
switch_user:
|
||||||
|
stateless: true
|
@ -49,8 +49,9 @@ class SwitchUserListener implements ListenerInterface
|
|||||||
private $role;
|
private $role;
|
||||||
private $logger;
|
private $logger;
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
|
private $stateless;
|
||||||
|
|
||||||
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, $providerKey, AccessDecisionManagerInterface $accessDecisionManager, LoggerInterface $logger = null, $usernameParameter = '_switch_user', $role = 'ROLE_ALLOWED_TO_SWITCH', EventDispatcherInterface $dispatcher = null)
|
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, $providerKey, AccessDecisionManagerInterface $accessDecisionManager, LoggerInterface $logger = null, $usernameParameter = '_switch_user', $role = 'ROLE_ALLOWED_TO_SWITCH', EventDispatcherInterface $dispatcher = null, $stateless = false)
|
||||||
{
|
{
|
||||||
if (empty($providerKey)) {
|
if (empty($providerKey)) {
|
||||||
throw new \InvalidArgumentException('$providerKey must not be empty.');
|
throw new \InvalidArgumentException('$providerKey must not be empty.');
|
||||||
@ -65,6 +66,7 @@ class SwitchUserListener implements ListenerInterface
|
|||||||
$this->role = $role;
|
$this->role = $role;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
|
$this->stateless = $stateless;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,13 +94,14 @@ class SwitchUserListener implements ListenerInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->stateless) {
|
||||||
$request->query->remove($this->usernameParameter);
|
$request->query->remove($this->usernameParameter);
|
||||||
$request->server->set('QUERY_STRING', http_build_query($request->query->all()));
|
$request->server->set('QUERY_STRING', http_build_query($request->query->all()));
|
||||||
|
|
||||||
$response = new RedirectResponse($request->getUri(), 302);
|
$response = new RedirectResponse($request->getUri(), 302);
|
||||||
|
|
||||||
$event->setResponse($response);
|
$event->setResponse($response);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to switch to another user.
|
* Attempts to switch to another user.
|
||||||
|
@ -266,4 +266,29 @@ class SwitchUserListenerTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame($replacedToken, $this->tokenStorage->getToken());
|
$this->assertSame($replacedToken, $this->tokenStorage->getToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSwitchUserStateless()
|
||||||
|
{
|
||||||
|
$token = new UsernamePasswordToken('username', '', 'key', array('ROLE_FOO'));
|
||||||
|
$user = new User('username', 'password', array());
|
||||||
|
|
||||||
|
$this->tokenStorage->setToken($token);
|
||||||
|
$this->request->query->set('_switch_user', 'kuba');
|
||||||
|
|
||||||
|
$this->accessDecisionManager->expects($this->once())
|
||||||
|
->method('decide')->with($token, array('ROLE_ALLOWED_TO_SWITCH'))
|
||||||
|
->will($this->returnValue(true));
|
||||||
|
|
||||||
|
$this->userProvider->expects($this->once())
|
||||||
|
->method('loadUserByUsername')->with('kuba')
|
||||||
|
->will($this->returnValue($user));
|
||||||
|
$this->userChecker->expects($this->once())
|
||||||
|
->method('checkPostAuth')->with($user);
|
||||||
|
|
||||||
|
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', null, true);
|
||||||
|
$listener->handle($this->event);
|
||||||
|
|
||||||
|
$this->assertInstanceOf('Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken', $this->tokenStorage->getToken());
|
||||||
|
$this->assertFalse($this->event->hasResponse());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user