8000 feature #26660 [SecurityBundle] allow using custom function inside al… · symfony/symfony@dc29b27 · GitHub
[go: up one dir, main page]

Skip to content

Commit dc29b27

Browse files
committed
feature #26660 [SecurityBundle] allow using custom function inside allow_if expressions (dmaicher)
This PR was merged into the 4.1-dev branch. Discussion ---------- [SecurityBundle] allow using custom function inside allow_if expressions | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | no | Fixed tickets | #23208 | License | MIT | Doc PR | symfony/symfony-docs#9552 This is a follow-up for #26263 As discussed there I propose to allow using custom functions as a new feature only and thus targeting `master` here. If we agree to move forward with this there are some todos: - [x] fix tests - [x] add cache warmer for allow_if expressions - [x] update documentation to mention this new feature - [x] update UPGRADE files ping @nicolas-grekas @stof Commits ------- 41552cd [SecurityBundle] allow using custom function inside allow_if expressions
2 parents 0f4c0e9 + 41552cd commit dc29b27

File tree

8 files changed

+176
-21
lines changed

8 files changed

+176
-21
lines changed

UPGRADE-4.1.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Security
8181
functionality, create a custom user-checker based on the
8282
`Symfony\Component\Security\Core\User\UserChecker`.
8383
* `AuthenticationUtils::getLastUsername()` now always returns a string.
84+
* The `ExpressionVoter::addExpressionLanguageProvider()` method is deprecated. Register the provider directly on the injected ExpressionLanguage instance instead.
8485

8586
SecurityBundle
8687
--------------

UPGRADE-5.0.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Security
7676

7777
* The `ContextListener::setLogoutOnUserChange()` method has been removed.
7878
* The `Symfony\Component\Security\Core\User\AdvancedUserInterface` has been removed.
79+
* The `ExpressionVoter::addExpressionLanguageProvider()` method has been removed.
7980

8081
SecurityBundle
8182
--------------
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\CacheWarmer;
13+
14+
use Symfony\Component\ExpressionLanguage\Expression;
15+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
16+
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
17+
18+
class ExpressionCacheWarmer implements CacheWarmerInterface
19+
{
20+
private $expressions;
21+
private $expressionLanguage;
22+
23+
/**
24+
* @param iterable|Expression[] $expressions
25+
*/
26+
public function __construct(iterable $expressions, ExpressionLanguage $expressionLanguage)
27+
{
28+
$this->expressions = $expressions;
29+
$this->expressionLanguage = $expressionLanguage;
30+
}
31+
32+
public function isOptional()
33+
{
34+
return true;
35+
}
36+
37+
public function warmUp($cacheDir)
38+
{
39+
foreach ($this->expressions as $expression) {
40+
$this->expressionLanguage->parse($expression, array('token', 'user', 'object', 'subject', 'roles', 'request', 'trust_resolver'));
41+
}
42+
}
43+
}

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

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
use Symfony\Component\DependencyInjection\Parameter;
2727
use Symfony\Component\DependencyInjection\Reference;
2828
use Symfony\Component\Config\FileLocator;
29-
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
3029
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
3130
use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder;
3231
use Symfony\Component\Security\Http\Controller\UserValueResolver;
@@ -45,7 +44,6 @@ class SecurityExtension extends Extension
4544
private $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me');
4645
private $factories = array();
4746
private $userProviderFactories = array();
48-
private $expressionLanguage;
4947

5048
public function __construct()
5149
{
@@ -136,10 +134,6 @@ private function createRoleHierarchy(array $config, ContainerBuilder $container)
136134

137135
private function createAuthorization($config, ContainerBuilder $container)
138136
{
139-
if (!$config['access_control']) {
140-
return;
141-
}
142-
143137
foreach ($config['access_control'] as $access) {
144138
$matcher = $this->createRequestMatcher(
145139
$container,
@@ -157,6 +151,14 @@ private function createAuthorization($config, ContainerBuilder $container)
157151
$container->getDefinition('security.access_map')
158152
->addMethodCall('add', array($matcher, $attributes, $access['requires_channel']));
159153
}
154+
155+
// allow cache warm-up for expressions
156+
if (count($this->expressions)) {
157+
$container->getDefinition('security.cache_warmer.expression')
158+
->replaceArgument(0, new IteratorArgument(array_values($this->expressions)));
159+
} else {
160+
$container->removeDefinition('security.cache_warmer.expression');
161+
}
160162
}
161163

162164
private function createFirewalls($config, ContainerBuilder $container)
@@ -636,11 +638,14 @@ private function createExpression($container, $expression)
636638
return $this->expressions[$id];
637639
}
638640

641+
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
642+
throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
643+
}
644+
639645
$container
640-
->register($id, 'Symfony\Component\ExpressionLanguage\SerializedParsedExpression')
646+
->register($id, 'Symfony\Component\ExpressionLanguage\Expression')
641647
->setPublic(false)
642648
->addArgument($expression)
643-
->addArgument(serialize($this->getExpressionLanguage()->parse($expression, array('token', 'user', 'object', 'roles', 'request', 'trust_resolver'))->getNodes()))
644649
;
645650

646651
return $this->expressions[$id] = new Reference($id);
@@ -703,16 +708,4 @@ public function getConfiguration(array $config, ContainerBuilder $container)
703708
// first assemble the factories
704709
return new MainConfiguration($this->factories, $this->userProviderFactories);
705710
}
706-
707-
private function getExpressionLanguage()
708-
{
709-
if (null === $this->expressionLanguage) {
710-
if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) {
711-
throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
712-
}
713-
$this->expressionLanguage = new ExpressionLanguage();
714-
}
715-
716-
return $this->expressionLanguage;
717-
}
718711
}

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@
8080

8181
<service id="security.user_checker" class="Symfony\Component\Security\Core\User\UserChecker" />
8282

83-
<service id="security.expression_language" class="Symfony\Component\Security\Core\Authorization\ExpressionLanguage" />
83+
<service id="security.expression_language" class="Symfony\Component\Security\Core\Authorization\ExpressionLanguage">
84+
<argument type="service" id="cache.security_expression_language"></argument>
85+
</service>
8486

8587
<service id="security.authentication_utils" class="Symfony\Component\Security\Http\Authentication\AuthenticationUtils" public="true">
8688
<argument type="service" id="request_stack" />
@@ -195,5 +197,17 @@
195197
<argument type="service" id="security.token_storage" />
196198
<argument type="service" id="security.encoder_factory" />
197199
</service>
200+
201+
<!-- Cache -->
202+
<service id="cache.security_expression_language" parent="cache.system" public="false">
203+
<tag name="cache.pool" />
204+
</service>
205+
206+
<!-- Cache Warmers -->
207+
<service id="security.cache_warmer.expression" class="Symfony\Bundle\SecurityBundle\CacheWarmer\ExpressionCacheWarmer">
208+
<tag name="kernel.cache_warmer" />
209+
<argument type="collection" /> <!-- expressions -->
210+
<argument type="service" id="security.expression_language" />
211+
</service>
198212
</services>
199213
</container>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\CacheWarmer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\SecurityBundle\CacheWarmer\ExpressionCacheWarmer;
16+
use Symfony\Component\ExpressionLanguage\Expression;
17+
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
18+
19+
class ExpressionCacheWarmerTest extends TestCase
20+
{
21+
public function testWarmUp()
22+
{
23+
$expressions = array(new Expression('A'), new Expression('B'));
24+
25+
$expressionLang = $this->createMock(ExpressionLanguage::class);
26+
$expressionLang->expects($this->exactly(2))
27+
->method('parse')
28+
->withConsecutive(
29+
array($expressions[0], array('token', 'user', 'object', 'subject', 'roles', 'request', 'trust_resolver')),
30+
array($expressions[1], array('token', 'user', 'object', 'subject', 'roles', 'request', 'trust_resolver'))
31+
);
32+
33+
(new ExpressionCacheWarmer($expressions, $expressionLang))->warmUp('');
34+
}
35+
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
1616
use Symfony\Bundle\SecurityBundle\SecurityB 10000 undle;
1717
use Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Fixtures\UserProvider\DummyProvider;
18+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1819
use Symfony\Component\DependencyInjection\ContainerBuilder;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
use Symfony\Component\ExpressionLanguage\Expression;
1922

2023
class SecurityExtensionTest extends TestCase
2124
{
@@ -207,6 +210,66 @@ public function testPerListenerProviderWithRememberMe()
207210
$this->addToAssertionCount(1);
208211
}
209212

213+
public function testRegisterRequestMatchersWithAllowIfExpression()
214+
{
215+
$container = $this->getRawContainer();
216+
217+
$rawExpression = "'foo' == 'bar' or 1 in [1, 3, 3]";
218+
219+
$container->loadFromExtension('security', array(
220+
'providers' => array(
221+
'default' => array('id' => 'foo'),
222+
),
223+
'firewalls' => array(
224+
'some_firewall' => array(
225+
'pattern' => '/.*',
226+
'http_basic' => array(),
227+
),
228+
),
229+
'access_control' => array(
230+
array('path' => '/', 'allow_if' => $rawExpression),
231+
),
232+
));
233+
234+
$container->compile();
235+
$accessMap = $container->getDefinition('security.access_map');
236+
$this->assertCount(1, $accessMap->getMethodCalls());
237+
$call = $accessMap->getMethodCalls()[0];
238+
$this->assertSame('add', $call[0]);
239+
$args = $call[1];
240+
$this->assertCount(3, $args);
241+
$expressionId = $args[1][0];
242+
$this->assertTrue($container->hasDefinition($expressionId));
243+
$expressionDef = $container->getDefinition($expressionId);
244+
$this->assertSame(Expression::class, $expressionDef->getClass());
245+
$this->assertSame($rawExpression, $expressionDef->getArgument(0));
246+
247+
$this->assertTrue($container->hasDefinition('security.cache_warmer.expression'));
248+
$this->assertEquals(
249+
new IteratorArgument(array(new Reference($expressionId))),
250+
$container->getDefinition('security.cache_warmer.expression')->getArgument(0)
251+
);
252+
}
253+
254+
public function testRemovesExpressionCacheWarmerDefinitionIfNoExpressions()
255+
{
256+
$container = $this->getRawContainer();
257+
$container->loadFromExtension('security', array(
258+
'providers' => array(
259+
'default' => array('id' => 'foo'),
260+
),
261+
'firewalls' => array(
262+
'some_firewall' => array(
263+
'pattern' => '/.*',
264+
'http_basic' => array(),
265+
),
266+
),
267+
));
268+
$container->compile();
269+
270+
$this->assertFalse($container->hasDefinition('security.cache_warmer.expression'));
271+
}
272+
210273
protected function getRawContainer()
211274
{
212275
$container = new ContainerBuilder();

src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ public function __construct(ExpressionLanguage $expressionLanguage, Authenticati
3737
$this->roleHierarchy = $roleHierarchy;
3838
}
3939

40+
/**
41+
* @deprecated since Symfony 4.1, register the provider directly on the injected ExpressionLanguage instance instead.
42+
*/
4043
public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider)
4144
{
45+
@trigger_error(sprintf('The %s() method is deprecated since Symfony 4.1, register the provider directly on the injected ExpressionLanguage instance instead.', __METHOD__), E_USER_DEPRECATED);
46+
4247
$this->expressionLanguage->registerProvider($provider);
4348
}
4449

0 commit comments

Comments
 (0)
0