Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 38b7fde

Browse files
committed
added support for expression in control access rules
1 parent 2777ac7 commit 38b7fde

File tree

15 files changed

+229
-9
lines changed

15 files changed

+229
-9
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode)
169169
->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
170170
->prototype('scalar')->end()
171171
->end()
172+
->scalarNode('allow_if')->defaultNull()->end()
172173
->end()
173174
->fixXmlConfig('role')
174175
->children()

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\DependencyInjection\Reference;
2424
use Symfony\Component\DependencyInjection\Parameter;
2525
use Symfony\Component\Config\FileLocator;
26+
use Symfony\Component\ExpressionLanguage\Expression;
2627

2728
/**
2829
* SecurityExtension.
@@ -33,6 +34,7 @@
3334
class SecurityExtension extends Extension
3435
{
3536
private $requestMatchers = array();
37+
private $expressions = array();
3638
private $contextListeners = array();
3739
private $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me');
3840
private $factories = array();
@@ -188,8 +190,13 @@ private function createAuthorization($config, ContainerBuilder $container)
188190
$access['ips']
189191
);
190192

193+
$attributes = $access['roles'];
194+
if ($access['allow_if']) {
195+
$attributes[] = $this->createExpression($container, $access['allow_if']);
196+
}
197+
191198
$container->getDefinition('security.access_map')
192-
->addMethodCall('add', array($matcher, $access['roles'], $access['requires_channel']));
199+
->addMethodCall('add', array($matcher, $attributes, $access['requires_channel']));
193200
}
194201
}
195202

@@ -596,6 +603,21 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv
596603
return $switchUserListenerId;
597604
}
598605

606+
private function createExpression($container, $expression)
607+
{
608+
if (isset($this->expressions[$id = 'security.expression.'.sha1($expression)])) {
609+
return $this->expressions[$id];
610+
}
611+
612+
$container
613+
->register($id, 'Symfony\Component\ExpressionLanguage\Expression')
614+
->setPublic(false)
615+
->addArgument($expression)
616+
;
617+
618+
return $this->expressions[$id] = new Reference($id);
619+
}
620+
599621
private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array())
600622
{
601623
$serialized = serialize(array($path, $host, $methods, $ip, $attributes));

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,21 @@
3232
<parameter key="security.access.simple_role_voter.class">Symfony\Component\Security\Core\Authorization\Voter\RoleVoter</parameter>
3333
<parameter key="security.access.authenticated_voter.class">Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter</parameter>
3434
<parameter key="security.access.role_hierarchy_voter.class">Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter</parameter>
35+
<parameter key="security.access.expression_voter.class">Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter</parameter>
3536

3637
<parameter key="security.firewall.class">Symfony\Component\Security\Http\Firewall</parameter>
3738
<parameter key="security.firewall.map.class">Symfony\Bundle\SecurityBundle\Security\FirewallMap</parameter>
3839
<parameter key="security.firewall.context.class">Symfony\Bundle\SecurityBundle\Security\FirewallContext</parameter>
3940
<parameter key="security.matcher.class">Symfony\Component\HttpFoundation\RequestMatcher</parameter>
41+
<parameter key="security.expression_matcher.class">Symfony\Component\HttpFoundation\ExpressionRequestMatcher</parameter>
4042

4143
<parameter key="security.role_hierarchy.class">Symfony\Component\Security\Core\Role\RoleHierarchy</parameter>
4244

4345
<parameter key="security.http_utils.class">Symfony\Component\Security\Http\HttpUtils</parameter>
4446

4547
<parameter key="security.validator.user_password.class">Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator</parameter>
48+
49+
<parameter key="security.expression_language.class">Symfony\Component\Security\Core\Authorization\ExpressionLanguage</parameter>
4650
</parameters>
4751

4852
<services>
@@ -78,6 +82,7 @@
7882

7983
<service id="security.user_checker" class="%security.user_checker.class%" public="false" />
8084

85+
<service id="security.expression_language" class="%security.expression_language.class%" public="false" />
8186

8287
<!-- Authorization related services -->
8388
<service id="security.access.decision_manager" class="%security.access.decision_manager.class%" public="false">
@@ -104,6 +109,13 @@
104109
<tag name="security.voter" priority="245" />
105110
</service>
106111

112+
<service id="security.access.expression_voter" class="%security.access.expression_voter.class%" public="false">
113+
<argument type="service" id="security.expression_language" />
114+
<argument type="service" id="security.authentication.trust_resolver" />
115+
<argument type="service" id="security.role_hierarchy" on-invalid="null" />
116+
<tag name="security.voter" priority="245" />
117+
</service>
118+
107119

108120
<!-- Firewall related services -->
109121
<service id="security.firewall" class="%security.firewall.class%">

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Bundle\SecurityBundle\SecurityBundle;
1818
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
1919
use Symfony\Component\DependencyInjection\ContainerBuilder;
20+
use Symfony\Component\ExpressionLanguage\Expression;
2021

2122
abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase
2223
{
@@ -133,27 +134,31 @@ public function testAccess()
133134

134135
$matcherIds = array();
135136
foreach ($rules as $rule) {
136-
list($matcherId, $roles, $channel) = $rule;
137+
list($matcherId, $attributes, $channel) = $rule;
137138
$requestMatcher = $container->getDefinition($matcherId);
138139

139140
$this->assertFalse(isset($matcherIds[$matcherId]));
140141
$matcherIds[$matcherId] = true;
141142

142143
$i = count($matcherIds);
143144
if (1 === $i) {
144-
$this->assertEquals(array('ROLE_USER'), $roles);
145+
$this->assertEquals(array('ROLE_USER'), $attributes);
145146
$this->assertEquals('https', $channel);
146147
$this->assertEquals(
147148
array('/blog/524', null, array('GET', 'POST')),
148149
$requestMatcher->getArguments()
149150
);
150151
} elseif (2 === $i) {
151-
$this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $roles);
152+
$this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $attributes);
152153
$this->assertNull($channel);
153154
$this->assertEquals(
154155
array('/blog/.*'),
155156
$requestMatcher->getArguments()
156157
);
158+
} elseif (3 === $i) {
159+
$this->assertEquals('IS_AUTHENTICATED_ANONYMOUSLY', $attributes[0]);
160+
$expression = $container->getDefinition($attributes[1])->getArgument(0);
161+
$this->assertEquals("token.getUsername() =~ '/^admin/'", $expression);
157162
}
158163
}
159164
}

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
'access_control' => array(
8383
array('path' => '/blog/524', 'role' => 'ROLE_USER', 'requires_channel' => 'https', 'methods' => array('get', 'POST')),
8484
array('path' => '/blog/.*', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'),
85+
array('path' => '/blog/524', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'allow_if' => "token.getUsername() =~ '/^admin/'"),
8586
),
8687

8788
'role_hierarchy' => array(

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,6 @@
6868

6969
<rule path="/blog/524" role="ROLE_USER" requires-channel="https" methods="get,POST" />
7070
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' path="/blog/.*" />
71+
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' allow-if="token.getUsername() =~ '/^admin/'" path="/blog/524" />
7172
</config>
7273
</srv:container>

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ security:
6969
-
7070
path: /blog/.*
7171
role: IS_AUTHENTICATED_ANONYMOUSLY
72+
- { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUsername() =~ '/^admin/'" }

src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Resources/config/routing.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ form_logout:
3737
form_secure_action:
3838
path: /secure-but-not-covered-by-access-control
3939
defaults: { _controller: FormLoginBundle:Login:secure }
40+
41+
protected-via-expression:
42+
path: /protected-via-expression

src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ public function testSecurityConfigurationForMultipleIPAddresses($config)
9191
$this->assertRestricted($barredClient, '/secured-by-two-ips');
9292
}
9393

94+
/**
95+
* @dataProvider getConfigs
96+
*/
97+
public function testSecurityConfigurationForExpression($config)
98+
{
99+
$allowedClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array('HTTP_USER_AGENT' => 'Firefox 1.0'));
100+
$this->assertAllowed($allowedClient, '/protected-via-expression');
101+
102+
$barredClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array());
103+
$this->assertRestricted($barredClient, '/protected-via-expression');
104+
105+
$allowedClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array());
106+
107+
$allowedClient->request('GET', '/protected-via-expression');
108+
$form = $allowedClient->followRedirect()->selectButton('login')->form();
109+
$form['_username'] = 'johannes';
110+
$form['_password'] = 'test';
111+
$allowedClient->submit($form);
112+
$this->assertRedirect($allowedClient->getResponse(), '/protected-via-expression');
113+
$this->assertAllowed($allowedClient, '/protected-via-expression');
114+
}
115+
94116
private function assertAllowed($client, $path)
95117
{
96118
$client->request('GET', $path);

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ security:
3131
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }
3232
- { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: IS_AUTHENTICATED_ANONYMOUSLY }
3333
- { path: ^/highly_protected_resource$, roles: IS_ADMIN }
34+
- { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and object.headers.get('user-agent') =~ '/Firefox/i') or has_role('ROLE_USER')" }
3435
- { path: .*, roles: IS_AUTHENTICATED_FULLY }

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"symfony/twig-bundle": "~2.2",
2626
"symfony/form": "~2.1",
2727
"symfony/validator": "~2.2",
28-
"symfony/yaml": "~2.0"
28+
"symfony/yaml": "~2.0",
29+
"symfony/expression-language": "~2.4"
2930
},
3031
"autoload": {
3132
"psr-0": { "Symfony\\Bundle\\SecurityBundle\\": "" }
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Security\Core\Authorization;
13+
14+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;
15+
16+
/**
17+
* Adds some function to the default ExpressionLanguage.
18+
*
19+
* @author Fabien Potencier <[email protected]>
20+
*/
21+
class ExpressionLanguage extends BaseExpressionLanguage
22+
{
23+
protected function registerFunctions()
24+
{
25+
parent::registerFunctions();
26+
27+
$this->addFunction('is_anonymous', function () {
28+
return '$trust_resolver->isAnonymous($token)';
29+
}, function (array $variables) {
30+
return $variables['trust_resolver']->isAnonymous($variables['token']);
31+
});
32+
33+
$this->addFunction('is_authenticated', function () {
34+
return '!$trust_resolver->isAnonymous($token)';
35+
}, function (array $variables) {
36+
return !$variables['trust_resolver']->isAnonymous($variables['token']);
37+
});
38+
39+
$this->addFunction('is_fully_authenticated', function () {
40+
return '!$trust_resolver->isFullFledge($token)';
41+
}, function (array $variables) {
42+
return !$variables['trust_resolver']->isFullFledge($variables['token']);
43+
});
44+
45+
$this->addFunction('is_remember_me', function () {
46+
return '!$trust_resolver->isRememberMe($token)';
47+
}, function (array $variables) {
48+
return !$variables['trust_resolver']->isRememberMe($variables['token']);
49+
});
50+
51+
$this->addFunction('has_role', function ($role) {
52+
return sprintf('in_array(%s, $roles)', $role);
53+
}, function (array $variables, $role) {
54+
return in_array($role, $variables['roles']);
55+
});
56+
}
57+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Component\Security\Core\Authorization\Voter;
13+
14+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
15+
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
16+
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
17+
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
18+
use Symfony\Component\ExpressionLanguage\Expression;
19+
20+
/**
21+
* ExpressionVoter votes based on the evaluation of an expression.
22+
*
23+
* @author Fabien Potencier <[email protected]>
24+
*/
25+
class ExpressionVoter implements VoterInterface
26+
{
27+
private $expressionLanguage;
28+
private $trustResolver;
29+
private $roleHierarchy;
30+
31+
/**
32+
* Constructor.
33+
*
34+
* @param ExpressionLanguage $expressionLanguage
35+
*/
36+
public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null)
37+
{
38+
$this->expressionLanguage = $expressionLanguage;
39+
$this->trustResolver = $trustResolver;
40+
$this->roleHierarchy = $roleHierarchy;
41+
}
42+
43+
/**
44+
* {@inheritdoc}
45+
*/
46+
public function supportsAttribute($attribute)
47+
{
48+
return $attribute instanceof Expression;
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function supportsClass($class)
55+
{
56+
return true;
57+
}
58+
59+
/**
60+
* {@inheritdoc}
61+
*/
62+
public function vote(TokenInterface $token, $object, array $attributes)
63+
{
64+
if (null !== $this->roleHierarchy) {
65+
$roles = $this->roleHierarchy->getReachableRoles($token->getRoles());
66+
} else {
67+
$roles = $token->getRoles();
68+
}
69+
70+
$variables = array(
71+
'token' => $token,
72+
'user' => $token->getUser(),
73+
'object' => $object,
74+
'roles' => array_map(function ($role) { return $role->getRole(); }, $roles),
75+
'trust_resolver' => $this->trustResolver,
76+
);
77+
78+
$result = VoterInterface::ACCESS_ABSTAIN;
79+
foreach ($attributes as $attribute) {
80+
if (!$this->supportsAttribute($attribute)) {
81+
continue;
82+
}
83+
84+
$result = VoterInterface::ACCESS_DENIED;
85+
if ($this->expressionLanguage->evaluate($attribute, $variables)) {
86+
return VoterInterface::ACCESS_GRANTED;
87+
}
88+
}
89+
90+
return $result;
91+
}
92+
}

src/Symfony/Component/Security/Http/AccessMap.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ class AccessMap implements AccessMapInterface
2828
* Constructor.
2929
*
3030
* @param RequestMatcherInterface $requestMatcher A RequestMatcherInterface instance
31-
* @param array $roles An array of roles needed to access the resource
31+
* @param array $attributes An array of attributes to pass to the access decision manager (like roles)
3232
* @param string|null $channel The channel to enforce (http, https, or null)
3333
*/
34-
public function add(RequestMatcherInterface $requestMatcher, array $roles = array(), $channel = null)
34+
public function add(RequestMatcherInterface $requestMatcher, array $attributes = array(), $channel = null)
3535
{
36-
$this->map[] = array($requestMatcher, $roles, $channel);
36+
$this->map[] = array($requestMatcher, $attributes, $channel);
3737
}
3838

3939
public function getPatterns(Request $request)

src/Symfony/Component/Security/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"doctrine/common": "~2.2",
2929
"doctrine/dbal": "~2.2",
3030
"psr/log": "~1.0",
31-
"ircmaxell/password-compat": "1.0.*"
31+
"ircmaxell/password-compat": "1.0.*",
32+
"symfony/expression-language": "~2.4"
3233
},
3334
"suggest": {
3435
"symfony/class-loader": "",

0 commit comments

Comments
 (0)