feature #21935 [FrameworkBundle][Workflow] Add a way to register a guard expression in the configuration (lyrixx)
This PR was merged into the 3.3-dev branch.
Discussion
----------
[FrameworkBundle][Workflow] Add a way to register a guard expression in the configuration
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
---
Many people already asked for this feature so ... here we go 🎉
---
Usage:
```yml
transitions:
journalist_approval:
guard: "is_fully_authenticated() and has_role('ROLE_JOURNALIST') or is_granted('POST_EDIT', subject)"
from: wait_for_journalist
to: approved_by_journalist
publish:
guard: "subject.isPublic()"
from: approved_by_journalist
to: published
```
Commits
-------
ab3b12d6dc
[FrameworkBundle][Workflow] Add a way to register a guard expression in the configuration
This commit is contained in:
commit
d1e591edd8
@ -336,6 +336,11 @@ class Configuration implements ConfigurationInterface
|
||||
->isRequired()
|
||||
->cannotBeEmpty()
|
||||
->end()
|
||||
->scalarNode('guard')
|
||||
->cannotBeEmpty()
|
||||
->info('An expression to block the transition')
|
||||
->example('is_fully_authenticated() and has_role(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'')
|
||||
->end()
|
||||
->arrayNode('from')
|
||||
->beforeNormalization()
|
||||
->ifString()
|
||||
|
@ -499,6 +499,30 @@ class FrameworkExtension extends Extension
|
||||
$listener->addArgument(new Reference('logger'));
|
||||
$container->setDefinition(sprintf('%s.listener.audit_trail', $workflowId), $listener);
|
||||
}
|
||||
|
||||
// Add Guard Listener
|
||||
$guard = new Definition(Workflow\EventListener\GuardListener::class);
|
||||
$configuration = array();
|
||||
foreach ($workflow['transitions'] as $transitionName => $config) {
|
||||
if (!isset($config['guard'])) {
|
||||
continue;
|
||||
}
|
||||
$eventName = sprintf('workflow.%s.guard.%s', $name, $transitionName);
|
||||
$guard->addTag('kernel.event_listener', array('event' => $eventName, 'method' => 'onTransition'));
|
||||
$configuration[$eventName] = $config['guard'];
|
||||
}
|
||||
if ($configuration) {
|
||||
$guard->setArguments(array(
|
||||
$configuration,
|
||||
new Reference('workflow.security.expression_language'),
|
||||
new Reference('security.token_storage'),
|
||||
new Reference('security.authorization_checker'),
|
||||
new Reference('security.authentication.trust_resolver'),
|
||||
new Reference('security.role_hierarchy'),
|
||||
));
|
||||
|
||||
$container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,5 +27,7 @@
|
||||
<argument type="service" id="workflow.registry" />
|
||||
<tag name="twig.extension" />
|
||||
</service>
|
||||
|
||||
<service id="workflow.security.expression_language" class="Symfony\Component\Workflow\EventListener\ExpressionLanguage" public="false" />
|
||||
</services>
|
||||
</container>
|
||||
|
@ -0,0 +1,33 @@
|
||||
<?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\Workflow\EventListener;
|
||||
|
||||
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage as BaseExpressionLanguage;
|
||||
|
||||
/**
|
||||
* Adds some function to the default Symfony Security ExpressionLanguage.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class ExpressionLanguage extends BaseExpressionLanguage
|
||||
{
|
||||
protected function registerFunctions()
|
||||
{
|
||||
parent::registerFunctions();
|
||||
|
||||
$this->register('is_granted', function ($attributes, $object = 'null') {
|
||||
return sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object);
|
||||
}, function (array $variables, $attributes, $object = null) {
|
||||
return $variables['auth_checker']->isGranted($attributes, $object);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?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\Workflow\EventListener;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class GuardListener
|
||||
{
|
||||
private $configuration;
|
||||
private $expressionLanguage;
|
||||
private $tokenStorage;
|
||||
private $authenticationChecker;
|
||||
private $trustResolver;
|
||||
private $roleHierarchy;
|
||||
|
||||
public function __construct($configuration, ExpressionLanguage $expressionLanguage, TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authenticationChecker, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->expressionLanguage = $expressionLanguage;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->authenticationChecker = $authenticationChecker;
|
||||
$this->trustResolver = $trustResolver;
|
||||
$this->roleHierarchy = $roleHierarchy;
|
||||
}
|
||||
|
||||
public function onTransition(GuardEvent $event, $eventName)
|
||||
{
|
||||
if (!isset($this->configuration[$eventName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->expressionLanguage->evaluate($this->configuration[$eventName], $this->getVariables($event))) {
|
||||
$event->setBlocked(true);
|
||||
}
|
||||
}
|
||||
|
||||
// code should be sync with Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter
|
||||
private function getVariables(GuardEvent $event)
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
|
||||
if (null !== $this->roleHierarchy) {
|
||||
$roles = $this->roleHierarchy->getReachableRoles($token->getRoles());
|
||||
} else {
|
||||
$roles = $token->getRoles();
|
||||
}
|
||||
|
||||
$variables = array(
|
||||
'token' => $token,
|
||||
'user' => $token->getUser(),
|
||||
'subject' => $event->getSubject(),
|
||||
'roles' => array_map(function ($role) {
|
||||
return $role->getRole();
|
||||
}, $roles),
|
||||
// needed for the is_granted expression function
|
||||
'auth_checker' => $this->authenticationChecker,
|
||||
// needed for the is_* expression function
|
||||
'trust_resolver' => $this->trustResolver,
|
||||
);
|
||||
|
||||
return $variables;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Workflow\EventListener\ExpressionLanguage;
|
||||
use Symfony\Component\Workflow\EventListener\GuardListener;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class GuardListenerTest extends TestCase
|
||||
{
|
||||
private $tokenStorage;
|
||||
private $listener;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$configuration = array(
|
||||
'event_name_a' => 'true',
|
||||
'event_name_b' => 'false',
|
||||
);
|
||||
|
||||
$expressionLanguage = new ExpressionLanguage();
|
||||
$this->tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();
|
||||
$authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
|
||||
$trustResolver = $this->getMockBuilder(AuthenticationTrustResolverInterface::class)->getMock();
|
||||
|
||||
$this->listener = new GuardListener($configuration, $expressionLanguage, $this->tokenStorage, $authenticationChecker, $trustResolver);
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
$this->listener = null;
|
||||
}
|
||||
|
||||
public function testWithNotSupportedEvent()
|
||||
{
|
||||
$event = $this->createEvent();
|
||||
$this->configureTokenStorage(false);
|
||||
|
||||
$this->listener->onTransition($event, 'not supported');
|
||||
|
||||
$this->assertFalse($event->isBlocked());
|
||||
}
|
||||
|
||||
public function testWithSupportedEventAndReject()
|
||||
{
|
||||
$event = $this->createEvent();
|
||||
$this->configureTokenStorage(true);
|
||||
|
||||
$this->listener->onTransition($event, 'event_name_a');
|
||||
|
||||
$this->assertFalse($event->isBlocked());
|
||||
}
|
||||
|
||||
public function testWithSupportedEventAndAccept()
|
||||
{
|
||||
$event = $this->createEvent();
|
||||
$this->configureTokenStorage(true);
|
||||
|
||||
$this->listener->onTransition($event, 'event_name_b');
|
||||
|
||||
$this->assertTrue($event->isBlocked());
|
||||
}
|
||||
|
||||
private function createEvent()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = new Marking();
|
||||
$transition = new Transition('name', 'from', 'to');
|
||||
|
||||
return new GuardEvent($subject, $subject->marking, $transition);
|
||||
}
|
||||
|
||||
private function configureTokenStorage($hasUser)
|
||||
{
|
||||
if (!$hasUser) {
|
||||
$this->tokenStorage
|
||||
->expects($this->never())
|
||||
->method('getToken')
|
||||
;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->getMockBuilder(TokenInterface::class)->getMock();
|
||||
$token
|
||||
->expects($this->once())
|
||||
->method('getRoles')
|
||||
->willReturn(array(new Role('ROLE_ADMIN')))
|
||||
;
|
||||
|
||||
$this->tokenStorage
|
||||
->expects($this->once())
|
||||
->method('getToken')
|
||||
->willReturn($token)
|
||||
;
|
||||
}
|
||||
}
|
@ -25,7 +25,9 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "~1.0",
|
||||
"symfony/event-dispatcher": "~2.1|~3.0"
|
||||
"symfony/event-dispatcher": "~2.1|~3.0",
|
||||
"symfony/expression-language": "~2.8|~3.0",
|
||||
"symfony/security-core": "~2.8|~3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Workflow\\": "" }
|
||||
|
Reference in New Issue
Block a user