Merge branch '3.4'

* 3.4:
  [FrameworkBundle] Register a NullLogger from test kernels
  [SecurityBundle] Deprecate auto picking the first provider
  [Security] Add user impersonation support for stateless authentication
This commit is contained in:
Fabien Potencier 2017-09-30 06:47:08 -07:00
commit bc4a69225f
21 changed files with 176 additions and 19 deletions

View File

@ -316,6 +316,13 @@ SecurityBundle
* Deprecated the HTTP digest authentication: `HttpDigestFactory` will be removed in 4.0.
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.
* Not configuring explicitly the provider on a firewall is ambiguous when there is more than one registered provider.
Using the first configured provider is deprecated since 3.4 and will throw an exception on 4.0.
Explicitly configure the provider to use on your firewalls.
Translation
-----------

View File

@ -693,6 +693,12 @@ SecurityBundle
* Removed the HTTP digest authentication system. The `HttpDigestFactory` class
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.
* Not configuring explicitly the provider on a firewall is ambiguous when there is more than one registered provider.
The first configured provider is not used anymore and an exception is thrown instead.
Explicitly configure the provider to use on your firewalls.
Serializer
----------

View File

@ -11,8 +11,10 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Command\CacheClearCommand\Fixture;
use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
class TestAppKernel extends Kernel
@ -33,4 +35,9 @@ class TestAppKernel extends Kernel
{
$loader->load(__DIR__.DIRECTORY_SEPARATOR.'config.yml');
}
protected function build(ContainerBuilder $container)
{
$container->register('logger', NullLogger::class);
}
}

View File

@ -11,7 +11,9 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app;
use Psr\Log\NullLogger;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Kernel;
@ -72,6 +74,11 @@ class AppKernel extends Kernel
$loader->load($this->rootConfig);
}
protected function build(ContainerBuilder $container)
{
$container->register('logger', NullLogger::class);
}
public function serialize()
{
return serialize(array($this->varDir, $this->testCase, $this->rootConfig, $this->getEnvironment(), $this->isDebug()));

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel;
use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
@ -77,6 +78,7 @@ class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
{
$c->register('logger', NullLogger::class);
$c->loadFromExtension('framework', array(
'secret' => '$ecret',
));

View File

@ -30,6 +30,8 @@ CHANGELOG
* deprecated command `acl:set` along with `SetAclCommand` class
* deprecated command `init:acl` along with `InitAclCommand` class
* Added support for the new Argon2i password encoder
* added `stateless` option to the `switch_user` listener
* deprecated auto picking the first registered provider when no configured provider on a firewall and ambiguous
3.3.0
-----

View File

@ -254,6 +254,7 @@ class MainConfiguration implements ConfigurationInterface
->scalarNode('provider')->end()
->scalarNode('parameter')->defaultValue('_switch_user')->end()
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
->booleanNode('stateless')->defaultValue(false)->end()
->end()
->end()
;

View File

@ -262,6 +262,10 @@ class SecurityExtension extends Extension
$defaultProvider = $providerIds[$normalizedName];
} else {
$defaultProvider = reset($providerIds);
if (count($providerIds) > 1) {
@trigger_error(sprintf('Firewall "%s" has no "provider" set but multiple providers exist. Using the first configured provider (%s) is deprecated since 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead.', $id, key($providerIds)), E_USER_DEPRECATED);
}
}
$config->replaceArgument(5, $defaultProvider);
@ -359,7 +363,7 @@ class SecurityExtension extends Extension
// Switch user listener
if (isset($firewall['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
@ -602,10 +606,15 @@ class SecurityExtension extends Extension
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;
// 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;
$listener = $container->setDefinition($switchUserListenerId, new ChildDefinition('security.authentication.switchuser_listener'));
$listener->replaceArgument(1, new Reference($userProvider));
@ -613,6 +622,7 @@ class SecurityExtension extends Extension
$listener->replaceArgument(3, $id);
$listener->replaceArgument(6, $config['parameter']);
$listener->replaceArgument(7, $config['role']);
$listener->replaceArgument(9, $config['stateless']);
return $switchUserListenerId;
}

View File

@ -230,6 +230,7 @@
<argument>_switch_user</argument>
<argument>ROLE_ALLOWED_TO_SWITCH</argument>
<argument type="service" id="event_dispatcher" on-invalid="null"/>
<argument>false</argument> <!-- Stateless -->
</service>
<service id="security.access_listener" class="Symfony\Component\Security\Http\Firewall\AccessListener">

View File

@ -116,6 +116,7 @@ abstract class CompleteConfigurationTest extends TestCase
array(
'parameter' => '_switch_user',
'role' => 'ROLE_ALLOWED_TO_SWITCH',
'stateless' => true,
),
),
array(

View File

@ -60,12 +60,13 @@ $container->loadFromExtension('security', array(
),
'firewalls' => array(
'simple' => array('pattern' => '/login', 'security' => false),
'simple' => array('provider' => 'default', 'pattern' => '/login', 'security' => false),
'secure' => array('stateless' => true,
'provider' => 'default',
'http_basic' => true,
'form_login' => true,
'anonymous' => true,
'switch_user' => true,
'switch_user' => array('stateless' => true),
'x509' => true,
'remote_user' => true,
'logout' => true,
@ -74,6 +75,7 @@ $container->loadFromExtension('security', array(
'logout_on_user_change' => true,
),
'host' => array(
'provider' => 'default',
'pattern' => '/test',
'host' => 'foo\\.example\\.org',
'methods' => array('GET', 'POST'),
@ -82,6 +84,7 @@ $container->loadFromExtension('security', array(
'logout_on_user_change' => true,
),
'with_user_checker' => array(
'provider' => 'default',
'user_checker' => 'app.user_checker',
'anonymous' => true,
'http_basic' => true,

View File

@ -17,7 +17,7 @@ $container->loadFromExtension('security', array(
'http_basic' => true,
'form_login' => true,
'anonymous' => true,
'switch_user' => true,
'switch_user' => array('stateless' => true),
'x509' => true,
'remote_user' => true,
'logout' => true,

View File

@ -43,13 +43,13 @@
<chain providers="service, basic" />
</provider>
<firewall name="simple" pattern="/login" security="false" />
<firewall name="simple" pattern="/login" security="false" provider="default" />
<firewall name="secure" stateless="true">
<firewall name="secure" stateless="true" provider="default">
<http-basic />
<form-login />
<anonymous />
<switch-user />
<switch-user stateless="true" />
<x509 />
<remote-user />
<user-checker />
@ -57,12 +57,12 @@
<remember-me secret="TheSecret"/>
</firewall>
<firewall name="host" pattern="/test" host="foo\.example\.org" methods="GET,POST" logout-on-user-change="true">
<firewall name="host" pattern="/test" host="foo\.example\.org" methods="GET,POST" logout-on-user-change="true" provider="default">
<anonymous />
<http-basic />
</firewall>
<firewall name="with_user_checker" logout-on-user-change="true">
<firewall name="with_user_checker" logout-on-user-change="true" provider="default">
<anonymous />
<http-basic />
<user-checker>app.user_checker</user-checker>

View File

@ -17,7 +17,7 @@
<http-basic />
<form-login />
<anonymous />
<switch-user />
<switch-user stateless="true" />
<x509 />
<remote-user />
<user-checker />

View File

@ -43,11 +43,13 @@ security:
firewalls:
simple: { pattern: /login, security: false }
secure:
provider: default
stateless: true
http_basic: true
form_login: true
anonymous: true
switch_user: true
switch_user:
stateless: true
x509: true
remote_user: true
logout: true
@ -56,6 +58,7 @@ security:
user_checker: ~
host:
provider: default
pattern: /test
host: foo\.example\.org
methods: [GET,POST]
@ -64,6 +67,7 @@ security:
logout_on_user_change: true
with_user_checker:
provider: default
anonymous: ~
http_basic: ~
user_checker: app.user_checker

View File

@ -12,7 +12,8 @@ security:
http_basic: true
form_login: true
anonymous: true
switch_user: true
switch_user:
stateless: true
x509: true
remote_user: true
logout: true

View File

@ -148,6 +148,57 @@ class SecurityExtensionTest extends TestCase
$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();
}
/**
* @group legacy
* @expectedDeprecation Firewall "default" has no "provider" set but multiple providers exist. Using the first configured provider (first) is deprecated since 3.4 and will throw an exception in 4.0, set the "provider" key on the firewall instead.
*/
public function testDeprecationForAmbiguousProvider()
{
$container = $this->getRawContainer();
$container->loadFromExtension('security', array(
'providers' => array(
'first' => array('id' => 'foo'),
'second' => array('id' => 'bar'),
),
'firewalls' => array(
'default' => array(
'http_basic' => null,
'logout_on_user_change' => true,
),
),
));
$container->compile();
}
protected function getRawContainer()
{
$container = new ContainerBuilder();

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
class SwitchUserTest extends WebTestCase
@ -50,6 +51,18 @@ class SwitchUserTest extends WebTestCase
$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()
{
return array(

View File

@ -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

View File

@ -49,8 +49,9 @@ class SwitchUserListener implements ListenerInterface
private $role;
private $logger;
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)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
@ -65,6 +66,7 @@ class SwitchUserListener implements ListenerInterface
$this->role = $role;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->stateless = $stateless;
}
/**
@ -92,12 +94,13 @@ class SwitchUserListener implements ListenerInterface
}
}
$request->query->remove($this->usernameParameter);
$request->server->set('QUERY_STRING', http_build_query($request->query->all()));
if (!$this->stateless) {
$request->query->remove($this->usernameParameter);
$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);
}
}
/**

View File

@ -266,4 +266,29 @@ class SwitchUserListenerTest extends TestCase
$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());
}
}