added support for expression in control access rules
This commit is contained in:
parent
2777ac7854
commit
38b7fde8ed
|
@ -169,6 +169,7 @@ class MainConfiguration implements ConfigurationInterface
|
|||
->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()
|
||||
|
|
|
@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\Parameter;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
|
||||
/**
|
||||
* SecurityExtension.
|
||||
|
@ -33,6 +34,7 @@ use Symfony\Component\Config\FileLocator;
|
|||
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();
|
||||
|
@ -188,8 +190,13 @@ class SecurityExtension extends Extension
|
|||
$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']));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -596,6 +603,21 @@ class SecurityExtension extends Extension
|
|||
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));
|
||||
|
|
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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%">
|
||||
|
|
|
@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Parameter;
|
|||
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
|
||||
{
|
||||
|
@ -133,7 +134,7 @@ abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase
|
|||
|
||||
$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]));
|
||||
|
@ -141,19 +142,23 @@ abstract class CompleteConfigurationTest extends \PHPUnit_Framework_TestCase
|
|||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ $container->loadFromExtension('security', array(
|
|||
'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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -69,3 +69,4 @@ security:
|
|||
-
|
||||
path: /blog/.*
|
||||
role: IS_AUTHENTICATED_ANONYMOUSLY
|
||||
- { path: /blog/524, role: IS_AUTHENTICATED_ANONYMOUSLY, allow_if: "token.getUsername() =~ '/^admin/'" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -91,6 +91,28 @@ class SecurityRoutingIntegrationTest extends WebTestCase
|
|||
$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);
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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\\": "" }
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
Reference in New Issue