diff --git a/.changes/unreleased/Fixed-20260115-172224.yaml b/.changes/unreleased/Fixed-20260115-172224.yaml new file mode 100644 index 000000000..7874e184e --- /dev/null +++ b/.changes/unreleased/Fixed-20260115-172224.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: fix issue with stored object permissions associated with workflows (as attachment, or through a related entity) +time: 2026-01-15T17:22:24.044767294+01:00 +custom: + Issue: "493" + SchemaChange: No schema change diff --git a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php index 6a2a27140..f7b37c54d 100644 --- a/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php +++ b/src/Bundle/ChillActivityBundle/Security/Authorization/ActivityStoredObjectVoter.php @@ -16,7 +16,8 @@ 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\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface; use Symfony\Component\Security\Core\Security; class ActivityStoredObjectVoter extends AbstractStoredObjectVoter @@ -24,9 +25,10 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter public function __construct( private readonly ActivityRepository $repository, Security $security, - WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php index 15e8d7aac..098d32de0 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AbstractStoredObjectVoter.php @@ -15,7 +15,10 @@ use Chill\DocStoreBundle\Entity\StoredObject; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface; -use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; +use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Security; @@ -34,7 +37,8 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function __construct( private readonly Security $security, - private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository, + private readonly WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService, ) {} public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool @@ -46,16 +50,6 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool { - // we first try to get the permission from the workflow, as attachement (this is the less intensive query) - $workflowPermissionAsAttachment = match ($attribute) { - StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($subject), - StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($subject), - }; - - if (WorkflowRelatedEntityPermissionHelper::FORCE_DENIED === $workflowPermissionAsAttachment) { - return false; - } - // Retrieve the related entity $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject); @@ -65,7 +59,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface $regularPermission = $this->security->isGranted($voterAttribute, $entity); if (!$this->canBeAssociatedWithWorkflow()) { - return $regularPermission; + return $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject); } $workflowPermission = match ($attribute) { @@ -74,9 +68,41 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface }; return match ($workflowPermission) { - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN => WorkflowRelatedEntityPermissionHelper::FORCE_GRANT === $workflowPermissionAsAttachment || $regularPermission, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT => true, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED => false, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN => $this->voteOnStoredObjectAsAttachementOfAWorkflow($attribute, $regularPermission, $subject), }; } + + private function voteOnStoredObjectAsAttachementOfAWorkflow(StoredObjectRoleEnum $attribute, bool $regularPermission, StoredObject $storedObject): bool + { + $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($storedObject); + + // we get all the entity workflows where the stored object is attached + $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments); + + // we compute all the permission for each entity workflow + $permissions = array_map(fn (EntityWorkflow $entityWorkflow): string => match ($attribute) { + StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entityWorkflow), + StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entityWorkflow), + }, $entityWorkflows); + + // now, we reduce the permissions: abstain are ignored. Between DENIED and and GRANT, DENIED takes precedence + $computedPermission = WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN; + foreach ($permissions as $permission) { + if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED === $permission) { + return false; + } + if (WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT === $permission) { + $computedPermission = WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT; + } + } + + if (WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN === $computedPermission) { + return $regularPermission; + } + + // this is the case where WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT is returned + return true; + } } diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php index e1a9add7d..aa34e7877 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/AccompanyingCourseDocumentStoredObjectVoter.php @@ -16,6 +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\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; @@ -25,8 +26,9 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb private readonly AccompanyingCourseDocumentRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php index 16833c535..d9bcc5597 100644 --- a/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php +++ b/src/Bundle/ChillDocStoreBundle/Security/Authorization/StoredObjectVoter/PersonDocumentStoredObjectVoter.php @@ -16,6 +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\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Symfony\Component\Security\Core\Security; @@ -25,8 +26,9 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter private readonly PersonDocumentRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php index d1a96a5bb..a01673165 100644 --- a/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php +++ b/src/Bundle/ChillDocStoreBundle/Tests/Security/Authorization/AbstractStoredObjectVoterTest.php @@ -16,8 +16,11 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\MainBundle\Entity\User; -use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; +use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; +use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelperInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Security; @@ -31,21 +34,31 @@ class AbstractStoredObjectVoterTest extends TestCase { use ProphecyTrait; + /** + * @param array $attachments + * + * @return void + */ private function buildStoredObjectVoter( bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + ?WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService = null, + array $attachments = [], ): AbstractStoredObjectVoter { + $attachmentsRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class); + $attachmentsRepository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments); + // Anonymous class extending the abstract class - return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter { + return new class ($canBeAssociatedWithWorkflow, $repository, $security, $attachmentsRepository->reveal(), $workflowDocumentService) extends AbstractStoredObjectVoter { public function __construct( private readonly bool $canBeAssociatedWithWorkflow, private readonly AssociatedEntityToStoredObjectInterface $repository, Security $security, - ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null, + EntityWorkflowAttachmentRepository $attachmentRepository, + WorkflowRelatedEntityPermissionHelperInterface $workflowDocumentService, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function attributeToRole($attribute): string @@ -72,28 +85,29 @@ class AbstractStoredObjectVoterTest extends TestCase public function testSupportsOnAttribute(): void { - $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), null); + $entityWorkflowService = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); + + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new \stdClass()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal()); self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); - $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), null); + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(new User()), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal()); self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); - $voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), null); + $voter = $this->buildStoredObjectVoter(false, new DummyRepository(null), $this->prophesize(Security::class)->reveal(), $entityWorkflowService->reveal()); self::assertFalse($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject())); } /** - * @dataProvider dataProviderVoteOnAttributeWithStoredObjectPermission + * @dataProvider dataProviderVoteOnAttributeWithWorkflow */ - public function testVoteOnAttributeWithStoredObjectPermission( + public function testVoteOnAttributeWithWorkflow( StoredObjectRoleEnum $attribute, bool $expected, bool $isGrantedRegularPermission, string $isGrantedWorkflowPermission, - string $isGrantedStoredObjectAttachment, ): void { $storedObject = new StoredObject(); $repository = new DummyRepository($related = new \stdClass()); @@ -102,31 +116,28 @@ class AbstractStoredObjectVoterTest extends TestCase $security = $this->prophesize(Security::class); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); - $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); + $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); $security = $this->prophesize(Security::class); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); + $attachementRepository = $this->prophesize(EntityWorkflowAttachmentRepository::class); + $attachementRepository->findByStoredObject($storedObject)->willReturn([]); + if (StoredObjectRoleEnum::SEE === $attribute) { - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject) - ->shouldBeCalled() - ->willReturn($isGrantedStoredObjectAttachment); $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) ->willReturn($isGrantedWorkflowPermission); } elseif (StoredObjectRoleEnum::EDIT === $attribute) { - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject) - ->shouldBeCalled() - ->willReturn($isGrantedStoredObjectAttachment); $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related) ->willReturn($isGrantedWorkflowPermission); } else { throw new \LogicException('Invalid attribute for StoredObjectVoter'); } - $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal()) extends AbstractStoredObjectVoter { - public function __construct(private $repository, $helper, $security) + $storedObjectVoter = new class ($repository, $workflowRelatedEntityPermissionHelper->reveal(), $security->reveal(), $attachementRepository->reveal()) extends AbstractStoredObjectVoter { + public function __construct(private $repository, $helper, $security, EntityWorkflowAttachmentRepository $attachmentRepository) { - parent::__construct($security, $helper); + parent::__construct($security, $attachmentRepository, $helper); } protected function getRepository(): AssociatedEntityToStoredObjectInterface @@ -155,96 +166,64 @@ class AbstractStoredObjectVoterTest extends TestCase self::assertEquals($expected, $actual); } - public static function dataProviderVoteOnAttributeWithStoredObjectPermission(): iterable + public static function dataProviderVoteOnAttributeWithWorkflow(): iterable { foreach (['read' => StoredObjectRoleEnum::SEE, 'write' => StoredObjectRoleEnum::EDIT] as $action => $attribute) { yield 'Not related to any workflow nor attachment ('.$action.')' => [ $attribute, true, true, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, ]; yield 'Not related to any workflow nor attachment (refuse) ('.$action.')' => [ $attribute, false, false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, ]; yield 'Is granted by a workflow takes precedence (workflow) ('.$action.')' => [ $attribute, false, true, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, ]; yield 'Is granted by a workflow takes precedence (stored object) ('.$action.')' => [ $attribute, false, - true, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - ]; - - yield 'Is granted by a workflow takes precedence (workflow) although grant ('.$action.')' => [ - $attribute, false, - true, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, ]; yield 'Is granted by a workflow takes precedence (stored object) although grant ('.$action.')' => [ $attribute, - false, true, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, + true, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, ]; yield 'Is granted by a workflow takes precedence (initially refused) (workflow) although grant ('.$action.')' => [ $attribute, false, false, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, ]; yield 'Is granted by a workflow takes precedence (initially refused) (stored object) although grant ('.$action.')' => [ - $attribute, - false, - false, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, - ]; - - yield 'Force grant inverse the regular permission (workflow) ('.$action.')' => [ $attribute, true, false, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, + WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, ]; - - yield 'Force grant inverse the regular permission (so) ('.$action.')' => [ - $attribute, - true, - false, - WorkflowRelatedEntityPermissionHelper::ABSTAIN, - WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, - ]; - } } /** - * @dataProvider dataProviderVoteOnAttributeWithoutStoredObjectPermission + * @dataProvider dataProviderVoteOnAttribute */ - public function testVoteOnAttributeWithoutStoredObjectPermission( + public function testVoteOnAttribute( StoredObjectRoleEnum $attribute, bool $expected, bool $canBeAssociatedWithWorkflow, @@ -260,10 +239,7 @@ class AbstractStoredObjectVoterTest extends TestCase $security = $this->prophesize(Security::class); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission); - $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class); - - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); - $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($storedObject)->willReturn(WorkflowRelatedEntityPermissionHelper::ABSTAIN); + $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); if (null !== $isGrantedWorkflowPermissionRead) { $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related) @@ -283,27 +259,155 @@ class AbstractStoredObjectVoterTest extends TestCase self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); } - public static function dataProviderVoteOnAttributeWithoutStoredObjectPermission(): iterable + public static function dataProviderVoteOnAttribute(): iterable { // 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']; // 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']; + yield [StoredObjectRoleEnum::SEE, true, true, true, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, null, 'associated on a workflow, read by regular, abstain by workflow, ask for read, should be granted']; + yield [StoredObjectRoleEnum::SEE, false, true, true, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, null, 'associated on a workflow, denied read by regular, force grant by workflow, ask for read, should be denied']; // 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']; + yield [StoredObjectRoleEnum::EDIT, true, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, 'associated on a workflow, write by regular, abstain by workflow, ask for write, should be granted']; + yield [StoredObjectRoleEnum::EDIT, false, true, true, null, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::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, WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, 'associated on a workflow, denied write by regular, force grant by workflow, ask for write, should be denied']; + } + + /** + * @dataProvider dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments + */ + public function testPrecedenceOfDirectAssociationOverWorkflowAttachments( + StoredObjectRoleEnum $attribute, + bool $expected, + bool $regularPermission, + string $directWorkflowPermission, + string $attachmentWorkflowPermission, + string $message, + ): void { + $storedObject = new StoredObject(); + $repository = new DummyRepository($related = new \stdClass()); + $token = new UsernamePasswordToken(new User(), 'dummy'); + + $security = $this->prophesize(Security::class); + $security->isGranted('SOME_ROLE', $related)->willReturn($regularPermission); + + $workflowHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelperInterface::class); + + // Direct association permission + if (StoredObjectRoleEnum::SEE === $attribute) { + $workflowHelper->isAllowedByWorkflowForReadOperation($related) + ->willReturn($directWorkflowPermission); + } else { + $workflowHelper->isAllowedByWorkflowForWriteOperation($related) + ->willReturn($directWorkflowPermission); + } + + // Attachment permission + $entityWorkflow = $this->prophesize(\Chill\MainBundle\Entity\Workflow\EntityWorkflow::class)->reveal(); + $attachment = $this->prophesize(EntityWorkflowAttachment::class); + $attachment->getEntityWorkflow()->willReturn($entityWorkflow); + + if (StoredObjectRoleEnum::SEE === $attribute) { + $workflowHelper->isAllowedByWorkflowForReadOperation($entityWorkflow) + ->willReturn($attachmentWorkflowPermission); + } else { + $workflowHelper->isAllowedByWorkflowForWriteOperation($entityWorkflow) + ->willReturn($attachmentWorkflowPermission); + } + + $voter = $this->buildStoredObjectVoter( + true, + $repository, + $security->reveal(), + $workflowHelper->reveal(), + [$attachment->reveal()] + ); + + self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message); + } + + public static function dataProviderPrecedenceOfDirectAssociationOverWorkflowAttachments(): iterable + { + $cases = [ + [ + 'expected' => true, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'message' => 'Direct FORCE_GRANT should win over attachment FORCE_DENIED', + ], + [ + 'expected' => false, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'message' => 'Direct FORCE_DENIED should win over attachment FORCE_GRANT', + ], + [ + 'expected' => true, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Direct FORCE_GRANT should win over attachment ABSTAIN', + ], + [ + 'expected' => false, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Direct FORCE_DENIED should win over attachment ABSTAIN', + ], + [ + 'expected' => true, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_GRANT, + 'message' => 'Direct ABSTAIN should let attachment FORCE_GRANT win', + ], + [ + 'expected' => false, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::FORCE_DENIED, + 'message' => 'Direct ABSTAIN should let attachment FORCE_DENIED win', + ], + [ + 'expected' => true, + 'regular' => true, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Both ABSTAIN should let regular permission (true) win', + ], + [ + 'expected' => false, + 'regular' => false, + 'direct' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'attachment' => WorkflowRelatedEntityPermissionHelperInterface::ABSTAIN, + 'message' => 'Both ABSTAIN should let regular permission (false) win', + ], + ]; + + foreach ([StoredObjectRoleEnum::SEE, StoredObjectRoleEnum::EDIT] as $attribute) { + foreach ($cases as $case) { + yield sprintf('%s - %s', $attribute->name, $case['message']) => [ + $attribute, + $case['expected'], + $case['regular'], + $case['direct'], + $case['attachment'], + $case['message'], + ]; + } + } } } diff --git a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php index 7e61db7d6..e6114276f 100644 --- a/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php +++ b/src/Bundle/ChillEventBundle/Security/Authorization/EventStoredObjectVoter.php @@ -14,6 +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\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\EventBundle\Entity\Event; use Chill\EventBundle\Repository\EventRepository; @@ -26,8 +27,9 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter private readonly EventRepository $repository, Security $security, WorkflowRelatedEntityPermissionHelper $workflowDocumentService, + EntityWorkflowAttachmentRepository $attachmentRepository, ) { - parent::__construct($security, $workflowDocumentService); + parent::__construct($security, $attachmentRepository, $workflowDocumentService); } protected function getRepository(): AssociatedEntityToStoredObjectInterface diff --git a/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php b/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php index aefcf7948..4e7ba4a00 100644 --- a/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Services/Workflow/CancelStaleWorkflowHandlerTest.php @@ -42,14 +42,14 @@ class CancelStaleWorkflowHandlerTest extends TestCase { use ProphecyTrait; - public function testWorkflowWithOneStepOlderThan90DaysIsCanceled(): void + public function testWorkflowWithOneStepOlderThan180DaysIsCanceled(): void { $clock = new MockClock('2024-01-01'); - $daysAgos = new \DateTimeImmutable('2023-09-01'); + $daysAgos = new \DateTimeImmutable('2023-06-01'); $workflow = new EntityWorkflow(); $workflow->setWorkflowName('dummy_workflow'); - $workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01')); + $workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01')); $workflow->setStep('step1', new WorkflowTransitionContextDTO($workflow), 'to_step1', $daysAgos, new User()); $em = $this->prophesize(EntityManagerInterface::class); @@ -94,7 +94,7 @@ class CancelStaleWorkflowHandlerTest extends TestCase $workflow = new EntityWorkflow(); $workflow->setWorkflowName('dummy_workflow'); - $workflow->setCreatedAt(new \DateTimeImmutable('2023-09-01')); + $workflow->setCreatedAt(new \DateTimeImmutable('2023-06-01')); $em = $this->prophesize(EntityManagerInterface::class); $em->flush()->shouldBeCalled(); diff --git a/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php b/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php index d28645182..a778c64af 100644 --- a/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Workflow/Helper/WorkflowRelatedEntityPermissionHelperTest.php @@ -11,9 +11,6 @@ declare(strict_types=1); namespace Chill\MainBundle\Tests\Workflow\Helper; -use Chill\DocStoreBundle\Entity\StoredObject; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; -use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; @@ -269,217 +266,7 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows); - $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class); - $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn([]); - - return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); - } - - /** - * @dataProvider provideDataAllowedByWorkflowReadOperationByAttachment - * - * @param list $entityWorkflows - */ - public function testAllowedByWorkflowReadByAttachment( - 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->buildHelperForAttachment($entityWorkflows, $user, $atDate); - - self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new StoredObject()), $message); - } - - public static function provideDataAllowedByWorkflowReadOperationByAttachment(): 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::ABSTAIN, new \DateTimeImmutable(), - 'Abstain: there is a signature for person, but the attachment is not concerned']; - } - - /** - * @dataProvider provideDataAllowedByWorkflowWriteOperationByAttachment - * - * @param list $entityWorkflows - */ - public function testAllowedByWorkflowWriteByAttachment( - 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->buildHelperForAttachment($entityWorkflows, $user, $atDate); - - self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new StoredObject()), $message); - } - - public static function provideDataAllowedByWorkflowWriteOperationByAttachment(): iterable - { - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); - - yield [[], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because there is no workflow']; - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because the user is not present as a dest user (and attachment)']; - - $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']; - - yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain because the user was not 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], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain: there is a signature, but not on the attachment']; - - $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], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(), - 'abstain: there is a signature, but the signature is not on the attachment']; - - $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']; - - $entityWorkflow = new EntityWorkflow(); - $dto = new WorkflowTransitionContextDTO($entityWorkflow); - $dto->futureDestUsers[] = $user = new User(); - $entityWorkflow->setStep('sent_external', $dto, 'to_sent_external', new \DateTimeImmutable(), $user); - - yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(), - 'force denied: the workflow is sent to an external user']; - } - - /** - * @param list $entityWorkflows - */ - private function buildHelperForAttachment(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'))->shouldNotBeCalled(); - - $repository = $this->prophesize(EntityWorkflowAttachmentRepository::class); - $attachments = []; - foreach ($entityWorkflows as $entityWorkflow) { - $attachments[] = new EntityWorkflowAttachment('dummy', ['id' => 1], $entityWorkflow, new StoredObject()); - } - $repository->findByStoredObject(Argument::type(StoredObject::class))->willReturn($attachments); - - return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $repository->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); + return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable())); } private static function buildRegistry(): Registry diff --git a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php index 6d393b318..4c7c3e130 100644 --- a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php +++ b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelper.php @@ -11,12 +11,9 @@ declare(strict_types=1); namespace Chill\MainBundle\Workflow\Helper; -use Chill\DocStoreBundle\Entity\StoredObject; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; -use Chill\MainBundle\Entity\Workflow\EntityWorkflowAttachment; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; -use Chill\MainBundle\Repository\Workflow\EntityWorkflowAttachmentRepository; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Security\Core\Security; @@ -52,48 +49,28 @@ use Symfony\Component\Workflow\Registry; * the workflow denys write operations; * - if there is no case above and the user is involved in the workflow (is part of the current step, of a step before), the user is granted; */ -class WorkflowRelatedEntityPermissionHelper +final readonly class WorkflowRelatedEntityPermissionHelper implements WorkflowRelatedEntityPermissionHelperInterface { - public const FORCE_GRANT = 'FORCE_GRANT'; - public const FORCE_DENIED = 'FORCE_DENIED'; - public const ABSTAIN = 'ABSTAIN'; - public function __construct( - private readonly Security $security, - private readonly EntityWorkflowManager $entityWorkflowManager, - private readonly EntityWorkflowAttachmentRepository $entityWorkflowAttachmentRepository, - private readonly Registry $registry, - private readonly ClockInterface $clock, + private Security $security, + private EntityWorkflowManager $entityWorkflowManager, + private Registry $registry, + private ClockInterface $clock, ) {} /** - * @param object $entity The entity may be an + * @param object $entity The entity may be an object that is a related entity of a workflow, or an EntityWorkflow itself * * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN' */ public function isAllowedByWorkflowForReadOperation(object $entity): string { - if ($entity instanceof StoredObject) { - $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity); - $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments); - $isAttached = true; - } else { - $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity); - $isAttached = false; - } - - if ([] === $entityWorkflows) { - return self::ABSTAIN; - } + $entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity); if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) { return self::FORCE_GRANT; } - if ($isAttached) { - return self::ABSTAIN; - } - // 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) { @@ -117,24 +94,20 @@ class WorkflowRelatedEntityPermissionHelper } /** + * @param object $entity the entity may be an object which is the related entity of a workflow + * * @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN' */ public function isAllowedByWorkflowForWriteOperation(object $entity): string { - if ($entity instanceof StoredObject) { - $attachments = $this->entityWorkflowAttachmentRepository->findByStoredObject($entity); - $entityWorkflows = array_map(static fn (EntityWorkflowAttachment $attachment) => $attachment->getEntityWorkflow(), $attachments); - $isAttached = true; - } else { - $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity); - $isAttached = false; - } + $entityWorkflows = $entity instanceof EntityWorkflow ? [$entity] : $this->entityWorkflowManager->findByRelatedEntity($entity); if ([] === $entityWorkflows) { return self::ABSTAIN; } - // if a workflow is finalized positive, anyone is allowed to edit the document anymore + + // if a workflow is finalized positive or isSentExternal, no one is allowed to edit the document anymore foreach ($entityWorkflows as $entityWorkflow) { $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); @@ -147,39 +120,64 @@ class WorkflowRelatedEntityPermissionHelper // the workflow is final, and final positive, or is sentExternal, so we stop here. return self::FORCE_DENIED; } - if ( - // if not finalized positive - $entityWorkflow->isFinal() && !($placeMetadata['isFinalPositive'] ?? false) - ) { - return self::ABSTAIN; - } } } - $runningWorkflows = array_filter($entityWorkflows, fn (EntityWorkflow $ew) => !$ew->isFinal()); + // if there is a signature on a **running workflow**, no one is allowed edit anymore, except if the workflow is canceled + foreach ($entityWorkflows as $entityWorkflow) { + // if the workflow is canceled, we ignore it + $isFinalNegative = false; + 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 (isset($placeMetadata['isFinalPositive']) && false === $placeMetadata['isFinalPositive']) { + $isFinalNegative = true; + } + } + } + if ($isFinalNegative) { + continue; + } - // if there is a signature on a **running workflow**, no one is allowed edit the workflow anymore - if (!$isAttached) { - foreach ($runningWorkflows as $entityWorkflow) { - foreach ($entityWorkflow->getSteps() as $step) { - foreach ($step->getSignatures() as $signature) { - if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { - return self::FORCE_DENIED; - } + foreach ($entityWorkflow->getSteps() as $step) { + foreach ($step->getSignatures() as $signature) { + if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) { + return self::FORCE_DENIED; } } } } + // if all workflows are finalized negative (= canceled), we should abstain + $runningWorkflows = []; + foreach ($entityWorkflows as $entityWorkflow) { + $isFinalNegative = false; + 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 (isset($placeMetadata['isFinalPositive']) && false === $placeMetadata['isFinalPositive']) { + $isFinalNegative = true; + } + } + } + if (!$isFinalNegative) { + $runningWorkflows[] = $entityWorkflow; + } + } + + if ([] === $runningWorkflows) { + return self::ABSTAIN; + } + // allow only the users involved if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) { return self::FORCE_GRANT; } - if ($isAttached) { - return self::ABSTAIN; - } - return self::FORCE_DENIED; } diff --git a/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelperInterface.php b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelperInterface.php new file mode 100644 index 000000000..eefadf0f8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Helper/WorkflowRelatedEntityPermissionHelperInterface.php @@ -0,0 +1,63 @@ +