diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowTransitionVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowTransitionVoter.php new file mode 100644 index 000000000..811c45f64 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowTransitionVoter.php @@ -0,0 +1,88 @@ + [ + self::APPLY_ALL_TRANSITIONS, + ], + ]; + } + + protected function supports(string $attribute, $subject): bool + { + return self::APPLY_ALL_TRANSITIONS === $attribute && $subject instanceof EntityWorkflowStep; + } + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + /** @var EntityWorkflowStep $subject */ + $entityWorkflow = $subject->getEntityWorkflow(); + + if (!$this->accessDecisionManager->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow)) { + return false; + } + + $handler = $this->workflowManager->getHandler($entityWorkflow); + $entity = $handler->getRelatedEntity($entityWorkflow); + + if (null === $entity) { + return false; + } + + $centers = $this->centerResolverManager->resolveCenters($entity); + $reachableCenters = $this->authorizationHelper->getReachableCenters(self::APPLY_ALL_TRANSITIONS); + + foreach ($centers as $center) { + if (in_array($center, $reachableCenters, true)) { + return true; + } + } + + return false; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Authorization/EntityWorkflowTransitionVoterTest.php b/src/Bundle/ChillMainBundle/Tests/Authorization/EntityWorkflowTransitionVoterTest.php new file mode 100644 index 000000000..035d4cf58 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Authorization/EntityWorkflowTransitionVoterTest.php @@ -0,0 +1,144 @@ +prophesize(EntityWorkflowHandlerInterface::class); + $handler->getRelatedEntity($entityWorkflow)->willReturn($object); + + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->getHandler($entityWorkflow)->willReturn($handler); + + $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); + $centerResolver->resolveCenters($object)->willReturn([$center, new Center()]); + + $autorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $autorizationHelper->getReachableCenters('CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION') + ->willReturn([$center, new Center()]); + + $token = new UsernamePasswordToken($user, 'default', $user->getRoles()); + + $accessDecision = $this->prophesize(AccessDecisionManagerInterface::class); + $accessDecision->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow) + ->willReturn(true)->shouldBeCalled(); + + $voter = new EntityWorkflowTransitionVoter( + $entityWorkflowManager->reveal(), + $autorizationHelper->reveal(), + $centerResolver->reveal(), + $accessDecision->reveal(), + ); + + self::assertEquals(Voter::ACCESS_GRANTED, $voter->vote($token, $entityWorkflow->getCurrentStep(), ['CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION'])); + } + + public function testVoteOnAttributeCenterNotReachable(): void + { + $entityWorkflow = new EntityWorkflow(); + $object = new \stdClass(); + $user = new User(); + + $handler = $this->prophesize(EntityWorkflowHandlerInterface::class); + $handler->getRelatedEntity($entityWorkflow)->willReturn($object); + + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->getHandler($entityWorkflow)->willReturn($handler); + + $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); + $centerResolver->resolveCenters($object)->willReturn([new Center()]); + + $autorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $autorizationHelper->getReachableCenters('CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION') + ->willReturn([new Center()]); + + $token = new UsernamePasswordToken($user, 'default', $user->getRoles()); + + $accessDecision = $this->prophesize(AccessDecisionManagerInterface::class); + $accessDecision->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow) + ->willReturn(true)->shouldBeCalled(); + + $voter = new EntityWorkflowTransitionVoter( + $entityWorkflowManager->reveal(), + $autorizationHelper->reveal(), + $centerResolver->reveal(), + $accessDecision->reveal(), + ); + + self::assertEquals(Voter::ACCESS_DENIED, $voter->vote($token, $entityWorkflow->getCurrentStep(), ['CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION'])); + } + + public function testVoteNotOnSupportedAttribute(): void + { + $entityWorkflow = new EntityWorkflow(); + $object = new \stdClass(); + $user = new User(); + + $handler = $this->prophesize(EntityWorkflowHandlerInterface::class); + $handler->getRelatedEntity($entityWorkflow)->willReturn($object); + + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->getHandler($entityWorkflow)->willReturn($handler); + + $centerResolver = $this->prophesize(CenterResolverManagerInterface::class); + $centerResolver->resolveCenters($object)->willReturn([new Center()]); + + $autorizationHelper = $this->prophesize(AuthorizationHelperForCurrentUserInterface::class); + $autorizationHelper->getReachableCenters('CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION') + ->willReturn([new Center()]); + + $token = new UsernamePasswordToken($user, 'default', $user->getRoles()); + + $accessDecision = $this->prophesize(AccessDecisionManagerInterface::class); + $accessDecision->decide($token, [EntityWorkflowVoter::SEE], $entityWorkflow) + ->willReturn(true); + + $voter = new EntityWorkflowTransitionVoter( + $entityWorkflowManager->reveal(), + $autorizationHelper->reveal(), + $centerResolver->reveal(), + $accessDecision->reveal(), + ); + + self::assertEquals(Voter::ACCESS_ABSTAIN, $voter->vote($token, new \stdClass(), ['CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION'])); + self::assertEquals(Voter::ACCESS_ABSTAIN, $voter->vote($token, $entityWorkflow->getCurrentStep(), ['SOMETHING_ELSE'])); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php index bccd6cf9e..dd3b12e86 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/EventSubscriber/EntityWorkflowGuardTransitionTest.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Tests\Workflow\EventSubscriber; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore; use Chill\MainBundle\Workflow\EventSubscriber\EntityWorkflowGuardTransition; @@ -84,12 +85,14 @@ class EntityWorkflowGuardTransitionTest extends TestCase /** * @dataProvider provideBlockingTransition */ - public function testTransitionGuardBlocked(EntityWorkflow $entityWorkflow, string $transition, ?User $user, string $uuid): void + public function testTransitionGuardBlocked(EntityWorkflow $entityWorkflow, string $transition, ?User $user, bool $isGrantedAllTransition, string $uuid): void { $userRender = $this->prophesize(UserRender::class); $userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string'); $security = $this->prophesize(Security::class); $security->getUser()->willReturn($user); + $security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep()) + ->willReturn($isGrantedAllTransition); $transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal()); $registry = self::buildRegistry($transitionGuard); @@ -115,12 +118,14 @@ class EntityWorkflowGuardTransitionTest extends TestCase /** * @dataProvider provideValidTransition */ - public function testTransitionGuardValid(EntityWorkflow $entityWorkflow, string $transition, ?User $user, string $newStep): void + public function testTransitionGuardValid(EntityWorkflow $entityWorkflow, string $transition, ?User $user, bool $isGrantedAllTransition, string $newStep): void { $userRender = $this->prophesize(UserRender::class); $userRender->renderString(Argument::type(User::class), [])->willReturn('user-as-string'); $security = $this->prophesize(Security::class); $security->getUser()->willReturn($user); + $security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep()) + ->willReturn($isGrantedAllTransition); $transitionGuard = new EntityWorkflowGuardTransition($userRender->reveal(), $security->reveal()); $registry = self::buildRegistry($transitionGuard); @@ -135,20 +140,25 @@ class EntityWorkflowGuardTransitionTest extends TestCase public static function provideBlockingTransition(): iterable { - yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc']; - yield [self::buildEntityWorkflow([]), 'transition1', null, 'd9e39a18-704c-11ef-b235-8fe0619caee7']; - yield [self::buildEntityWorkflow([new User()]), 'transition1', null, 'd9e39a18-704c-11ef-b235-8fe0619caee7']; - yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; + yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), false, 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc']; + yield [self::buildEntityWorkflow([]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7']; + yield [self::buildEntityWorkflow([new User()]), 'transition1', null, false, 'd9e39a18-704c-11ef-b235-8fe0619caee7']; + yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, false, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; + yield [self::buildEntityWorkflow([$user = new User()]), 'transition3', $user, true, '5b6b95e0-704d-11ef-a5a9-4b6fc11a8eeb']; } public static function provideValidTransition(): iterable { - yield [self::buildEntityWorkflow([$u = new User()]), 'transition1', $u, 'step1']; - yield [self::buildEntityWorkflow([$u = new User()]), 'transition2', $u, 'step2']; - yield [self::buildEntityWorkflow([new User()]), 'transition2', null, 'step2']; - yield [self::buildEntityWorkflow([]), 'transition2', null, 'step2']; - yield [self::buildEntityWorkflow([new User()]), 'transition3', null, 'step3']; - yield [self::buildEntityWorkflow([]), 'transition3', null, 'step3']; + yield [self::buildEntityWorkflow([$u = new User()]), 'transition1', $u, false, 'step1']; + yield [self::buildEntityWorkflow([$u = new User()]), 'transition2', $u, false, 'step2']; + yield [self::buildEntityWorkflow([new User()]), 'transition2', null, false, 'step2']; + yield [self::buildEntityWorkflow([]), 'transition2', null, false, 'step2']; + yield [self::buildEntityWorkflow([new User()]), 'transition3', null, false, 'step3']; + yield [self::buildEntityWorkflow([]), 'transition3', null, false, 'step3']; + + // transition allowed thanks to permission "apply all transitions" + yield [self::buildEntityWorkflow([new User()]), 'transition1', new User(), true, 'step1']; + yield [self::buildEntityWorkflow([new User()]), 'transition2', new User(), true, 'step2']; } public static function buildEntityWorkflow(array $futureDestUsers): EntityWorkflow diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php index 25ed21e98..1c9d26fc7 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowGuardTransition.php @@ -13,12 +13,24 @@ namespace Chill\MainBundle\Workflow\EventSubscriber; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter; use Chill\MainBundle\Templating\Entity\UserRender; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Security; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\TransitionBlocker; +/** + * Prevent apply a transition on an entity workflow. + * + * This apply logic and rules to decide if a transition can be applyed. + * + * Those rules are: + * + * - if the transition is system-only or is allowed for user; + * - if the user is present in the dest users for a workflow; + * - or if the user have permission to apply all the transitions + */ class EntityWorkflowGuardTransition implements EventSubscriberInterface { public function __construct( @@ -85,11 +97,17 @@ class EntityWorkflowGuardTransition implements EventSubscriberInterface ); } - if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($user)) { + if ( + !$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($user) + ) { if ($event->getMarking()->has('initial')) { return; } + if ($this->security->isGranted(EntityWorkflowTransitionVoter::APPLY_ALL_TRANSITIONS, $entityWorkflow->getCurrentStep())) { + return; + } + $event->addTransitionBlocker(new TransitionBlocker( 'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%', 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc', diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 003dcbee5..495b9a3bd 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -532,6 +532,8 @@ workflow: On hold: En attente Automated transition: Transition automatique waiting_for_signature: En attente de signature + Permissions: Workflows (suivi de décision) + signature_zone: title: Signatures électroniques @@ -551,6 +553,7 @@ workflow: Subscribe final: Recevoir une notification à l'étape finale Subscribe all steps: Recevoir une notification à chaque étape +CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION: Appliquer les transitions sur tous les workflows notification: Notification: Notification