Skip to content
This repository has been archived by the owner on Sep 16, 2021. It is now read-only.

Commit

Permalink
Merge pull request #47 from symfony-cmf/issue-45/default-voter
Browse files Browse the repository at this point in the history
Added security configuration
  • Loading branch information
dbu authored Feb 1, 2017
2 parents 67e6d04 + 2dd330c commit 548e312
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 9 deletions.
12 changes: 12 additions & 0 deletions src/DependencyInjection/CmfResourceRestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('resource-rest.xml');

$this->configurePayloadAliasRegistry($container, $config['payload_alias_map']);
$this->configureSecurityVoter($loader, $container, $config['security']);
}

private function configureSecurityVoter(XmlFileLoader $loader, ContainerBuilder $container, array $config)
{
if ([] === $config['access_control']) {
return;
}

$container->setParameter('cmf_resource_rest.security.access_map', $config['access_control']);

$loader->load('security.xml');
}

public function getNamespace()
Expand Down
36 changes: 35 additions & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Cmf\Bundle\ResourceRestBundle\DependencyInjection;

use Symfony\Cmf\Bundle\ResourceRestBundle\Controller\ResourceController;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

Expand All @@ -29,6 +30,39 @@ public function getConfigTreeBuilder()
->children()
->integerNode('max_depth')->defaultValue(2)->end()
->booleanNode('expose_payload')->defaultFalse()->end()

->arrayNode('security')
->fixXmlConfig('rule', 'access_control')
->addDefaultsIfNotSet()
->children()
->arrayNode('access_control')
->defaultValue([])
->prototype('array')
->fixXmlConfig('attribute')
->children()
->scalarNode('pattern')->defaultValue('^/')->end()
->scalarNode('repository')->defaultNull()->end()
->arrayNode('attributes')
->defaultValue([ResourceController::ROLE_RESOURCE_READ, ResourceController::ROLE_RESOURCE_WRITE])
->prototype('scalar')->end()
->end()
->arrayNode('require')
->isRequired()
->requiresAtLeastOneElement()
->beforeNormalization()
->ifString()
->then(function ($v) {
return [$v];
})
->end()
->prototype('scalar')->end()
->end() // roles
->end()
->end()
->end() // access_control
->end()
->end() // security

->arrayNode('payload_alias_map')
->useAttributeAsKey('name')
->prototype('array')
Expand All @@ -37,7 +71,7 @@ public function getConfigTreeBuilder()
->scalarNode('type')->end()
->end()
->end()
->end()
->end() // payload_alias_map
->end();

return $treeBuilder;
Expand Down
13 changes: 13 additions & 0 deletions src/Resources/config/schema/resource-rest.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<xsd:complexType name="config">
<xsd:sequence>
<xsd:element name="payload-alias" type="payload_alias" minOccurs="0" />
<xsd:element name="rule" type="rule" minOccurs="0" />
</xsd:sequence>

<xsd:attribute name="max-depth" type="xsd:integer" default="2" />
Expand All @@ -19,5 +20,17 @@
<xsd:attribute name="type" type="xsd:string" />
</xsd:complexType>

<xsd:complexType name="rule">
<xsd:sequence>
<xsd:element name="require" type="xsd:string" minOccurs="0" />
<xsd:element name="attribute" type="xsd:string" minOccurs="0" />
</xsd:sequence>

<xsd:attribute name="pattern" type="xsd:string" />
<xsd:attribute name="repository" type="xsd:string" />
<xsd:attribute name="require" type="xsd:string" />
<xsd:attribute name="attribute" type="xsd:string" />
</xsd:complexType>

</xsd:schema>

15 changes: 15 additions & 0 deletions src/Resources/config/security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="cmf_resource_rest.security.resource_path_voter" class="Symfony\Cmf\Bundle\ResourceRestBundle\Security\ResourcePathVoter" public="false">
<argument type="service" id="security.access.decision_manager" />
<argument>%cmf_resource_rest.security.access_map%</argument>

<tag name="security.voter"/>
</service>
</services>
</container>
76 changes: 76 additions & 0 deletions src/Security/ResourcePathVoter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/*
* This file is part of the Symfony CMF package.
*
* (c) 2011-2017 Symfony CMF
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Cmf\Bundle\ResourceRestBundle\Security;

use Symfony\Cmf\Bundle\ResourceRestBundle\Controller\ResourceController;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

/**
* @author Wouter de Jong <[email protected]>
*/
class ResourcePathVoter extends Voter
{
private $accessDecisionManager;
private $accessMap;

public function __construct(AccessDecisionManagerInterface $accessDecisionManager, array $accessMap)
{
$this->accessDecisionManager = $accessDecisionManager;
$this->accessMap = $accessMap;
}

/**
* {@inheritdoc}
*/
protected function supports($attribute, $subject)
{
return in_array($attribute, [ResourceController::ROLE_RESOURCE_READ, ResourceController::ROLE_RESOURCE_WRITE])
&& is_array($subject) && isset($subject['repository_name']) && isset($subject['path']);
}

/**
* {@inheritdoc}
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
foreach ($this->accessMap as $rule) {
if (!$this->ruleMatches($rule, $attribute, $subject)) {
continue;
}

if ($this->accessDecisionManager->decide($token, $rule['require'])) {
return true;
}
}

return false;
}

private function ruleMatches($rule, $attribute, $subject)
{
if (!in_array($attribute, $rule['attributes'])) {
return false;
}

if (null !== $rule['repository'] && $rule['repository'] !== $subject['repository_name']) {
return false;
}

if (!preg_match('{'.$rule['pattern'].'}', $subject['path'])) {
return false;
}

return true;
}
}
5 changes: 5 additions & 0 deletions tests/Features/nesting.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Feature: Nesting resources
default:
type: doctrine/phpcr-odm
basepath: /tests/cmf/articles
cmf_resource_rest:
security:
access_control:
- { pattern: '^/', require: IS_AUTHENTICATED_ANONYMOUSLY }
"""
And there exists an "Article" document at "/cmf/articles/foo":
| title | Article 1 |
Expand Down
3 changes: 3 additions & 0 deletions tests/Features/resource_api_phpcr.feature
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Feature: PHPCR resource repository
cmf_resource_rest:
expose_payload: true
security:
access_control:
- { pattern: '^/', require: IS_AUTHENTICATED_ANONYMOUSLY }
"""


Expand Down
3 changes: 3 additions & 0 deletions tests/Features/resource_api_phpcr_odm.feature
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Feature: PHPCR-ODM resource repository
article:
repository: doctrine/phpcr-odm
type: "Symfony\\Cmf\\Bundle\\ResourceRestBundle\\Tests\\Resources\\TestBundle\\Document\\Article"
security:
access_control:
- { pattern: '^/', require: IS_AUTHENTICATED_ANONYMOUSLY }
"""


Expand Down
5 changes: 5 additions & 0 deletions tests/Features/security.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Feature: Security
security:
type: phpcr/phpcr
basepath: /tests/cmf/articles
cmf_resource_rest:
security:
access_control:
- { pattern: '^/tests/cmf/articles/private', repository: security, require: ROLE_ADMIN }
"""
And there exists an "Article" document at "/private/foo":
| title | Article 1 |
Expand Down
4 changes: 0 additions & 4 deletions tests/Resources/app/config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
*/

use Symfony\Cmf\Bundle\ResourceRestBundle\Tests\Resources\TestBundle\Description\DummyEnhancer;
use Symfony\Cmf\Bundle\ResourceRestBundle\Tests\Resources\TestBundle\Security\ResourceVoter;

$container->setParameter('cmf_testing.bundle_fqn', 'Symfony\Cmf\Bundle\ResourceRestBundle');
$loader->import(CMF_TEST_CONFIG_DIR.'/dist/parameters.yml');
Expand All @@ -20,8 +19,5 @@
$loader->import(CMF_TEST_CONFIG_DIR.'/dist/security.yml');
$loader->import(CMF_TEST_CONFIG_DIR.'/phpcr_odm.php');

$container->register('app.resource_voter', ResourceVoter::class)
->addTag('security.voter');

$container->register('app.dummy_enhancer', DummyEnhancer::class)
->addTag('cmf_resource.description.enhancer', ['alias' => 'dummy']);
7 changes: 7 additions & 0 deletions tests/Unit/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public function testConfig($source)
],
'max_depth' => 2,
'expose_payload' => false,
'security' => [
'access_control' => [
['pattern' => '^/cms/public', 'attributes' => ['CMF_RESOURCE_READ'], 'require' => ['IS_AUTHENTICATED_ANONYMOUSLY'], 'repository' => null],
['pattern' => '^/cms/members-only', 'attributes' => ['CMF_RESOURCE_READ'], 'require' => ['ROLE_USER'], 'repository' => null],
['pattern' => '^/', 'attributes' => ['CMF_RESOURCE_WRITE'], 'require' => ['ROLE_ADMIN'], 'repository' => null],
],
],
], [$source]);
}
}
23 changes: 19 additions & 4 deletions tests/Unit/DependencyInjection/fixtures/config.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cmf_resource_rest="http://cmf.symfony.com/schema/dic/cmf_resource_rest"

xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<cmf_resource_rest:config>
<cmf_resource_rest:payload_alias name="article" type="Namespace\Article" repository="doctrine_phpcr_odm" />
</cmf_resource_rest:config>

<config xmlns="http://cmf.symfony.com/schema/dic/cmf_resource_rest">

<payload-alias name="article" type="Namespace\Article" repository="doctrine_phpcr_odm" />

<security>
<rule pattern="^/cms/public" require="IS_AUTHENTICATED_ANONYMOUSLY">
<attribute>CMF_RESOURCE_READ</attribute>
</rule>

<rule pattern="^/cms/members-only" attribute="CMF_RESOURCE_READ">
<require>ROLE_USER</require>
</rule>

<rule pattern="^/" attribute="CMF_RESOURCE_WRITE" require="ROLE_ADMIN" />
</security>
</config>

</container>
6 changes: 6 additions & 0 deletions tests/Unit/DependencyInjection/fixtures/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ cmf_resource_rest:
article:
repository: doctrine_phpcr_odm
type: Namespace\Article

security:
access_control:
- { pattern: ^/cms/public, attributes: [CMF_RESOURCE_READ], require: IS_AUTHENTICATED_ANONYMOUSLY }
- { pattern: ^/cms/members-only, attributes: [CMF_RESOURCE_READ], require: ROLE_USER }
- { pattern: ^/, attributes: [CMF_RESOURCE_WRITE], require: ROLE_ADMIN }
81 changes: 81 additions & 0 deletions tests/Unit/Security/ResourcePathVoterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/*
* This file is part of the Symfony CMF package.
*
* (c) 2011-2017 Symfony CMF
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Cmf\Bundle\ResourceRestBundle\Tests\Security;

use Symfony\Cmf\Bundle\ResourceRestBundle\Security\ResourcePathVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter as V;

class ResourcePathVoterTest extends \PHPUnit_Framework_TestCase
{
private $accessDecisionManager;

protected function setUp()
{
$this->accessDecisionManager = $this->prophesize(AccessDecisionManagerInterface::class);
}

/**
* @dataProvider provideVoteData
*/
public function testVote($rules, $subject, array $attributes, $result)
{
$token = $this->prophesize(TokenInterface::class)->reveal();

$this->accessDecisionManager->decide($token, ['ROLE_USER'])->willReturn(true);
$this->accessDecisionManager->decide($token, ['ROLE_ADMIN'])->willReturn(false);

$voter = new ResourcePathVoter($this->accessDecisionManager->reveal(), $rules);

$this->assertSame($result, $voter->vote($token, $subject, $attributes));
}

public function provideVoteData()
{
$ruleSet1 = [
$this->buildRule('^/', ['ROLE_USER'], ['CMF_RESOURCE_READ']),
$this->buildRule('^/cms/private', ['ROLE_ADMIN'], ['CMF_RESOURCE_WRITE']),
];

return [
// Basic behaviour
[[$this->buildRule('^/')], $this->buildSubject('/cms/articles/foo'), ['CMF_RESOURCE_READ'], V::ACCESS_GRANTED],
[[$this->buildRule('^/')], $this->buildSubject('/cms/articles/foo'), ['CMF_RESOURCE_WRITE'], V::ACCESS_GRANTED],
[[$this->buildRule('^/', ['ROLE_ADMIN'])], $this->buildSubject('/cms/articles/foo'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED],

// Multiple rules
[$ruleSet1, $this->buildSubject('/cms/private/admin'), ['CMF_RESOURCE_READ'], V::ACCESS_GRANTED],
[$ruleSet1, $this->buildSubject('/cms/private/admin'), ['CMF_RESOURCE_WRITE'], V::ACCESS_DENIED],
[$ruleSet1, $this->buildSubject('/cms/public'), ['CMF_RESOURCE_READ', 'CMF_RESOURCE_WRITE'], V::ACCESS_GRANTED],

// Unsupported attributes or subjects
[[], $this->buildSubject('/cms/articles'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED],
[[$this->buildRule('^/')], $this->buildSubject('/cms/articles'), ['ROLE_USER'], V::ACCESS_ABSTAIN],
[[$this->buildRule('^/')], new \stdClass(), ['CMF_RESOURCE_READ'], V::ACCESS_ABSTAIN],

// Repository name matching
[[$this->buildRule('^/')], $this->buildSubject('/cms/articles', 'other_repo'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED],
[[$this->buildRule('^/', ['ROLE_USER'], ['CMF_RESOURCE_READ'], 'other_repo')], $this->buildSubject('/cms/articles'), ['CMF_RESOURCE_READ'], V::ACCESS_DENIED],
];
}

private function buildRule($pattern, $require = ['ROLE_USER'], $attributes = ['CMF_RESOURCE_READ', 'CMF_RESOURCE_WRITE'], $repository = 'default')
{
return ['pattern' => $pattern, 'attributes' => $attributes, 'require' => $require, 'repository' => $repository];
}

private function buildSubject($path, $repository = 'default')
{
return ['path' => $path, 'repository_name' => $repository];
}
}

0 comments on commit 548e312

Please sign in to comment.