mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Merge branch '307-permission-apply-all-transitions' into 'signature-app-master'
Create a permission to apply all transitions Closes #307 See merge request Chill-Projet/chill-bundles!729
This commit is contained in:
		| @@ -0,0 +1,88 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Security\Authorization; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; | ||||
| use Chill\MainBundle\Security\ProvideRoleHierarchyInterface; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||||
| use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; | ||||
| use Symfony\Component\Security\Core\Authorization\Voter\Voter; | ||||
|  | ||||
| /** | ||||
|  * A voter class that determines if a user has permission to apply all transitions | ||||
|  * in a workflow based on their roles and the centers they have access to. | ||||
|  */ | ||||
| final class EntityWorkflowTransitionVoter extends Voter implements ProvideRoleHierarchyInterface | ||||
| { | ||||
|     final public const APPLY_ALL_TRANSITIONS = 'CHILL_MAIN_WORKFLOW_APPLY_ALL_TRANSITION'; | ||||
|  | ||||
|     public function __construct( | ||||
|         private readonly EntityWorkflowManager $workflowManager, | ||||
|         private readonly AuthorizationHelperForCurrentUserInterface $authorizationHelper, | ||||
|         private readonly CenterResolverManagerInterface $centerResolverManager, | ||||
|         private readonly AccessDecisionManagerInterface $accessDecisionManager, | ||||
|     ) {} | ||||
|  | ||||
|     public function getRoles(): array | ||||
|     { | ||||
|         return [self::APPLY_ALL_TRANSITIONS]; | ||||
|     } | ||||
|  | ||||
|     public function getRolesWithoutScope(): array | ||||
|     { | ||||
|         return [self::APPLY_ALL_TRANSITIONS]; | ||||
|     } | ||||
|  | ||||
|     public function getRolesWithHierarchy(): array | ||||
|     { | ||||
|         return [ | ||||
|             'workflow.Permissions' => [ | ||||
|                 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; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,144 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types=1); | ||||
|  | ||||
| /* | ||||
|  * Chill is a software for social workers | ||||
|  * | ||||
|  * For the full copyright and license information, please view | ||||
|  * the LICENSE file that was distributed with this source code. | ||||
|  */ | ||||
|  | ||||
| namespace Chill\MainBundle\Tests\Authorization; | ||||
|  | ||||
| use Chill\MainBundle\Entity\Center; | ||||
| use Chill\MainBundle\Entity\User; | ||||
| use Chill\MainBundle\Entity\Workflow\EntityWorkflow; | ||||
| use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface; | ||||
| use Chill\MainBundle\Security\Authorization\EntityWorkflowTransitionVoter; | ||||
| use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter; | ||||
| use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; | ||||
| use Chill\MainBundle\Workflow\EntityWorkflowManager; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use Prophecy\PhpUnit\ProphecyTrait; | ||||
| use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; | ||||
| use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; | ||||
| use Symfony\Component\Security\Core\Authorization\Voter\Voter; | ||||
|  | ||||
| /** | ||||
|  * @internal | ||||
|  * | ||||
|  * @coversNothing | ||||
|  */ | ||||
| class EntityWorkflowTransitionVoterTest extends TestCase | ||||
| { | ||||
|     use ProphecyTrait; | ||||
|  | ||||
|     public function testVoteOnAttributeHappyScenario(): void | ||||
|     { | ||||
|         $entityWorkflow = new EntityWorkflow(); | ||||
|         $object = new \stdClass(); | ||||
|         $center = new Center(); | ||||
|         $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([$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'])); | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user