diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 35631064bacf3..48bc7b44a991b 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG --- * The `security.access_control` now accepts a `RequestMatcherInterface` under the `request_matcher` option as scope configuration + * The `security.access_control` now accepts an `attributes` array to match request attributes in the `RequestMatcher` + * The `security.access_control` now accepts a `route` option to match request route in the `RequestMatcher` * Display the inherited roles of the logged-in user in the Web Debug Toolbar 6.0 diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index dec107d65dc18..43d936755dcb4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -147,6 +147,10 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode) ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() ->end() + ->arrayNode('attributes') + ->prototype('scalar')->end() + ->end() + ->scalarNode('route')->defaultNull()->end() ->arrayNode('methods') ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end() ->prototype('scalar')->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 803fc5ed5d796..707a4303e0ff3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -199,24 +199,34 @@ private function createAuthorization(array $config, ContainerBuilder $container) { foreach ($config['access_control'] as $access) { if (isset($access['request_matcher'])) { - if ($access['path'] || $access['host'] || $access['port'] || $access['ips'] || $access['methods']) { + if ($access['path'] || $access['host'] || $access['port'] || $access['ips'] || $access['methods'] || $access['attributes'] || $access['route']) { throw new InvalidConfigurationException('The "request_matcher" option should not be specified alongside other options. Consider integrating your constraints inside your RequestMatcher directly.'); } $matcher = new Reference($access['request_matcher']); } else { + $attributes = $access['attributes']; + + if ($access['route']) { + if (\array_key_exists('_route', $attributes)) { + throw new InvalidConfigurationException('The "route" option should not be specified alongside "attributes._route" option. Use just one of the options.'); + } + $attributes['_route'] = $access['route']; + } + $matcher = $this->createRequestMatcher( $container, $access['path'], $access['host'], $access['port'], $access['methods'], - $access['ips'] + $access['ips'], + $attributes ); } - $attributes = $access['roles']; + $roles = $access['roles']; if ($access['allow_if']) { - $attributes[] = $this->createExpression($container, $access['allow_if']); + $roles[] = $this->createExpression($container, $access['allow_if']); } $emptyAccess = 0 === \count(array_filter($access)); @@ -226,7 +236,7 @@ private function createAuthorization(array $config, ContainerBuilder $container) } $container->getDefinition('security.access_map') - ->addMethodCall('add', [$matcher, $attributes, $access['requires_channel']]); + ->addMethodCall('add', [$matcher, $roles, $access['requires_channel']]); } // allow cache warm-up for expressions diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index bb74850729a58..c381845d9472a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -323,6 +323,105 @@ public function provideAdditionalRequestMatcherConstraints() yield 'Invalid configuration with port' => [['port' => 80]]; yield 'Invalid configuration with methods' => [['methods' => ['POST']]]; yield 'Invalid configuration with ips' => [['ips' => ['0.0.0.0']]]; + yield 'Invalid configuration with attributes' => [['attributes' => ['_route' => 'foo_route']]]; + yield 'Invalid configuration with route' => [['route' => 'foo_route']]; + } + + public function testRegisterAccessControlWithSpecifiedAttributes() + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + 'firewalls' => [ + 'some_firewall' => [ + 'pattern' => '/.*', + 'http_basic' => [], + ], + ], + 'access_control' => [ + ['attributes' => ['_route' => 'foo_route']], + ], + ]); + + $container->compile(); + + $accessMap = $container->getDefinition('security.access_map'); + $this->assertCount(1, $accessMap->getMethodCalls()); + $call = $accessMap->getMethodCalls()[0]; + $this->assertSame('add', $call[0]); + $args = $call[1]; + $requestMatcherId = (string) $args[0]; + + $requestMatcherDefinition = $container->getDefinition($requestMatcherId); + $requestMatcherConstructorArguments = $requestMatcherDefinition->getArguments(); + $this->assertArrayHasKey(4, $requestMatcherConstructorArguments); + $attributes = $requestMatcherConstructorArguments[4]; + $this->assertArrayHasKey('_route', $attributes); + $this->assertSame('foo_route', $attributes['_route']); + } + + public function testRegisterAccessControlWithSpecifiedRoute() + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + 'firewalls' => [ + 'some_firewall' => [ + 'pattern' => '/.*', + 'http_basic' => [], + ], + ], + 'access_control' => [ + ['route' => 'foo_route'], + ], + ]); + + $container->compile(); + + $accessMap = $container->getDefinition('security.access_map'); + $this->assertCount(1, $accessMap->getMethodCalls()); + $call = $accessMap->getMethodCalls()[0]; + $this->assertSame('add', $call[0]); + $args = $call[1]; + $requestMatcherId = (string) $args[0]; + + $requestMatcherDefinition = $container->getDefinition($requestMatcherId); + $requestMatcherConstructorArguments = $requestMatcherDefinition->getArguments(); + $this->assertArrayHasKey(4, $requestMatcherConstructorArguments); + $attributes = $requestMatcherConstructorArguments[4]; + $this->assertArrayHasKey('_route', $attributes); + $this->assertSame('foo_route', $attributes['_route']); + } + + public function testRegisterAccessControlWithSpecifiedAttributesThrowsException() + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + 'firewalls' => [ + 'some_firewall' => [ + 'pattern' => '/.*', + 'http_basic' => [], + ], + ], + 'access_control' => [ + ['route' => 'anything', 'attributes' => ['_route' => 'foo_route']], + ], + ]); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The "route" option should not be specified alongside "attributes._route" option. Use just one of the options.'); + + $container->compile(); } public function testRemovesExpressionCacheWarmerDefinitionIfNoExpressions()