Merge remote-tracking branch 'origin/master' into signature-app-master

This commit is contained in:
Julien Fastré 2024-11-14 12:18:07 +01:00
commit b7e27536bd
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
6 changed files with 349 additions and 278 deletions

View File

@ -0,0 +1,6 @@
kind: Fixed
body: Adjust household list export to include households even if their address is
NULL
time: 2024-10-29T14:33:04.393943822+01:00
custom:
Issue: ""

View File

@ -46,29 +46,27 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool 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); $entity = $this->getRepository()->findAssociatedEntityToStoredObject($subject);
if ($this->workflowDocumentService->isAllowedByWorkflow($entity)) { // Determine the attribute to pass to the voter for argument
// read and write permissions are granted by workflow
return true;
}
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
$voterAttribute = $this->attributeToRole($attribute); $voterAttribute = $this->attributeToRole($attribute);
if (false === $this->security->isGranted($voterAttribute, $entity)) { $regularPermission = $this->security->isGranted($voterAttribute, $entity);
return false;
if (!$this->canBeAssociatedWithWorkflow()) {
return $regularPermission;
} }
if (StoredObjectRoleEnum::SEE !== $attribute && $this->canBeAssociatedWithWorkflow()) { $workflowPermission = match ($attribute) {
if (null === $this->workflowDocumentService) { StoredObjectRoleEnum::SEE => $this->workflowDocumentService->isAllowedByWorkflowForReadOperation($entity),
throw new \LogicException('Provide a workflow document service'); StoredObjectRoleEnum::EDIT => $this->workflowDocumentService->isAllowedByWorkflowForWriteOperation($entity),
} };
return $this->workflowDocumentService->notBlockedByWorkflow($entity); return match ($workflowPermission) {
} WorkflowRelatedEntityPermissionHelper::FORCE_GRANT => true,
WorkflowRelatedEntityPermissionHelper::FORCE_DENIED => false,
return true; WorkflowRelatedEntityPermissionHelper::ABSTAIN => $regularPermission,
};
} }
} }

View File

@ -15,10 +15,11 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface; use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum; use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter; use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoter\AbstractStoredObjectVoter;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use PHPUnit\Framework\TestCase; 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; use Symfony\Component\Security\Core\Security;
/** /**
@ -28,19 +29,14 @@ use Symfony\Component\Security\Core\Security;
*/ */
class AbstractStoredObjectVoterTest extends TestCase class AbstractStoredObjectVoterTest extends TestCase
{ {
private AssociatedEntityToStoredObjectInterface $repository; use ProphecyTrait;
private Security $security;
private WorkflowRelatedEntityPermissionHelper $workflowDocumentService;
protected function setUp(): void private function buildStoredObjectVoter(
{ bool $canBeAssociatedWithWorkflow,
$this->repository = $this->createMock(AssociatedEntityToStoredObjectInterface::class); AssociatedEntityToStoredObjectInterface $repository,
$this->security = $this->createMock(Security::class); Security $security,
$this->workflowDocumentService = $this->createMock(WorkflowRelatedEntityPermissionHelper::class); ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
} ): AbstractStoredObjectVoter {
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null): AbstractStoredObjectVoter
{
// Anonymous class extending the abstract class // Anonymous class extending the abstract class
return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter { return new class ($canBeAssociatedWithWorkflow, $repository, $security, $workflowDocumentService) extends AbstractStoredObjectVoter {
public function __construct( public function __construct(
@ -74,114 +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 testIsAllowedByWorkflow(): void
{
[$user, $token, $subject, $entity] = $this->setupMockObjects();
$workflowRelatedEntityPermissionHelper = $this->createMock(WorkflowRelatedEntityPermissionHelper::class);
$workflowRelatedEntityPermissionHelper->method('isAllowedByWorkflow')->withAnyParameters()->willReturn(true);
$associatedObjectRepository = $this->createMock(AssociatedEntityToStoredObjectInterface::class);
$associatedObjectRepository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
$voter = $this->buildStoredObjectVoter(
true,
$associatedObjectRepository,
$this->createMock(Security::class),
$workflowRelatedEntityPermissionHelper
);
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::EDIT, $subject, $token));
}
public function testSupportsOnAttribute(): void 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 self::assertTrue($voter->supports(StoredObjectRoleEnum::SEE, new StoredObject()));
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
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 /**
{ * @dataProvider dataProviderVoteOnAttribute
[$user, $token, $subject, $entity] = $this->setupMockObjects(); */
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 $security = $this->prophesize(Security::class);
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, true); $security->isGranted('SOME_ROLE', $related)->willReturn($isGrantedRegularPermission);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// The voteOnAttribute method should return True when workflow is allowed $workflowRelatedEntityPermissionHelper = $this->prophesize(WorkflowRelatedEntityPermissionHelper::class);
self::assertTrue($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token)); if (null !== $isGrantedWorkflowPermissionRead) {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)
->willReturn($isGrantedWorkflowPermissionRead)->shouldBeCalled();
} else {
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForReadOperation($related)->shouldNotBeCalled();
} }
public function testVoteOnAttributeNotAllowed(): void if (null !== $isGrantedWorkflowPermissionWrite) {
{ $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
[$user, $token, $subject, $entity] = $this->setupMockObjects(); ->willReturn($isGrantedWorkflowPermissionWrite)->shouldBeCalled();
} else {
// Setup mocks for voteOnAttribute method where isGranted() returns false $workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)->shouldNotBeCalled();
$this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
// The voteOnAttribute method should return True when workflow is allowed
self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
} }
public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void $voter = $this->buildStoredObjectVoter($canBeAssociatedWithWorkflow, $dummyRepository, $security->reveal(), $workflowRelatedEntityPermissionHelper->reveal());
{ self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
[$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 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 // associated on a workflow, read operation
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false); 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'];
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService); 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'];
// Test voteOnAttribute method // association on a workflow, write operation
$attribute = StoredObjectRoleEnum::SEE; 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'];
$result = $voter->voteOnAttribute($attribute, $subject, $token); 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'];
// Assert that access is denied when workflow is not allowed 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'];
$this->assertTrue($result); 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;
} }
} }

View File

@ -15,7 +15,6 @@ use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore; use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
@ -23,6 +22,7 @@ use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\DefinitionBuilder; use Symfony\Component\Workflow\DefinitionBuilder;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
@ -41,67 +41,85 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
use ProphecyTrait; use ProphecyTrait;
/** /**
* @dataProvider provideDataNotBlockByWorkflow * @dataProvider provideDataAllowedByWorkflowReadOperation
*
* @param list<EntityWorkflow> $entityWorkflows
*/ */
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void 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 // all entities must have this workflow name, so we are ok to set it here
foreach ($entityWorkflows as $entityWorkflow) {
$entityWorkflow->setWorkflowName('dummy'); $entityWorkflow->setWorkflowName('dummy');
$object = new \stdClass(); }
$helper = $this->buildHelper($object, $entityWorkflow, $user); $helper = $this->buildHelper($entityWorkflows, $user, $atDate);
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message); self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()), $message);
} }
/** /**
* @dataProvider provideDataAllowedByWorkflow * @dataProvider provideDataAllowedByWorkflowWriteOperation
*
* @param list<EntityWorkflow> $entityWorkflows
*/ */
public function testAllowedByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void 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 // all entities must have this workflow name, so we are ok to set it here
foreach ($entityWorkflows as $entityWorkflow) {
$entityWorkflow->setWorkflowName('dummy'); $entityWorkflow->setWorkflowName('dummy');
$object = new \stdClass(); }
$helper = $this->buildHelper($object, $entityWorkflow, $user); $helper = $this->buildHelper($entityWorkflows, $user, $atDate);
self::assertEquals($expected, $helper->isAllowedByWorkflow($entityWorkflow), $message); self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()), $message);
} }
public function testNoWorkflow(): void public function testNoWorkflow(): void
{ {
$object = new \stdClass(); $helper = $this->buildHelper([], new User(), null);
$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"); self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()));
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()));
} }
private function buildHelper(object $relatedEntity, ?EntityWorkflow $entityWorkflow, User $user): WorkflowRelatedEntityPermissionHelper /**
* @param list<EntityWorkflow> $entityWorkflows
*/
private function buildHelper(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper
{ {
$security = $this->prophesize(Security::class); $security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user); $security->getUser()->willReturn($user);
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class); $entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
if (null !== $entityWorkflow) { $entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
} else { return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([]);
} }
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry()); public static function provideDataAllowedByWorkflowReadOperation(): iterable
}
public static function provideDataAllowedByWorkflow(): iterable
{ {
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, new User(), false, 'not allowed because the user is not present as a dest user']; yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
'abstain because the user is not present as a dest user'];
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User(); $dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, $user, true, 'allowed because the user is a current user']; yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant because the user is a current user'];
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
@ -112,7 +130,68 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
$dto->futureDestUsers[] = new User(); $dto->futureDestUsers[] = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User()); $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, $user, true, 'allowed because the user was a previous 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(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
@ -122,7 +201,8 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User()); $entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
$entityWorkflow->getCurrentStep()->setIsFinal(true); $entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, false, 'not allowed because: user was a previous user, but it is finalized positive']; yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
'force denied: user was a previous user, but it is finalized positive'];
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
@ -132,80 +212,48 @@ class WorkflowRelatedEntityPermissionHelperTest extends TestCase
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable()); $entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
$entityWorkflow->getCurrentStep()->setIsFinal(true); $entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, true, 'allowed: user was a previous user, it is finalized, but finalized negative']; yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
'abstain: user was a previous user, it is finalized, but finalized negative'];
}
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(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User(); $dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $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, true, 'allowed because the user is present as a dest user']; yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
'force denied because there is a signature'];
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User(); $dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = new User(); $dto->futurePersonSignatures[] = new Person();
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user); $entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a **previous** dest user']; yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
'force grant: there is a signature, but still pending'];
$entityWorkflow = new EntityWorkflow(); $entityWorkflow = new EntityWorkflow();
$dto = new WorkflowTransitionContextDTO($entityWorkflow); $dto = new WorkflowTransitionContextDTO($entityWorkflow);
$dto->futureDestUsers[] = $user = new User(); $dto->futureDestUsers[] = $user = new User();
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $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); $entityWorkflow->getCurrentStep()->setIsFinal(true);
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive']; 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('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 private static function buildRegistry(): Registry

View File

@ -11,29 +11,100 @@ declare(strict_types=1);
namespace Chill\MainBundle\Workflow\Helper; namespace Chill\MainBundle\Workflow\Helper;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\Clock\ClockInterface;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
/** /**
* Check if an object, associated with a workflow, is blocked, or not, by this workflow. * Helper to give supplementary permissions to a related entity.
*
* If a related entity is associated within a workflow, the logic of the workflow can give more permissions, or
* remove some permissions.
*
* The methods of this helper return either:
*
* - FORCE_GRANT, which means that a permission can be given, even if it would be denied when the related
* entity is not associated with a workflow;
* - FORCE_DENIED, which means that a permission should be denied, even if it would be granted when the related entity
* is not associated with a workflow
* - ABSTAIN, if there is no workflow logic to add or remove permission
*
* For read operations:
*
* - if the user is involved in the workflow (is part of the current step, of a step before), the user is granted read
* operation;
* - if there is a pending signature for a person, the workflow grant access to the related entity;
* - if there a signature applyied in less than 12 hours, the workflow grant access to the related entity. This allow to
* show the related entity to the person during this time frame.
*
*
* For write operation:
*
* - if the workflow is finalized "positive" (means "not canceled"), the workflow denys write operations;
* - if there isn't any finalized "positive" workflow, and if there is a signature appliyed for a running workflow (not finalized nor canceled),
* 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 class WorkflowRelatedEntityPermissionHelper
{ {
public const FORCE_GRANT = 'FORCE_GRANT';
public const FORCE_DENIED = 'FORCE_DENIED';
public const ABSTAIN = 'ABSTAIN';
public function __construct( public function __construct(
private readonly Security $security, private readonly Security $security,
private readonly EntityWorkflowManager $entityWorkflowManager, private readonly EntityWorkflowManager $entityWorkflowManager,
private readonly Registry $registry, private readonly Registry $registry,
private readonly ClockInterface $clock,
) {} ) {}
public function isAllowedByWorkflow(object $entity): bool /**
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
*/
public function isAllowedByWorkflowForReadOperation(object $entity): string
{ {
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity); $entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
$currentUser = $this->security->getUser();
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) { foreach ($entityWorkflows as $entityWorkflow) {
// if the user is finalized, we have to check if the workflow is finalPositive, or not
if ($entityWorkflow->isFinal()) { if ($entityWorkflow->isFinal()) {
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow); $marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
@ -41,12 +112,40 @@ class WorkflowRelatedEntityPermissionHelper
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place); $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
if (true === ($placeMetadata['isFinalPositive'] ?? false)) { if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
// the workflow is final, and final positive, so we stop here. // the workflow is final, and final positive, so we stop here.
return false; 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<EntityWorkflow> $entityWorkflows
*/
private function isUserInvolvedInAWorkflow(array $entityWorkflows): bool
{
$currentUser = $this->security->getUser();
foreach ($entityWorkflows as $entityWorkflow) { foreach ($entityWorkflows as $entityWorkflow) {
// so, the workflow is running... We return true if the current user is involved // so, the workflow is running... We return true if the current user is involved
foreach ($entityWorkflow->getSteps() as $step) { foreach ($entityWorkflow->getSteps() as $step) {
@ -58,58 +157,4 @@ class WorkflowRelatedEntityPermissionHelper
return false; return false;
} }
/**
* Return true if the user is allowed to update the given object.
*
* Return false if some workflow block the edition of the object.
*/
public function notBlockedByWorkflow(object $entity): bool
{
$entityWorkflows = $this->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;
}
} }

View File

@ -219,6 +219,8 @@ class ListHouseholdInPeriod implements ListInterface, GroupedExportInterface
$qb $qb
->leftJoin('household.addresses', 'addresses') ->leftJoin('household.addresses', 'addresses')
->andWhere( ->andWhere(
$qb->expr()->orX(
$qb->expr()->isNull('addresses'), // Include households without any address
$qb->expr()->andX( $qb->expr()->andX(
$qb->expr()->lte('addresses.validFrom', ':calcDate'), $qb->expr()->lte('addresses.validFrom', ':calcDate'),
$qb->expr()->orX( $qb->expr()->orX(
@ -226,6 +228,7 @@ class ListHouseholdInPeriod implements ListInterface, GroupedExportInterface
$qb->expr()->gt('addresses.validTo', ':calcDate') $qb->expr()->gt('addresses.validTo', ':calcDate')
) )
) )
)
); );
$this->addressHelper->addSelectClauses( $this->addressHelper->addSelectClauses(
ExportAddressHelper::F_ALL, ExportAddressHelper::F_ALL,