diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index 78def7318..6a2a27140 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -16,7 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; class ActivityStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly ActivityRepository $repository, Security $security, - WorkflowStoredObjectPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelper $workflowDocumentService, ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php index c6c564be1..b443779bb 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php @@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface; use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter; +use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -75,6 +76,7 @@ class SignatureRequestController // options for user render 'absence' => false, 'main_scope' => false, + UserRender::SPLIT_LINE_BEFORE_CHARACTER => 30, // options for person render 'addAge' => false, ]), diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 9d77211c2..161360c52 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -15,7 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; @@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function __construct( private readonly Security $security, - private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null, + private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, ) {} public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool @@ -46,24 +46,27 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool { - // Retrieve the related accompanying course document + // Retrieve the related entity $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); - // Determine the attribute to pass to AccompanyingCourseDocumentVoter + // Determine the attribute to pass to the voter for argument $voterAttribute = $this->attributeToRole($attribute); - if (false === $this->security->isGranted($voterAttribute, $entity)) { - return false; + $regularPermission = $this->security->isGranted($voterAttribute, $entity); + + if (!$this->canBeAssociatedWithWorkflow()) { + return $regularPermission; } - if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) { - if (null === $this->workflowDocumentService) { - throw new \LogicException('Provide a workflow document service'); - } + $workflowPermission = match ($attribute) { + StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entity), + StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entity), + }; - return $this->workflowDocumentService->notBlockedByWorkflow($entity); - } - - return true; + return match ($workflowPermission) { + WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true, + WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false, + WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission, + }; } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index 94ff9149d..e1a9add7d 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb public function __construct( private readonly AccompanyingCourseDocumentRepository $repository, Security $security, - WorkflowStoredObjectPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelper $workflowDocumentService, ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index 4c8ae693e..16833c535 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\PersonDocumentRepository; use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly PersonDocumentRepository $repository, Security $security, - WorkflowStoredObjectPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelper $workflowDocumentService, ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php b/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php deleted file mode 100644 index 95cc77194..000000000 --- a/src/Bundle/ChillDocStoreBundle/Service/WorkflowStoredObjectPermissionHelper.php +++ /dev/null @@ -1,83 +0,0 @@ -entityWorkflowManager->findByRelatedEntity($entity); - $currentUser = $this->security->getUser(); - - $usersInvolved = []; - $entityWorkflowsNotFinalizedPositive = []; - foreach ($entityWorkflows as $entityWorkflow) { - // as soon as there is one signatured applyied, we are not able to - // edit the document any more - foreach ($entityWorkflow->getSteps() as $step) { - foreach ($step->getSignatures() as $signature) { - if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { - return false; - } - } - } - - if ($entityWorkflow->isFinal()) { - $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); - $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); - foreach ($marking->getPlaces() as $place => $active) { - $metadata = $workflow->getMetadataStore()->getPlaceMetadata($place); - if ($metadata['isFinalPositive'] ?? true) { - return false; - } - } - } else { - $entityWorkflowsNotFinalizedPositive[] = $entityWorkflow; - foreach ($entityWorkflow->getSteps() as $step) { - foreach ($step->getAllDestUser()->toArray() as $user) { - $usersInvolved[] = $user; - } - } - } - } - - // if there isn't any user, but a workflow, blocked - if ([] !== $entityWorkflowsNotFinalizedPositive) { - if ([] === $usersInvolved) { - return false; - } - - return in_array($currentUser, $usersInvolved, true); - } - - return true; - } -} diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index a045a1fd6..fc6099557 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -15,10 +15,11 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Security; /** @@ -28,26 +29,21 @@ use Symfony\Component\Security\Core\Security; */ class AbstractStoredObjectVoterTest extends TestCase { - private AssociatedEntityToStoredObjectInterface $repository; - private Security $security; - private WorkflowStoredObjectPermissionHelper $workflowDocumentService; + use ProphecyTrait; - protected function setUp(): void - { - $this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class); - $this->security = $this->createMock(Security::class); - $this->workflowDocumentService = $this->createMock(WorkflowStoredObjectPermissionHelper::class); - } - - private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter - { + private function buildStoredObjectVoter( + bool $canBeAssociatedWithWorkflow, + AssociatedEntityToStoredObjectInterface $repository, + Security $security, + ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + ): AbstractStoredObjectVoter { // Anonymous class extending the abstract class return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter { public function __construct( private readonly bool $canBeAssociatedWithWorkflow, private readonly AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null, + ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, ) { parent::__construct($security, $workflowDocumentService); } @@ -74,95 +70,89 @@ class AbstractStoredObjectVoterTest extends TestCase }; } - private function setupMockObjects(): array - { - $user = new User(); - $token = $this->createMock(TokenInterface::class); - $subject = new StoredObject(); - $entity = new \stdClass(); - - return [$user, $token, $subject, $entity]; - } - - private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void - { - // Set up token to return user - $token->method('getUser')->willReturn($user); - - // Mock the return of an AccompanyingCourseDocument by the repository - $this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity); - - // Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument - $this->security->method('isGranted')->willReturn($isGrantedForEntity); - - // Mock case where user is blocked or not by workflow - $this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed); - } - public function testSupportsOnAttribute(): void { - [$user, $token, $subject, $entity] = $this->setupMockObjects(); + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null); - // Setup mocks for voteOnAttribute method - $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); - $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService); + self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); - self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, $subject)); + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null); + + self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); + + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null); + + self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); } - public function testVoteOnAttributeAllowedAndWorkflowAllowed(): void - { - [$user, $token, $subject, $entity] = $this->setupMockObjects(); + /** + * @dataProvider dataProviderVoteOnAttribute + */ + public function testVoteOnAttribute( + StoredObjectRoleEnum $attribute, + bool $expected, + bool $canBeAssociatedWithWorkflow, + bool $isGrantedRegularPermission, + ?string $isGrantedWorkflowPermissionRead, + ?string $isGrantedWorkflowPermissionWrite, + string $message, + ): void { + $storedObject = new StoredObject(); + $dummyRepository = new DummyRepository($related = new \stdClass()); + $token = new UsernamePasswordToken(new User(), 'dummy'); - // Setup mocks for voteOnAttribute method - $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); - $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService); + $security = $this->prophesize(Security::class); + $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); - // The voteOnAttribute method should return True when workflow is allowed - self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); + $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); + if (null !== $isGrantedWorkflowPermissionRead) { + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) + ->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled(); + } else { + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)->shouldNotBeCalled(); + } + + if (null !== $isGrantedWorkflowPermissionWrite) { + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related) + ->willReturn($isGrantedWorkflowPermissionWrite)->shouldBeCalled(); + } else { + $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)->shouldNotBeCalled(); + } + + $voter = $this->buildStoredObjectVoter($canBeAssociatedWithWorkflow, $dummyRepository, $security->reveal(), $workflowRelatedEntityPermissionHelper->reveal()); + self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); } - public function testVoteOnAttributeNotAllowed(): void + public static function dataProviderVoteOnAttribute(): iterable { - [$user, $token, $subject, $entity] = $this->setupMockObjects(); + // not associated on a workflow + yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper']; + yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper']; - // Setup mocks for voteOnAttribute method where isGranted() returns false - $this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true); - $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService); + // associated on a workflow, read operation + yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, read by regular, force grant by workflow, ask for read, should be denied']; + yield [StoredObjectRoleEnum::SEE, true, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::ABSTAIN, null, 'associated on a workflow, denied read by regular, abstain by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, false, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied']; - // The voteOnAttribute method should return True when workflow is allowed - self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); - } - - public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void - { - [$user, $token, $subject, $entity] = $this->setupMockObjects(); - - // Setup mocks for voteOnAttribute method - $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false); - $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService); - - // Test voteOnAttribute method - $attribute = StoredObjectRoleEnum::EDIT; - $result = $voter->voteOnAttribute($attribute, $subject, $token); - - // Assert that access is denied when workflow is not allowed - $this->assertFalse($result); - } - - public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): void - { - [$user, $token, $subject, $entity] = $this->setupMockObjects(); - - // Setup mocks for voteOnAttribute method - $this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false); - $voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService); - - // Test voteOnAttribute method - $attribute = StoredObjectRoleEnum::SEE; - $result = $voter->voteOnAttribute($attribute, $subject, $token); - - // Assert that access is denied when workflow is not allowed - $this->assertTrue($result); + // association on a workflow, write operation + yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, write by regular, force grant by workflow, ask for write, should be denied']; + yield [StoredObjectRoleEnum::EDIT, true, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::ABSTAIN, 'associated on a workflow, denied write by regular, abstain by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, false, null, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied']; + } +} + +class DummyRepository implements AssociatedEntityToStoredObjectInterface +{ + public function __construct(private readonly ?object $relatedEntity) {} + + public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object + { + return $this->relatedEntity; } } diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Service/WorkflowStoredObjectPermissionHelperTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Service/WorkflowStoredObjectPermissionHelperTest.php deleted file mode 100644 index 9cfd55c57..000000000 --- a/src/Bundle/ChillDocStoreBundle/Tests/Service/WorkflowStoredObjectPermissionHelperTest.php +++ /dev/null @@ -1,182 +0,0 @@ -setWorkflowName('dummy'); - $object = new \stdClass(); - $helper = $this->buildHelper($object, $entityWorkflow, $user); - - self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message); - } - - public function testNoWorkflow(): void - { - $object = new \stdClass(); - $helper = $this->buildHelper($object, null, $user = new User()); - self::assertTrue($helper->notBlockedByWorkflow($object), "the user is not blocked by the user, as there aren't any user inside"); - } - - private function buildHelper(object $relatedEntity, ?EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper - { - $security = $this->prophesize(Security::class); - $security->getUser()->willReturn($user); - - $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); - if (null !== $entityWorkflow) { - $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]); - } else { - $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([]); - } - - return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry()); - } - - public static function provideDataNotBlockByWorkflow(): iterable - { - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable()); - - yield [$entityWorkflow, new User(), false, 'blocked because the user is not present as a dest user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); - - yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); - - yield [$entityWorkflow, $user, true, 'allowed because the user is present as a **previous** dest user']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user); - $entityWorkflow->getCurrentStep()->setIsFinal(true); - - yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user); - $entityWorkflow->getCurrentStep()->setIsFinal(true); - - yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); - $step = $entityWorkflow->getCurrentStep(); - new EntityWorkflowStepSignature($step, new Person()); - - yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); - $step = $entityWorkflow->getCurrentStep(); - $signature = new EntityWorkflowStepSignature($step, new Person()); - $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED); - - yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user); - $step = $entityWorkflow->getCurrentStep(); - $signature = new EntityWorkflowStepSignature($step, new Person()); - $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED); - - yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative']; - - } - - private static function buildRegistry(): Registry - { - $builder = new DefinitionBuilder(); - $builder - ->setInitialPlaces(['initial']) - ->addPlaces(['initial', 'test', 'final_positive', 'final_negative']) - ->setMetadataStore( - new InMemoryMetadataStore( - placesMetadata: [ - 'final_positive' => [ - 'isFinal' => true, - 'isFinalPositive' => true, - ], - 'final_negative' => [ - 'isFinal' => true, - 'isFinalPositive' => false, - ], - ] - ) - ); - - $workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy'); - $registry = new Registry(); - $registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface { - public function supports(WorkflowInterface $workflow, object $subject): bool - { - return true; - } - }); - - return $registry; - } -} diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 9a23d6d85..7e61db7d6 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; use Chill\EventBundle\Security\EventVoter; @@ -25,7 +25,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly EventRepository $repository, Security $security, - WorkflowStoredObjectPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelper $workflowDocumentService, ) { parent::__construct($security, $workflowDocumentService); } diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php index 053e8d611..50fe26a3f 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php @@ -13,8 +13,6 @@ namespace Chill\MainBundle\Templating\Entity; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; -use DateTime; -use DateTimeImmutable; use Symfony\Component\Clock\ClockInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Error\LoaderError; @@ -26,11 +24,17 @@ use Twig\Error\SyntaxError; */ class UserRender implements ChillEntityRenderInterface { + public const SPLIT_LINE_BEFORE_CHARACTER = 'split_lines_before_characters'; final public const DEFAULT_OPTIONS = [ 'main_scope' => true, 'user_job' => true, 'absence' => true, 'at_date' => null, // instanceof DateTimeInterface + /* + * when set, the jobs and service will be splitted in multiple lines. The line will be splitted + * before the given character. Only for renderString, renderBox is not concerned. + */ + self::SPLIT_LINE_BEFORE_CHARACTER => null, ]; public function __construct( @@ -65,8 +69,6 @@ class UserRender implements ChillEntityRenderInterface { $opts = \array_merge(self::DEFAULT_OPTIONS, $options); - // $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date']; - if (null === $opts['at_date']) { $opts['at_date'] = $this->clock->now(); } elseif ($opts['at_date'] instanceof \DateTime) { @@ -89,6 +91,28 @@ class UserRender implements ChillEntityRenderInterface $str .= ' ('.$this->translator->trans('absence.Absent').')'; } + if (null !== $opts[self::SPLIT_LINE_BEFORE_CHARACTER]) { + if (!is_int($opts[self::SPLIT_LINE_BEFORE_CHARACTER])) { + throw new \InvalidArgumentException('Only integer for option split_lines_before_characters is allowed'); + } + + $characterPerLine = $opts[self::SPLIT_LINE_BEFORE_CHARACTER]; + $exploded = explode(' ', $str); + $charOnLine = 0; + $str = ''; + foreach ($exploded as $word) { + if ($charOnLine + strlen($word) > $characterPerLine) { + $str .= "\n"; + $charOnLine = 0; + } + if ($charOnLine > 0) { + $str .= ' '; + } + $str .= $word; + $charOnLine += strlen($word); + } + } + return $str; } diff --git a/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php b/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php index 2b0e07732..b22580569 100644 --- a/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Templating/Entity/UserRenderTest.php @@ -35,7 +35,6 @@ class UserRenderTest extends TestCase public function testRenderUserWithJobAndScopeAtCertainDate(): void { // Create a user with a certain user job - $user = new User(); $userJobA = new UserJob(); $scopeA = new Scope(); @@ -106,4 +105,52 @@ class UserRenderTest extends TestCase $expectedStringC = 'BOB ISLA (directrice) (service B)'; $this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate)); } + + public function testRenderStringWithSplitLines(): void + { + + // Create a user with a certain user job + $user = new User(); + $userJobA = new UserJob(); + $scopeA = new Scope(); + + $userJobA->setLabel(['fr' => 'assistant social en maison de service accompagné']) + ->setActive(true); + $scopeA->setName(['fr' => 'service de l\'assistant professionnel']); + $user->setLabel('Robert Van Zorrizzeen Gorikke'); + + $userJobHistoryA = (new User\UserJobHistory()) + ->setUser($user) + ->setJob($userJobA) + ->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00')) + ->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00')); + + $userScopeHistoryA = (new User\UserScopeHistory()) + ->setUser($user) + ->setScope($scopeA) + ->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00')) + ->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00')); + + $user->getUserJobHistories()->add($userJobHistoryA); + $user->getUserScopeHistories()->add($userScopeHistoryA); + + // Create renderer + $translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']); + + $engineMock = $this->createMock(Environment::class); + $translatorMock = $this->createMock(TranslatorInterface::class); + $clock = new MockClock(new \DateTimeImmutable('2023-11-15 12:00:00')); + + $renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock); + + $actual = $renderer->renderString($user, ['split_lines_before_characters' => 30]); + self::assertEquals(<<<'STR' + Robert Van Zorrizzeen Gorikke + (assistant social en maison de + service accompagné) (service de + l'assistant professionnel) + STR, $actual); + + } } diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php new file mode 100644 index 000000000..fc06a533f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php @@ -0,0 +1,291 @@ + $entityWorkflows + */ + public function testAllowedByWorkflowRead( + array $entityWorkflows, + User $user, + string $expected, + ?\DateTimeImmutable $atDate, + string $message, + ): void { + // all entities must have this workflow name, so we are ok to set it here + foreach ($entityWorkflows as $entityWorkflow) { + $entityWorkflow->setWorkflowName('dummy'); + } + $helper = $this->buildHelper($entityWorkflows, $user, $atDate); + + self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()), $message); + } + + /** + * @dataProvider provideDataAllowedByWorkflowWriteOperation + * + * @param list $entityWorkflows + */ + public function testAllowedByWorkflowWrite( + array $entityWorkflows, + User $user, + string $expected, + ?\DateTimeImmutable $atDate, + string $message, + ): void { + // all entities must have this workflow name, so we are ok to set it here + foreach ($entityWorkflows as $entityWorkflow) { + $entityWorkflow->setWorkflowName('dummy'); + } + $helper = $this->buildHelper($entityWorkflows, $user, $atDate); + + self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()), $message); + } + + public function testNoWorkflow(): void + { + $helper = $this->buildHelper([], new User(), null); + + self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass())); + self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForReadOperation(new \stdClass())); + } + + /** + * @param list $entityWorkflows + */ + private function buildHelper(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper + { + $security = $this->prophesize(Security::class); + $security->getUser()->willReturn($user); + + $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); + $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows); + + return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); + } + + public static function provideDataAllowedByWorkflowReadOperation(): iterable + { + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), + 'abstain because the user is not present as a dest user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), + 'force grant because the user is a current user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), + 'force grant because the user was a previous user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), + 'force grant because there is a signature for person']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); + $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable('2024-01-01T12:00:00')); + + yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable('2024-01-10T12:00:00'), + 'abstain because there is a signature for person, already signed, and for a long time ago']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); + $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED) + ->setStateDate(new \DateTimeImmutable('2024-01-01T12:00:00')); + + yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + new \DateTimeImmutable('2024-01-01T12:30:00'), + 'force grant because there is a signature for person, already signed, a short time ago']; + } + + public static function provideDataAllowedByWorkflowWriteOperation(): iterable + { + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), + 'abstain because the user is not present as a dest user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), + 'force grant because the user is a current user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), + 'force grant because the user was a previous user']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User()); + $entityWorkflow->getCurrentStep()->setIsFinal(true); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), + 'force denied: user was a previous user, but it is finalized positive']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable()); + $entityWorkflow->getCurrentStep()->setIsFinal(true); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), + 'abstain: user was a previous user, it is finalized, but finalized negative']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); + $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable()); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), + 'force denied because there is a signature']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(), + 'force grant: there is a signature, but still pending']; + + $entityWorkflow = new EntityWorkflow(); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futureDestUsers[] = $user = new User(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $dto->futurePersonSignatures[] = new Person(); + $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); + $signature = $entityWorkflow->getCurrentStep()->getSignatures()->first(); + $signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable()); + $dto = new WorkflowTransitionContextDTO($entityWorkflow); + $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User()); + $entityWorkflow->getCurrentStep()->setIsFinal(true); + + yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), + 'abstain: there is a signature on a canceled workflow']; + } + + private static function buildRegistry(): Registry + { + $builder = new DefinitionBuilder(); + $builder + ->setInitialPlaces(['initial']) + ->addPlaces(['initial', 'test', 'final_positive', 'final_negative']) + ->setMetadataStore( + new InMemoryMetadataStore( + placesMetadata: [ + 'final_positive' => [ + 'isFinal' => true, + 'isFinalPositive' => true, + ], + 'final_negative' => [ + 'isFinal' => true, + 'isFinalPositive' => false, + ], + ] + ) + ); + + $workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy'); + $registry = new Registry(); + $registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface { + public function supports(WorkflowInterface $workflow, object $subject): bool + { + return true; + } + }); + + return $registry; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php new file mode 100644 index 000000000..cd890154e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php @@ -0,0 +1,160 @@ +entityWorkflowManager->findByRelatedEntity($entity); + + if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) { + return self::FORCE_GRANT; + } + + // give a view permission if there is a Person signature pending, or in the 12 hours following + // the signature last state + foreach ($entityWorkflows as $workflow) { + foreach ($workflow->getCurrentStep()->getSignatures() as $signature) { + if ('person' === $signature->getSignerKind()) { + if (EntityWorkflowSignatureStateEnum::PENDING === $signature->getState()) { + return self::FORCE_GRANT; + } + $signatureDate = $signature->getStateDate(); + $visibleUntil = $signatureDate->add(new \DateInterval('PT12H')); + + if ($visibleUntil > $this->clock->now()) { + return self::FORCE_GRANT; + } + + } + } + } + + return self::ABSTAIN; + } + + /** + * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN' + */ + public function isAllowedByWorkflowForWriteOperation(object $entity): string + { + $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity); + $runningWorkflows = []; + + // if a workflow is finalized positive, we are not allowed to edit to document any more + + foreach ($entityWorkflows as $entityWorkflow) { + if ($entityWorkflow->isFinal()) { + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); + foreach ($marking->getPlaces() as $place => $int) { + $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place); + if (true === ($placeMetadata['isFinalPositive'] ?? false)) { + // the workflow is final, and final positive, so we stop here. + return self::FORCE_DENIED; + } + } + } else { + $runningWorkflows[] = $entityWorkflow; + } + } + + // if there is a signature on a **running workflow**, no one can edit the workflow any more + foreach ($runningWorkflows as $entityWorkflow) { + foreach ($entityWorkflow->getSteps() as $step) { + foreach ($step->getSignatures() as $signature) { + if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { + return self::FORCE_DENIED; + } + } + } + } + + // allow only the users involved + if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) { + return self::FORCE_GRANT; + } + + return self::ABSTAIN; + } + + /** + * @param list $entityWorkflows + */ + private function isUserInvolvedInAWorkflow(array $entityWorkflows): bool + { + $currentUser = $this->security->getUser(); + + foreach ($entityWorkflows as $entityWorkflow) { + // so, the workflow is running... We return true if the current user is involved + foreach ($entityWorkflow->getSteps() as $step) { + if ($step->getAllDestUser()->contains($currentUser)) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php index e2ede26e5..4137cb077 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/StoredObjectVoter/AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter.php @@ -14,7 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; -use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter; @@ -25,7 +25,7 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract public function __construct( private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository, Security $security, - WorkflowStoredObjectPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelper $workflowDocumentService, ) { parent::__construct($security, $workflowDocumentService); }