diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index ac4f18da0280c..ae2250e3d44c0 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -336,6 +336,11 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
->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()
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 51fc87faa664d..8204a818c0601 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -498,6 +498,30 @@ private function registerWorkflowConfiguration(array $workflows, ContainerBuilde
$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);
+ }
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml
index 76592087a2260..7bfd2f7b00bab 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml
@@ -27,5 +27,7 @@
+
+
diff --git a/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php b/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php
new file mode 100644
index 0000000000000..46d5d7520c0fb
--- /dev/null
+++ b/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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
+ */
+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);
+ });
+ }
+}
diff --git a/src/Symfony/Component/Workflow/EventListener/GuardListener.php b/src/Symfony/Component/Workflow/EventListener/GuardListener.php
new file mode 100644
index 0000000000000..20ba04c007fc2
--- /dev/null
+++ b/src/Symfony/Component/Workflow/EventListener/GuardListener.php
@@ -0,0 +1,79 @@
+
+ *
+ * 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
+ */
+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;
+ }
+}
diff --git a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php
new file mode 100644
index 0000000000000..b46ee9092c573
--- /dev/null
+++ b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php
@@ -0,0 +1,105 @@
+ '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)
+ ;
+ }
+}
diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json
index f0447d7a9b211..66f8f65dc041f 100644
--- a/src/Symfony/Component/Workflow/composer.json
+++ b/src/Symfony/Component/Workflow/composer.json
@@ -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\\": "" }