diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSend.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSend.php index 528107561..5030d8b87 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSend.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowSend.php @@ -56,7 +56,7 @@ class EntityWorkflowSend implements TrackCreationInterface /** * @var Collection */ - #[ORM\OneToMany(targetEntity: EntityWorkflowSendView::class, mappedBy: 'send')] + #[ORM\OneToMany(mappedBy: 'send', targetEntity: EntityWorkflowSendView::class, cascade: ['remove'])] private Collection $views; public function __construct( diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php index cd60d78d1..40a7bc1a2 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -66,7 +66,7 @@ class EntityWorkflowStep /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)] + #[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $signatures; #[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')] @@ -115,7 +115,7 @@ class EntityWorkflowStep /** * @var Collection */ - #[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist'], orphanRemoval: true)] + #[ORM\OneToMany(mappedBy: 'entityWorkflowStep', targetEntity: EntityWorkflowSend::class, cascade: ['persist', 'remove'], orphanRemoval: true)] private Collection $sends; public function __construct() diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php index ee45d1e56..9f3ea6845 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php @@ -17,6 +17,7 @@ use Chill\MainBundle\Workflow\Helper\DuplicateEntityWorkflowFinder; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Workflow\Registry; class EntityWorkflowVoter extends Voter { @@ -32,6 +33,7 @@ class EntityWorkflowVoter extends Voter private readonly EntityWorkflowManager $manager, private readonly Security $security, private readonly DuplicateEntityWorkflowFinder $duplicateEntityWorkflowFinder, + private readonly Registry $registry, ) {} protected function supports($attribute, $subject) @@ -83,7 +85,25 @@ class EntityWorkflowVoter extends Voter return false; case self::DELETE: - return 'initial' === $subject->getStep(); + if ('initial' === $subject->getStep()) { + return true; + } + + if (!$subject->isFinal()) { + return false; + } + + // the entity workflow is finalized. We check if the final is not positive. If yes, we can + // delete the entity + $workflow = $this->registry->get($subject, $subject->getWorkflowName()); + foreach ($workflow->getMarking($subject)->getPlaces() as $place => $key) { + $metadata = $workflow->getMetadataStore()->getPlaceMetadata($place); + if (false === ($metadata['isFinalPositive'] ?? true)) { + return true; + } + } + + return false; case self::SHOW_ENTITY_LINK: if ('initial' === $subject->getStep()) { diff --git a/src/Bundle/ChillMainBundle/Tests/Security/Authorization/EntityWorkflowVoterTest.php b/src/Bundle/ChillMainBundle/Tests/Security/Authorization/EntityWorkflowVoterTest.php new file mode 100644 index 000000000..2cdf67df6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Security/Authorization/EntityWorkflowVoterTest.php @@ -0,0 +1,194 @@ +buildEntityWorkflow(); + + $voter = $this->buildVoter(); + $token = $this->buildToken(); + + $actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]); + + self::assertEquals(Voter::ACCESS_GRANTED, $actual); + } + + public function testVoteDeleteEntityWorkflowForSomeOherPlace(): void + { + $entityWorkflow = $this->buildEntityWorkflow(); + $registry = $this->buildRegistry(); + $workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers = [new User()]; + $workflow->apply($entityWorkflow, 'move_to_in_between', ['context' => $dto, 'transition' => 'move_to_in_between', 'transitionAt' => new \DateTimeImmutable()]); + + assert('in_between' === $entityWorkflow->getStep(), 'we ensure that the workflow is well transitionned'); + + $voter = $this->buildVoter(); + $token = $this->buildToken(); + + $actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]); + + self::assertEquals(Voter::ACCESS_DENIED, $actual); + } + + public function testVoteDeleteEntityWorkflowForFinalPositive(): void + { + $entityWorkflow = $this->buildEntityWorkflow(); + $registry = $this->buildRegistry(); + $workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers = [new User()]; + $workflow->apply($entityWorkflow, 'move_to_final_positive', ['context' => $dto, 'transition' => 'move_to_final_positive', 'transitionAt' => new \DateTimeImmutable()]); + + assert('final_positive' === $entityWorkflow->getStep(), 'we ensure that the workflow is well transitionned'); + + $voter = $this->buildVoter(); + $token = $this->buildToken(); + + $actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]); + + self::assertEquals(Voter::ACCESS_DENIED, $actual); + } + + public function testVoteDeleteEntityWorkflowForFinalNegative(): void + { + $entityWorkflow = $this->buildEntityWorkflow(); + $registry = $this->buildRegistry(); + $workflow = $registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers = [new User()]; + $workflow->apply($entityWorkflow, 'move_to_final_negative', ['context' => $dto, 'transition' => 'move_to_final_negative', 'transitionAt' => new \DateTimeImmutable()]); + + $voter = $this->buildVoter(); + $token = $this->buildToken(); + + $actual = $voter->vote($token, $entityWorkflow, [EntityWorkflowVoter::DELETE]); + + self::assertEquals(Voter::ACCESS_GRANTED, $actual); + } + + private function buildToken(): TokenInterface + { + return new UsernamePasswordToken($user = new User(), 'main', $user->getRoles()); + } + + private function buildVoter(): EntityWorkflowVoter + { + $manager = $this->createMock(EntityWorkflowManager::class); + $security = $this->createMock(Security::class); + $duplicateEntityWorkflowFind = $this->createMock(DuplicateEntityWorkflowFinder::class); + + return new EntityWorkflowVoter( + $manager, + $security, + $duplicateEntityWorkflowFind, + $this->buildRegistry() + ); + } + + private function buildEntityWorkflow(): EntityWorkflow + { + $entityWorkflow = new EntityWorkflow(); + $entityWorkflow->setWorkflowName('dummy'); + $entityWorkflow->setRelatedEntityId(1)->setRelatedEntityClass(\stdClass::class); + + return $entityWorkflow; + } + + private function buildRegistry(): Registry + { + $security = $this->createMock(Security::class); + $security->method('getUser')->willReturn(new User()); + + $builder = new DefinitionBuilder(); + $builder->addPlaces(['initial', 'in_between', 'final_positive', 'final_negative']); + + $metadataStore = new InMemoryMetadataStore( + placesMetadata: [ + 'final_positive' => [ + 'isFinal' => true, + 'isFinalPositive' => true, + ], + 'final_negative' => [ + 'isFinal' => true, + 'isFinalPositive' => false, + ], + ] + ); + + $builder->setMetadataStore($metadataStore); + + $transitions = [ + new Transition('move_to_in_between', 'initial', 'in_between'), + new Transition('move_to_final_positive', 'initial', 'final_positive'), + new Transition('move_to_final_negative', 'initial', 'final_negative'), + ]; + + foreach ($transitions as $transition) { + $builder->addTransition($transition); + } + + $definition = $builder->build(); + + $eventSubscriber = new EventDispatcher(); + $eventSubscriber->addSubscriber( + new EntityWorkflowTransitionEventSubscriber( + new NullLogger(), + $security + ) + ); + + $registry = new Registry(); + $workflow = new Workflow($definition, new EntityWorkflowMarkingStore(), $eventSubscriber, name: 'dummy'); + + $registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface { + public function supports(WorkflowInterface $workflow, $subject): bool + { + return true; + } + }); + + return $registry; + } +}