Skip to content

Commit ca308f0

Browse files
authored
Merge pull request #9 from lolautruche/simplifiedVoter
Simplified voter
2 parents 6b0f0f8 + c271343 commit ca308f0

9 files changed

Lines changed: 319 additions & 7 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ Adds extra features to eZ Publish 5.4 / eZ Platform.
5151
5252
Lets you define a theme fallback order for your templates, similar to
5353
[legacy design fallback system](https://doc.ez.no/eZ-Publish/Technical-manual/5.x/Concepts-and-basics/Designs/Design-combinations).
54+
55+
* **[Simplified authorization checks](Resources/doc/simplified_auth_checks.md)**
56+
57+
Simplifies calls to `$this->isGranted()` from inside controllers and `is_granted()` from within templates when checking
58+
against eZ inner permission system (module/function/valueObject).
5459

5560
## Requirements
5661
EzCoreExtraBundle currently works with **eZ Publish 5.4/2014.11** (and *should work* with Netgen variant)

Resources/config/services.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,11 @@ services:
4242
- [setContextAwareGlobals, ["$twig_globals;ez_core_extra$"]]
4343
tags:
4444
- { name: twig.extension }
45+
46+
ez_core_extra.security.simplified_core_voter:
47+
class: Lolautruche\EzCoreExtraBundle\Security\Voter\SimplifiedCoreVoter
48+
arguments:
49+
- "@ezpublish.security.voter.core"
50+
- "@ezpublish.security.voter.value_object"
51+
tags:
52+
- { name: "security.voter" }
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Simplified authorization checks
2+
3+
This feature simplifies the way you check authorization with eZ inner ACL system, using
4+
`module/function` and optionnaly a value object (e.g. a content object).
5+
6+
Without eZCoreExtraBundle, when one want to check if a user has access to a module/function like
7+
`content/read`, they have to implement the following in their controller:
8+
9+
```php
10+
namespace Acme\Controller;
11+
12+
use eZ\Bundle\EzPublishCoreBundle\Controller;
13+
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute as AuthorizationAttribute;
14+
15+
class MyController extends Controller
16+
{
17+
public function fooAction()
18+
{
19+
// ...
20+
$accessGranted = $this->isGranted(new AuthorizationAttribute('content', 'read'));
21+
22+
// Or with an actual content
23+
$accessGranted = $this->isGranted(
24+
new AuthorizationAttribute('content', 'read', ['valueObject' => $myContent])
25+
);
26+
}
27+
}
28+
```
29+
30+
While this is efficient, it is a bit cumbersome to write.
31+
Furthermore, it's not possible to do such security checks within Twig templates, as it's not possible
32+
to instantiate new objects from there.
33+
34+
EzCoreExtraBundle adds a new simplified syntax for such checks, usable in templates.
35+
36+
## Usage
37+
In order to check access for a `module`/`function` pair, instead of instantiating an `AuthorizationAttribute`
38+
object, just use the following syntax:
39+
40+
```
41+
ez:<module>:<function>
42+
```
43+
44+
Taking the example from the introduction, it will be:
45+
46+
```php
47+
namespace Acme\Controller;
48+
49+
use eZ\Bundle\EzPublishCoreBundle\Controller;
50+
51+
class MyController extends Controller
52+
{
53+
public function fooAction()
54+
{
55+
// ...
56+
$accessGranted = $this->isGranted('ez:content:read');
57+
58+
// Or with an actual content
59+
$accessGranted = $this->isGranted('ez:content:read', $myContent);
60+
}
61+
}
62+
```
63+
64+
In a template, the syntax will be:
65+
66+
```jinja
67+
{% set accessGranted = is_granted('ez:content:read') %}
68+
69+
{# Or with an actual content #}
70+
{% set accessGranted = is_granted('ez:content:read', my_content) %}
71+
```
72+
73+
Et voilà :-)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the EzCoreExtraBundle package.
5+
*
6+
* (c) Jérôme Vieilledent <jerome@vieilledent.fr>
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 Lolautruche\EzCoreExtraBundle\Security\Voter;
13+
14+
use eZ\Publish\API\Repository\Values\ValueObject;
15+
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute as AuthorizationAttribute;
16+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
18+
19+
class SimplifiedCoreVoter implements VoterInterface
20+
{
21+
const EZ_ROLE_PREFIX = 'ez:';
22+
23+
/**
24+
* @var VoterInterface
25+
*/
26+
private $coreVoter;
27+
28+
/**
29+
* @var VoterInterface
30+
*/
31+
private $valueObjectVoter;
32+
33+
public function __construct(VoterInterface $coreVoter, VoterInterface $valueObjectVoter)
34+
{
35+
$this->coreVoter = $coreVoter;
36+
$this->valueObjectVoter = $valueObjectVoter;
37+
}
38+
39+
public function supportsAttribute($attribute)
40+
{
41+
return is_string($attribute) && stripos($attribute, static::EZ_ROLE_PREFIX) === 0;
42+
}
43+
44+
public function supportsClass($class)
45+
{
46+
return true;
47+
}
48+
49+
public function vote(TokenInterface $token, $object, array $attributes)
50+
{
51+
foreach ($attributes as $attribute) {
52+
if (!$this->supportsAttribute($attribute)) {
53+
continue;
54+
}
55+
56+
$attribute = substr($attribute, strlen(static::EZ_ROLE_PREFIX));
57+
list($module, $function) = explode(':', $attribute);
58+
$attributeObject = new AuthorizationAttribute($module, $function);
59+
if ($object instanceof ValueObject) {
60+
$attributeObject->limitations = ['valueObject' => $object];
61+
return $this->valueObjectVoter->vote($token, $object, [$attributeObject]);
62+
} else {
63+
return $this->coreVoter->vote($token, $object, [$attributeObject]);
64+
}
65+
}
66+
67+
return static::ACCESS_ABSTAIN;
68+
}
69+
}

Tests/Asset/AssetPathResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class AssetPathResolverTest extends PHPUnit_Framework_TestCase
1919
{
2020
public function testResolveAssetPathFail()
2121
{
22-
$logger = $this->getMock('\Psr\Log\LoggerInterface');
22+
$logger = $this->createMock('\Psr\Log\LoggerInterface');
2323
$logger
2424
->expects($this->once())
2525
->method('warning');

Tests/Asset/ThemePackageTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ protected function setUp()
3030
{
3131
parent::setUp();
3232

33-
$this->assetPathResolver = $this->getMock('\Lolautruche\EzCoreExtraBundle\Asset\AssetPathResolverInterface');
34-
$this->innerPackage = $this->getMock('\Symfony\Component\Asset\PackageInterface');
33+
$this->assetPathResolver = $this->createMock('\Lolautruche\EzCoreExtraBundle\Asset\AssetPathResolverInterface');
34+
$this->innerPackage = $this->createMock('\Symfony\Component\Asset\PackageInterface');
3535
}
3636

3737
public function testGetUrl()

Tests/EventListener/ViewTemplateListenerTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ViewTemplateListenerTest extends PHPUnit_Framework_TestCase
3434
protected function setUp()
3535
{
3636
parent::setUp();
37-
$this->configResolver = $this->getMock('\eZ\Publish\Core\MVC\ConfigResolverInterface');
37+
$this->configResolver = $this->createMock('\eZ\Publish\Core\MVC\ConfigResolverInterface');
3838
$this->dynamicSettingParser = new DynamicSettingParser();
3939
}
4040

@@ -55,9 +55,9 @@ private function generateView()
5555
{
5656
// \eZ\Publish\Core\MVC\Symfony\View\View is only defined in kernel >=6.0
5757
if (interface_exists('\eZ\Publish\Core\MVC\Symfony\View\View')) {
58-
$view = $this->getMock('\eZ\Publish\Core\MVC\Symfony\View\View');
58+
$view = $this->createMock('\eZ\Publish\Core\MVC\Symfony\View\View');
5959
} else {
60-
$view = $this->getMock('\eZ\Publish\Core\MVC\Symfony\View\ContentViewInterface');
60+
$view = $this->createMock('\eZ\Publish\Core\MVC\Symfony\View\ContentViewInterface');
6161
}
6262

6363
return $view;
@@ -161,7 +161,7 @@ public function testOnPreViewContentParameterProvider()
161161
$event = new PreContentViewEvent($view);
162162

163163
$providerAlias = 'some_provider';
164-
$provider = $this->getMock('\Lolautruche\EzCoreExtraBundle\Templating\ViewParameterProviderInterface');
164+
$provider = $this->createMock('\Lolautruche\EzCoreExtraBundle\Templating\ViewParameterProviderInterface');
165165
$listener = new ViewTemplateListener($this->configResolver, $this->dynamicSettingParser);
166166
$listener->addParameterProvider($provider, $providerAlias);
167167

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the EzCoreExtraBundle package.
5+
*
6+
* (c) Jérôme Vieilledent <jerome@vieilledent.fr>
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 Lolautruche\EzCoreExtraBundle\Tests\Security\Voter;
13+
14+
use eZ\Publish\API\Repository\Values\ValueObject;
15+
use eZ\Publish\Core\MVC\Symfony\Security\Authorization\Attribute;
16+
use Lolautruche\EzCoreExtraBundle\Security\Voter\SimplifiedCoreVoter;
17+
use PHPUnit_Framework_TestCase;
18+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
19+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
20+
21+
class SimplifiedCoreVoterTest extends PHPUnit_Framework_TestCase
22+
{
23+
/**
24+
* @var \PHPUnit_Framework_MockObject_MockObject|VoterInterface
25+
*/
26+
private $coreVoter;
27+
28+
/**
29+
* @var \PHPUnit_Framework_MockObject_MockObject|VoterInterface
30+
*/
31+
private $valueObjectVoter;
32+
33+
/**
34+
* @var SimplifiedCoreVoter
35+
*/
36+
private $voter;
37+
38+
protected function setUp()
39+
{
40+
parent::setUp();
41+
$this->coreVoter = $this->createMock(VoterInterface::class);
42+
$this->valueObjectVoter = $this->createMock(VoterInterface::class);
43+
$this->voter = new SimplifiedCoreVoter($this->coreVoter, $this->valueObjectVoter);
44+
}
45+
46+
/**
47+
* @dataProvider supportsAttributeProvider
48+
*/
49+
public function testSupportsAttribute($attribute, $expectedResult)
50+
{
51+
$this->assertSame($expectedResult, $this->voter->supportsAttribute($attribute));
52+
}
53+
54+
public function supportsAttributeProvider()
55+
{
56+
return [
57+
['foo', false],
58+
['bar', false],
59+
[SimplifiedCoreVoter::EZ_ROLE_PREFIX.'foo:bar', true],
60+
[new \stdClass(), false],
61+
[[], false]
62+
];
63+
}
64+
65+
public function testVoteNotSupportedAttribute()
66+
{
67+
$this->assertSame(
68+
VoterInterface::ACCESS_ABSTAIN,
69+
$this->voter->vote($this->createMock(TokenInterface::class), null, ['foo'])
70+
);
71+
}
72+
73+
public function testVoteGrantedNoValueObject()
74+
{
75+
$token = $this->createMock(TokenInterface::class);
76+
$object = null;
77+
$attribute = 'ez:foo:bar';
78+
$attributeObject = new Attribute('foo', 'bar');
79+
$this->coreVoter
80+
->expects($this->once())
81+
->method('vote')
82+
->with($token, $object, [$attributeObject])
83+
->willReturn(VoterInterface::ACCESS_GRANTED);
84+
$this->valueObjectVoter
85+
->expects($this->never())
86+
->method('vote');
87+
88+
$this->assertSame(
89+
VoterInterface::ACCESS_GRANTED,
90+
$this->voter->vote($token, $object, [$attribute])
91+
);
92+
}
93+
94+
public function testVoteDeniedNoValueObject()
95+
{
96+
$token = $this->createMock(TokenInterface::class);
97+
$object = null;
98+
$attribute = 'ez:foo:bar';
99+
$attributeObject = new Attribute('foo', 'bar');
100+
$this->coreVoter
101+
->expects($this->once())
102+
->method('vote')
103+
->with($token, $object, [$attributeObject])
104+
->willReturn(VoterInterface::ACCESS_DENIED);
105+
$this->valueObjectVoter
106+
->expects($this->never())
107+
->method('vote');
108+
109+
$this->assertSame(
110+
VoterInterface::ACCESS_DENIED,
111+
$this->voter->vote($token, $object, [$attribute])
112+
);
113+
}
114+
115+
public function testVoteGrantedWithValueObject()
116+
{
117+
$token = $this->createMock(TokenInterface::class);
118+
$object = $this->createMock(ValueObject::class);
119+
$attribute = 'ez:foo:bar';
120+
$attributeObject = new Attribute('foo', 'bar', ['valueObject' => $object]);
121+
$this->valueObjectVoter
122+
->expects($this->once())
123+
->method('vote')
124+
->with($token, $object, [$attributeObject])
125+
->willReturn(VoterInterface::ACCESS_GRANTED);
126+
$this->coreVoter
127+
->expects($this->never())
128+
->method('vote');
129+
130+
$this->assertSame(
131+
VoterInterface::ACCESS_GRANTED,
132+
$this->voter->vote($token, $object, [$attribute])
133+
);
134+
}
135+
136+
public function testVoteDeniedWithValueObject()
137+
{
138+
$token = $this->createMock(TokenInterface::class);
139+
$object = $this->createMock(ValueObject::class);
140+
$attribute = 'ez:foo:bar';
141+
$attributeObject = new Attribute('foo', 'bar', ['valueObject' => $object]);
142+
$this->valueObjectVoter
143+
->expects($this->once())
144+
->method('vote')
145+
->with($token, $object, [$attributeObject])
146+
->willReturn(VoterInterface::ACCESS_DENIED);
147+
$this->coreVoter
148+
->expects($this->never())
149+
->method('vote');
150+
151+
$this->assertSame(
152+
VoterInterface::ACCESS_DENIED,
153+
$this->voter->vote($token, $object, [$attribute])
154+
);
155+
}
156+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"psr-4": {"Lolautruche\\EzCoreExtraBundle\\": ""}
1616
},
1717
"require-dev": {
18+
"phpunit/phpunit": "^5.4",
1819
"mikey179/vfsStream": "^1.6.3"
1920
}
2021
}

0 commit comments

Comments
 (0)