diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowGuardListenerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowGuardListenerPass.php index 349d78cfc4fed..c5498adc4728d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowGuardListenerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowGuardListenerPass.php @@ -17,6 +17,7 @@ /** * @author Christian Flothmann + * @author Grégoire Pineau */ class WorkflowGuardListenerPass implements CompilerPassInterface { @@ -31,20 +32,17 @@ public function process(ContainerBuilder $container) $container->getParameterBag()->remove('workflow.has_guard_listeners'); - if (!$container->has('security.token_storage')) { - throw new LogicException('The "security.token_storage" service is needed to be able to use the workflow guard listener.'); - } - - if (!$container->has('security.authorization_checker')) { - throw new LogicException('The "security.authorization_checker" service is needed to be able to use the workflow guard listener.'); - } - - if (!$container->has('security.authentication.trust_resolver')) { - throw new LogicException('The "security.authentication.trust_resolver" service is needed to be able to use the workflow guard listener.'); - } - - if (!$container->has('security.role_hierarchy')) { - throw new LogicException('The "security.role_hierarchy" service is needed to be able to use the workflow guard listener.'); + $servicesNeeded = array( + 'security.token_storage', + 'security.authorization_checker', + 'security.authentication.trust_resolver', + 'security.role_hierarchy', + ); + + foreach ($servicesNeeded as $service) { + if (!$container->has($service)) { + throw new LogicException(sprintf('The "%s" service is needed to be able to use the workflow guard listener.', $service)); + } } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 90673719814b5..9bdbb373bb0f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -85,6 +85,7 @@ use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\ObjectInitializerInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\WebLink\HttpHeaderSerializer; use Symfony\Component\Workflow; use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand; @@ -752,6 +753,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ new Reference('security.authorization_checker'), new Reference('security.authentication.trust_resolver'), new Reference('security.role_hierarchy'), + new Reference('validator', ContainerInterface::NULL_ON_INVALID_REFERENCE), )); $container->setDefinition(sprintf('%s.listener.guard', $workflowId), $guard); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php index fead9e28f26d0..44c87b188fa17 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php @@ -14,12 +14,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowGuardListenerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Exception\LogicException; 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\RoleHierarchy; -use Symfony\Component\Workflow\EventListener\GuardListener; +use Symfony\Component\Validator\Validator\ValidatorInterface; class WorkflowGuardListenerPassTest extends TestCase { @@ -29,53 +28,37 @@ class WorkflowGuardListenerPassTest extends TestCase protected function setUp() { $this->container = new ContainerBuilder(); - $this->container->register('foo.listener.guard', GuardListener::class); - $this->container->register('bar.listener.guard', GuardListener::class); $this->compilerPass = new WorkflowGuardListenerPass(); } - public function testListenersAreNotRemovedIfParameterIsNotSet() + public function testNoExeptionIfParameterIsNotSet() { $this->compilerPass->process($this->container); - $this->assertTrue($this->container->hasDefinition('foo.listener.guard')); - $this->assertTrue($this->container->hasDefinition('bar.listener.guard')); - } - - public function testParameterIsRemovedWhenThePassIsProcessed() - { - $this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard')); - - try { - $this->compilerPass->process($this->container); - } catch (LogicException $e) { - // Here, we are not interested in the exception handling. This is tested further down. - } - $this->assertFalse($this->container->hasParameter('workflow.has_guard_listeners')); } - public function testListenersAreNotRemovedIfAllDependenciesArePresent() + public function testNoExeptionIfAllDependenciesArePresent() { - $this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard')); + $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); $this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class); $this->container->register('security.role_hierarchy', RoleHierarchy::class); + $this->container->register('validator', ValidatorInterface::class); $this->compilerPass->process($this->container); - $this->assertTrue($this->container->hasDefinition('foo.listener.guard')); - $this->assertTrue($this->container->hasDefinition('bar.listener.guard')); + $this->assertFalse($this->container->hasParameter('workflow.has_guard_listeners')); } /** * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException * @expectedExceptionMessage The "security.token_storage" service is needed to be able to use the workflow guard listener. */ - public function testListenersAreRemovedIfTheTokenStorageServiceIsNotPresent() + public function testExceptionIfTheTokenStorageServiceIsNotPresent() { - $this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard')); + $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); $this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class); $this->container->register('security.role_hierarchy', RoleHierarchy::class); @@ -87,9 +70,9 @@ public function testListenersAreRemovedIfTheTokenStorageServiceIsNotPresent() * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException * @expectedExceptionMessage The "security.authorization_checker" service is needed to be able to use the workflow guard listener. */ - public function testListenersAreRemovedIfTheAuthorizationCheckerServiceIsNotPresent() + public function testExceptionIfTheAuthorizationCheckerServiceIsNotPresent() { - $this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard')); + $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class); $this->container->register('security.role_hierarchy', RoleHierarchy::class); @@ -101,9 +84,9 @@ public function testListenersAreRemovedIfTheAuthorizationCheckerServiceIsNotPres * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException * @expectedExceptionMessage The "security.authentication.trust_resolver" service is needed to be able to use the workflow guard listener. */ - public function testListenersAreRemovedIfTheAuthenticationTrustResolverServiceIsNotPresent() + public function testExceptionIfTheAuthenticationTrustResolverServiceIsNotPresent() { - $this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard')); + $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); $this->container->register('security.role_hierarchy', RoleHierarchy::class); @@ -115,9 +98,9 @@ public function testListenersAreRemovedIfTheAuthenticationTrustResolverServiceIs * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException * @expectedExceptionMessage The "security.role_hierarchy" service is needed to be able to use the workflow guard listener. */ - public function testListenersAreRemovedIfTheRoleHierarchyServiceIsNotPresent() + public function testExceptionIfTheRoleHierarchyServiceIsNotPresent() { - $this->container->setParameter('workflow.has_guard_listeners', array('foo.listener.guard', 'bar.listener.guard')); + $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); $this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class); diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md index 02c04f6dc08f6..abbf3238011c1 100644 --- a/src/Symfony/Component/Workflow/CHANGELOG.md +++ b/src/Symfony/Component/Workflow/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.4.0 ----- + * Added guard `is_valid()` method support. * Added support for `Event::getWorkflowName()` for "announce" events. * Added `workflow.completed` events which are fired after a transition is completed. diff --git a/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php b/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php index 46d5d7520c0fb..e1bcc7997d317 100644 --- a/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php +++ b/src/Symfony/Component/Workflow/EventListener/ExpressionLanguage.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Workflow\EventListener; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage as BaseExpressionLanguage; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Component\Workflow\Exception\RuntimeException; /** * Adds some function to the default Symfony Security ExpressionLanguage. @@ -29,5 +31,17 @@ protected function registerFunctions() }, function (array $variables, $attributes, $object = null) { return $variables['auth_checker']->isGranted($attributes, $object); }); + + $this->register('is_valid', function ($object = 'null', $groups = 'null') { + return sprintf('0 === count($validator->validate(%s, null, %s))', $object, $groups); + }, function (array $variables, $object = null, $groups = null) { + if (!$variables['validator'] instanceof ValidatorInterface) { + throw new RuntimeException('"is_valid" cannot be used as the Validator component is not installed.'); + } + + $errors = $variables['validator']->validate($object, null, $groups); + + return 0 === count($errors); + }); } } diff --git a/src/Symfony/Component/Workflow/EventListener/GuardListener.php b/src/Symfony/Component/Workflow/EventListener/GuardListener.php index 20ba04c007fc2..1fa082ac4d1c0 100644 --- a/src/Symfony/Component/Workflow/EventListener/GuardListener.php +++ b/src/Symfony/Component/Workflow/EventListener/GuardListener.php @@ -15,6 +15,7 @@ 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\Validator\Validator\ValidatorInterface; use Symfony\Component\Workflow\Event\GuardEvent; /** @@ -28,8 +29,9 @@ class GuardListener private $authenticationChecker; private $trustResolver; private $roleHierarchy; + private $validator; - public function __construct($configuration, ExpressionLanguage $expressionLanguage, TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authenticationChecker, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null) + public function __construct($configuration, ExpressionLanguage $expressionLanguage, TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authenticationChecker, AuthenticationTrustResolverInterface $trustResolver, RoleHierarchyInterface $roleHierarchy = null, ValidatorInterface $validator = null) { $this->configuration = $configuration; $this->expressionLanguage = $expressionLanguage; @@ -37,6 +39,7 @@ public function __construct($configuration, ExpressionLanguage $expressionLangua $this->authenticationChecker = $authenticationChecker; $this->trustResolver = $trustResolver; $this->roleHierarchy = $roleHierarchy; + $this->validator = $validator; } public function onTransition(GuardEvent $event, $eventName) @@ -72,6 +75,8 @@ private function getVariables(GuardEvent $event) 'auth_checker' => $this->authenticationChecker, // needed for the is_* expression function 'trust_resolver' => $this->trustResolver, + // needed for the is_valid expression function + 'validator' => $this->validator, ); return $variables; diff --git a/src/Symfony/Component/Workflow/Exception/RuntimeException.php b/src/Symfony/Component/Workflow/Exception/RuntimeException.php new file mode 100644 index 0000000000000..7e9caf1bf5b1f --- /dev/null +++ b/src/Symfony/Component/Workflow/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Exception; + +/** + * Base RuntimeException for the Workflow component. + * + * @author Alain Flaus + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php index b46ee9092c573..f532639ae09c5 100644 --- a/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php +++ b/src/Symfony/Component/Workflow/Tests/EventListener/GuardListenerTest.php @@ -8,6 +8,7 @@ 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\Validator\Validator\ValidatorInterface; use Symfony\Component\Workflow\EventListener\ExpressionLanguage; use Symfony\Component\Workflow\EventListener\GuardListener; use Symfony\Component\Workflow\Event\GuardEvent; @@ -16,59 +17,85 @@ class GuardListenerTest extends TestCase { - private $tokenStorage; + private $authenticationChecker; + private $validator; private $listener; protected function setUp() { $configuration = array( - 'event_name_a' => 'true', - 'event_name_b' => 'false', + 'test_is_granted' => 'is_granted("something")', + 'test_is_valid' => 'is_valid(subject)', ); - $expressionLanguage = new ExpressionLanguage(); - $this->tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); - $authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); + $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $token->expects($this->any())->method('getRoles')->willReturn(array(new Role('ROLE_USER'))); + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage->expects($this->any())->method('getToken')->willReturn($token); + $this->authenticationChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock(); $trustResolver = $this->getMockBuilder(AuthenticationTrustResolverInterface::class)->getMock(); - - $this->listener = new GuardListener($configuration, $expressionLanguage, $this->tokenStorage, $authenticationChecker, $trustResolver); + $this->validator = $this->getMockBuilder(ValidatorInterface::class)->getMock(); + $this->listener = new GuardListener($configuration, $expressionLanguage, $tokenStorage, $this->authenticationChecker, $trustResolver, null, $this->validator); } protected function tearDown() { + $this->authenticationChecker = null; + $this->validator = null; $this->listener = null; } public function testWithNotSupportedEvent() { $event = $this->createEvent(); - $this->configureTokenStorage(false); + $this->configureAuthenticationChecker(false); + $this->configureValidator(false); $this->listener->onTransition($event, 'not supported'); $this->assertFalse($event->isBlocked()); } - public function testWithSupportedEventAndReject() + public function testWithSecuritySupportedEventAndReject() { $event = $this->createEvent(); - $this->configureTokenStorage(true); + $this->configureAuthenticationChecker(true, false); - $this->listener->onTransition($event, 'event_name_a'); + $this->listener->onTransition($event, 'test_is_granted'); + + $this->assertTrue($event->isBlocked()); + } + + public function testWithSecuritySupportedEventAndAccept() + { + $event = $this->createEvent(); + $this->configureAuthenticationChecker(true, true); + + $this->listener->onTransition($event, 'test_is_granted'); $this->assertFalse($event->isBlocked()); } - public function testWithSupportedEventAndAccept() + public function testWithValidatorSupportedEventAndReject() { $event = $this->createEvent(); - $this->configureTokenStorage(true); + $this->configureValidator(true, false); - $this->listener->onTransition($event, 'event_name_b'); + $this->listener->onTransition($event, 'test_is_valid'); $this->assertTrue($event->isBlocked()); } + public function testWithValidatorSupportedEventAndAccept() + { + $event = $this->createEvent(); + $this->configureValidator(true, true); + + $this->listener->onTransition($event, 'test_is_valid'); + + $this->assertFalse($event->isBlocked()); + } + private function createEvent() { $subject = new \stdClass(); @@ -78,28 +105,39 @@ private function createEvent() return new GuardEvent($subject, $subject->marking, $transition); } - private function configureTokenStorage($hasUser) + private function configureAuthenticationChecker($isUsed, $granted = true) { - if (!$hasUser) { - $this->tokenStorage + if (!$isUsed) { + $this->authenticationChecker ->expects($this->never()) - ->method('getToken') + ->method('isGranted') ; return; } - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); - $token + $this->authenticationChecker ->expects($this->once()) - ->method('getRoles') - ->willReturn(array(new Role('ROLE_ADMIN'))) + ->method('isGranted') + ->willReturn($granted) ; + } + + private function configureValidator($isUsed, $valid = true) + { + if (!$isUsed) { + $this->validator + ->expects($this->never()) + ->method('validate') + ; + + return; + } - $this->tokenStorage + $this->validator ->expects($this->once()) - ->method('getToken') - ->willReturn($token) + ->method('validate') + ->willReturn($valid ? array() : array('a violation')) ; } } diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json index 9a472dec60e34..1ff206983b822 100644 --- a/src/Symfony/Component/Workflow/composer.json +++ b/src/Symfony/Component/Workflow/composer.json @@ -28,7 +28,8 @@ "symfony/dependency-injection": "~2.8|~3.0|~4.0", "symfony/event-dispatcher": "~2.1|~3.0|~4.0", "symfony/expression-language": "~2.8|~3.0|~4.0", - "symfony/security-core": "~2.8|~3.0|~4.0" + "symfony/security-core": "~2.8|~3.0|~4.0", + "symfony/validator": "~2.8|~3.4|~4.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Workflow\\": "" }