8000 New Component: Expression Language by fabpot · Pull Request #8913 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

New Component: Expression Language #8913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 19, 2013
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
added support for expression in control access rules
  • Loading branch information
fabpot committed Sep 19, 2013
commit 38b7fde8ed94021a62ccd4ab207980bc4501b749
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode)
->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
->prototype('scalar')->end()
->end()
->scalarNode('allow_if')->defaultNull()->end()
->end()
->fixXmlConfig('role')
->children()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\ExpressionLanguage\Expression;

/**
* SecurityExtension.
Expand All @@ -33,6 +34,7 @@
class SecurityExtension extends Extension
{
private $requestMatchers = array();
private $expressions = array();
private $contextListeners = array();
private $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me');
private $factories = array();
Expand Down Expand Up @@ -188,8 +190,13 @@ private function createAuthorization($config, ContainerBuilder $container)
$access['ips']
);

$attributes = $access['roles'];
if ($access['allow_if']) {
$attributes[] = $this->createExpression($container, $access['allow_if']);
}

$container->getDefinition('security.access_map')
->addMethodCall('add', array($matcher, $access['roles'], $access['requires_channel']));
->addMethodCall('add', array($matcher, $attributes, $access['requires_channel']));
}
}

Expand Down Expand Up @@ -596,6 +603,21 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv
return $switchUserListenerId;
}

private function createExpression($container, $expression)
{
if (isset($this->expressions[$id = 'security.expression.'.sha1($expression)])) {
return $this->expressions[$id];
}

$container
->register($id, 'Symfony\Component\ExpressionLanguage\Expression')
->setPublic(false)
->addArgument($expression)
;

return $this->expressions[$id] = new Reference($id);
}

private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array())
{
$serialized = serialize(array($path, $host, $methods, $ip, $attributes));
Expand Down
12 changes: 12 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,21 @@
<parameter key="security.access.simple_role_voter.class">Symfony\Component\Security\Core\Authorization\Voter\RoleVoter</parameter>
<parameter key="security.access.authenticated_voter.class">Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter</parameter>
<parameter key="security.access.role_hierarchy_voter.class">Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter</parameter>
<parameter key="security.access.expression_voter.class">Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter</parameter>

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

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

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

<parameter key="security.validator.user_password.class">Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator</parameter>

<parameter key="security.expression_language.class">Symfony\Component\Security\Core\Authorization\ExpressionLanguage</parameter>
</parameters>

<services>
Expand Down Expand Up @@ -78,6 +82,7 @@

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

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

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

<service id="security.access.expression_voter" class="%security.access.expression_voter.class%" public="false">
<argument type="service" id="security.expression_language" />
<argument type="service" id="security.authentication.trust_resolver" />
<argument type="service" id="security.role_hierarchy" on-invalid="null" />
<tag name="security.voter" priority="245" />
</service>


<!-- Firewall related services -->
<service id="security.firewall" class="%security.firewall.class%">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\ExpressionLanguage\Expression;

abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase
{
Expand Down Expand Up @@ -133,27 +134,31 @@ public function testAccess()

$matcherIds = array();
foreach ($rules as $rule) {
list($matcherId, $roles, $channel) = $rule;
list($matcherId, $attributes, $channel) = $rule;
$requestMatcher = $container->getDefinition($matcherId);

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

$i = count($matcherIds);
if (1 === $i) {
$this->assertEquals(array('ROLE_USER'), $roles);
$this->assertEquals(array('ROLE_USER'), $attributes);
$this->assertEquals('https', $channel);
$this->assertEquals(
array('/blog/524', null, array('GET', 'POST')),
$requestMatcher->getArguments()
);
} elseif (2 === $i) {
$this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $roles);
$this->assertEquals(array('IS_AUTHENTICATED_ANONYMOUSLY'), $attributes);
$this->assertNull($channel);
$this->assertEquals(
array('/blog/.*'),
$requestMatcher->getArguments()
);
} elseif (3 === $i) {
$this->assertEquals('IS_AUTHENTICATED_ANONYMOUSLY', $attributes[0]);
$expression = $container->getDefinition($attributes[1])->getArgument(0);
$this->assertEquals("token.getUsername() =~ '/^admin/'", $expression);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
'access_control' => array(
array('path' => '/blog/524', 'role' => 'ROLE_USER', 'requires_channel' => 'https', 'methods' => array('get', 'POST')),
array('path' => '/blog/.*', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'),
array('path' => '/blog/524', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'allow_if' => "token.getUsername() =~ '/^admin/'"),
),

'role_hierarchy' => array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@

<rule path="/blog/524" role="ROLE_USER" requires-channel="https" methods="get,POST" />
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' path="/blog/.*" />
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' allow-if="token.getUsername() =~ '/^admin/'" path="/blog/524" />
</config>
</srv:container>
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ security:
-
path: /blog/.*
role: IS_AUTHENTICATED_ANONYMOUSLY
- { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUsername() =~ '/^admin/'" }
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ form_logout:
form_secure_action:
path: /secure-but-not-covered-by-access-control
defaults: { _controller: FormLoginBundle:Login:secure }

protected-via-expression:
path: /protected-via-expression
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ public function testSecurityConfigurationForMultipleIPAddresses($config)
$this->assertRestricted($barredClient, '/secured-by-two-ips');
}

/**
* @dataProvider getConfigs
*/
public function testSecurityConfigurationForExpression($config)
{
$allowedClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array('HTTP_USER_AGENT' => 'Firefox 1.0'));
$this->assertAllowed($allowedClient, '/protected-via-expression');

$barredClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array());
$this->assertRestricted($barredClient, '/protected-via-expression');

$allowedClient = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => $config), array());

$allowedClient->request('GET', '/protected-via-expression');
$form = $allowedClient->followRedirect()->selectButton('login')->form();
$form['_username'] = 'johannes';
$form['_password'] = 'test';
$allowedClient->submit($form);
$this->assertRedirect($allowedClient->getResponse(), '/protected-via-expression');
$this->assertAllowed($allowedClient, '/protected-via-expression');
}

private function assertAllowed($client, $path)
{
$client->request('GET', $path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ security:
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/secured-by-two-ips$, ips: [1.1.1.1, 2.2.2.2], roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/highly_protected_resource$, roles: IS_ADMIN }
- { path: ^/protected-via-expression$, allow_if: "(is_anonymous() and object.headers.get('user-agent') =~ '/Firefox/i') or has_role('ROLE_USER')" }
- { path: .*, roles: IS_AUTHENTICATED_FULLY }
3 changes: 2 additions & 1 deletion src/Symfony/Bundle/SecurityBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"symfony/twig-bundle": "~2.2",
"symfony/form": "~2.1",
"symfony/validator": "~2.2",
"symfony/yaml": "~2.0"
"symfony/yaml": "~2.0",
"symfony/expression-language": "~2.4"
},
"autoload": {
"psr-0": { "Symfony\\Bundle\\SecurityBundle\\": "" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authorization;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage as BaseExpressionLanguage;

/**
* Adds some function to the default ExpressionLanguage.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionLanguage extends BaseExpressionLanguage
{
protected function registerFunctions()
{
parent::registerFunctions();

$this->addFunction('is_anonymous', function () {
return '$trust_resolver->isAnonymous($token)';
}, function (array $variables) {
return $variables['trust_resolver']->isAnonymous($variables['token']);
});

$this->addFunction('is_authenticated', function () {
return '!$trust_resolver->isAnonymous($token)';
}, function (array $variables) {
return !$variables['trust_resolver']->isAnonymous($variables['token']);
});

$this->addFunction('is_fully_authenticated', function () {
return '!$trust_resolver->isFullFledge($token)';
}, function (array $variables) {
return !$variables['trust_resolver']->isFullFledge($variables['token']);
});

$this->addFunction('is_remember_me', function () {
return '!$trust_resolver->isRememberMe($token)';
}, function (array $variables) {
return !$variables['trust_resolver']->isRememberMe($variables['token']);
});

$this->addFunction('has_role', function ($role) {
return sprintf('in_array(%s, $roles)', $role);
}, function (array $variables, $role) {
return in_array($role, $variables['roles']);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authorization\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\ExpressionLanguage\Expression;

/**
* ExpressionVoter votes based on the evaluation of an expression.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExpressionVoter implements VoterInterface
{
private $expressionLanguage;
private $trustResolver;
private $roleHierarchy;

/**
* Constructor.
*
* @param ExpressionLanguage $expressionLanguage
*/
public function __construct(ExpressionLanguage $expressionLanguage, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null)
{
$this->expressionLanguage = $expressionLanguage;
$this->trustResolver = $trustResolver;
$this->roleHierarchy = $roleHierarchy;
}

/**
* {@inheritdoc}
*/
public function supportsAttribute($attribute)
{
return $attribute instanceof Expression;
}

/**
* {@inheritdoc}
*/
public function supportsClass($class)
{
return true;
}

/**
* {@inheritdoc}
*/
public function vote(TokenInterface $token, $object, array $attributes)
{
if (null !== $this->roleHierarchy) {
$roles = $this->roleHierarchy->getReachableRoles($token->getRoles());
} else {
$roles = $token->getRoles();
}

$variables = array(
'token' => $token,
'user' => $token->getUser(),
'object' => $object,
'roles' => array_map(function ($role) { return $role->getRole(); }, $roles),
'trust_resolver' => $this->trustResolver,
);

$result = VoterInterface::ACCESS_ABSTAIN;
foreach ($attributes as $attribute) {
if (!$this->supportsAttribute($attribute)) {
continue;
}

$result = VoterInterface::ACCESS_DENIED;
if ($this->expressionLanguage->evaluate($attribute, $variables)) {
return VoterInterface::ACCESS_GRANTED;
}
}

return $result;
}
}
6 changes: 3 additions & 3 deletions src/Symfony/Component/Security/Http/AccessMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ class AccessMap implements AccessMapInterface
* Constructor.
*
* @param RequestMatcherInterface $requestMatcher A RequestMatcherInterface instance
* @param array $roles An array of roles needed to access the resource
* @param array $attributes An array of attributes to pass to the access decision manager (like roles)
* @param string|null $channel The channel to enforce (http, https, or null)
*/
public function add(RequestMatcherInterface $requestMatcher, array $roles = array(), $channel = null)
public function add(RequestMatcherInterface $requestMatcher, array $attributes = array(), $channel = null)
{
$this->map[] = array($requestMatcher, $roles, $channel);
$this->map[] = array($requestMatcher, $attributes, $channel);
}

public function getPatterns(Request $request)
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Component/Security/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"doctrine/common": "~2.2",
"doctrine/dbal": "~2.2",
"psr/log": "~1.0",
"ircmaxell/password-compat": "1.0.*"
"ircmaxell/password-compat": "1.0.*",
"symfony/expression-language": "~2.4"
},
"suggest": {
"symfony/class-loader": "",
Expand Down
0