Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 09efa63

Browse filesBrowse files
committed
[FrameworkBundle][Workflow] Add a way to register a guard expression in the configuration
1 parent 323529c commit 09efa63
Copy full SHA for 09efa63

File tree

6 files changed

+251
-0
lines changed
Filter options

6 files changed

+251
-0
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
333333
->isRequired()
334334
->cannotBeEmpty()
335335
->end()
336+
->scalarNode('guard')
337+
->cannotBeEmpty()
338+
->info('An expression to block the transition')
339+
->example('is_fully_authenticated() and has_role(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'')
340+
->end()
336341
->arrayNode('from')
337342
->beforeNormalization()
338343
->ifString()

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+24Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,30 @@ private function registerWorkflowConfiguration(array $workflows, ContainerBuilde
487487
} elseif (isset($workflow['support_strategy'])) {
488488
$registryDefinition->addMethodCall('add', array(new Reference($workflowId), new Reference($workflow['support_strategy'])));
489489
}
490+
491+
// Add Guard Listener
492+
$guard = new Definition(Workflow\EventListener\GuardListener::class);
493+
$configuration = [];
494+
foreach ($workflow['transitions'] as $transitionName => $config) {
495+
if (!isset($config['guard'])) {
496+
continue;
497+
}
498+
$eventName = sprintf('workflow.%s.guard.%s', $name, $transitionName);
499+
$guard->addTag('kernel.event_listener', array('event' => $eventName, 'method' => 'onTransition'));
500+
$configuration[$eventName] = $config['guard'];
501+
}
502+
if ($configuration) {
503+
$guard->setArguments(array(
504+
$configuration,
505+
new Reference('workflow.security.expression_language'),
506+
new Reference('security.token_storage'),
507+
new Reference('security.authorization_checker'),
508+
new Reference('security.authentication.trust_resolver'),
509+
new Reference('security.role_hierarchy'),
510+
));
511+
512+
$container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard);
513+
}
490514
}
491515
}
492516

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@
2727
<argument type="service" id="workflow.registry" />
2828
<tag name="twig.extension" />
2929
</service>
30+
31+
<service id="workflow.security.expression_language" class="Symfony\Component\Workflow\EventListener\ExpressionLanguage" public="false" />
3032
</services>
3133
</container>
+33Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Workflow\EventListener;
13+
14+
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage as BaseExpressionLanguage;
15+
16+
/**
17+
* Adds some function to the default Symfony Security ExpressionLanguage.
18+
*
19+
* @author Fabien Potencier <fabien@symfony.com>
20+
*/
21+
class ExpressionLanguage extends BaseExpressionLanguage
22+
{
23+
protected function registerFunctions()
24+
{
25+
parent::registerFunctions();
26+
27+
$this->register('is_granted', function ($attributes, $object = 'null') {
28+
return sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object);
29+
}, function (array $variables, $attributes, $object = null) {
30+
return $variables['auth_checker']->isGranted($attributes, $object);
31+
});
32+
}
33+
}
+79Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Workflow\EventListener;
13+
14+
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
15+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
16+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
17+
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
18+
use Symfony\Component\Workflow\Event\GuardEvent;
19+
20+
/**
21+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
22+
*/
23+
class GuardListener
24+
{
25+
private $configuration;
26+
private $expressionLanguage;
27+
private $tokenStorage;
28+
private $authenticationChecker;
29+
private $trustResolver;
30+
private $roleHierarchy;
31+
32+
public function __construct($configuration, ExpressionLanguage $expressionLanguage, TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authenticationChecker, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null)
33+
{
34+
$this->configuration = $configuration;
35+
$this->expressionLanguage = $expressionLanguage;
36+
$this->tokenStorage = $tokenStorage;
37+
$this->authenticationChecker = $authenticationChecker;
38+
$this->trustResolver = $trustResolver;
39+
$this->roleHierarchy = $roleHierarchy;
40+
}
41+
42+
public function onTransition(GuardEvent $event, $eventName)
43+
{
44+
if (!isset($this->configuration[$eventName])) {
45+
return;
46+
}
47+
48+
if (!$this->expressionLanguage->evaluate($this->configuration[$eventName], $this->getVariables($event))) {
49+
$event->setBlocked(true);
50+
}
51+
}
52+
53+
// code should be sync with Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter
54+
private function getVariables(GuardEvent $event)
55+
{
56+
$token = $this->tokenStorage->getToken();
57+
58+
if (null !== $this->roleHierarchy) {
59+
$roles = $this->roleHierarchy->getReachableRoles($token->getRoles());
60+
} else {
61+
$roles = $token->getRoles();
62+
}
63+
64+
$variables = array(
65+
'token' => $token,
66+
'user' => $token->getUser(),
67+
'subject' => $event->getSubject(),
68+
'roles' => array_map(function ($role) {
69+
return $role->getRole();
70+
}, $roles),
71+
// needed for the is_granted expression function
72+
'auth_checker' => $this->authenticationChecker,
73+
// needed for the is_* expression function
74+
'trust_resolver' => $this->trustResolver,
75+
);
76+
77+
return $variables;
78+
}
79+
}
+108Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Symfony\Component\Workflow\Tests\EventListener;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
7+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
8+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
9+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
10+
use Symfony\Component\Security\Core\Role\Role;
11+
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
12+
use Symfony\Component\Workflow\EventListener\ExpressionLanguage;
13+
use Symfony\Component\Workflow\EventListener\GuardListener;
14+
use Symfony\Component\Workflow\Event\Event;
15+
use Symfony\Component\Workflow\Event\GuardEvent;
16+
use Symfony\Component\Workflow\Marking;
17+
use Symfony\Component\Workflow\Transition;
18+
19+
class GuardListenerTest extends TestCase
20+
{
21+
private $tokenStorage;
22+
private $listener;
23+
24+
protected function setUp()
25+
{
26+
$configuration = [
27+
'event_name_a' => 'true',
28+
'event_name_b' => 'false',
29+
];
30+
31+
$expressionLanguage = new ExpressionLanguage();
32+
$this->tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();
33+
$authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
34+
$trustResolver = $this->getMockBuilder(AuthenticationTrustResolverInterface::class)->getMock();
35+
36+
$this->listener = new GuardListener($configuration, $expressionLanguage, $this->tokenStorage, $authenticationChecker, $trustResolver);
37+
}
38+
39+
protected function tearDown()
40+
{
41+
$this->listener = null;
42+
}
43+
44+
public function testWithNotSupportedEvent()
45+
{
46+
$event = $this->createEvent();
47+
$this->configureTokenStorage(false);
48+
49+
$this->listener->onTransition($event, 'not supported');
50+
51+
$this->assertFalse($event->isBlocked());
52+
}
53+
54+
public function testWithSupportedEventAndReject()
55+
{
56+
$event = $this->createEvent();
57+
$this->configureTokenStorage(true);
58+
59+
$this->listener->onTransition($event, 'event_name_a');
60+
61+
$this->assertFalse($event->isBlocked());
62+
}
63+
64+
public function testWithSupportedEventAndAccept()
65+
{
66+
$event = $this->createEvent();
67+
$this->configureTokenStorage(true);
68+
69+
$this->listener->onTransition($event, 'event_name_b');
70+
71+
$this->assertTrue($event->isBlocked());
72+
}
73+
74+
private function createEvent()
75+
{
76+
$subject = new \stdClass();
77+
$subject->marking = new Marking();
78+
$transition = new Transition('name', 'from', 'to');
79+
80+
return new GuardEvent($subject, $subject->marking, $transition);
81+
82+
}
83+
84+
private function configureTokenStorage($hasUser)
85+
{
86+
if (!$hasUser) {
87+
$this->tokenStorage
88+
->expects($this->never())
89+
->method('getToken')
90+
;
91+
92+
return;
93+
}
94+
95+
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
96+
$token
97+
->expects($this->once())
98+
->method('getRoles')
99+
->willReturn([new Role('ROLE_ADMIN')])
100+
;
101+
102+
$this->tokenStorage
103+
->expects($this->once())
104+
->method('getToken')
105+
->willReturn($token)
106+
;
107+
}
108+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.