mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch '323-related-entity-permission-give-from-workflow' into 'master'
Permettre aux utilisateurs concerné par un workflow de modifier un document, même si, en dehors du workflow, ils ne possèdent pas de droits d'édition et de lecture sur ce document Closes #323 See merge request Chill-Projet/chill-bundles!756
This commit is contained in:
commit
887f3e0aa2
@ -16,7 +16,7 @@ use Chill\ActivityBundle\Repository\ActivityRepository;
|
|||||||
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\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
@ -24,7 +24,7 @@ class ActivityStoredObjectVoter extends AbstractStoredObjectVoter
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ActivityRepository $repository,
|
private readonly ActivityRepository $repository,
|
||||||
Security $security,
|
Security $security,
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||||
) {
|
) {
|
||||||
parent::__construct($security, $workflowDocumentService);
|
parent::__construct($security, $workflowDocumentService);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface;
|
||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
|
||||||
|
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@ -75,6 +76,7 @@ class SignatureRequestController
|
|||||||
// options for user render
|
// options for user render
|
||||||
'absence' => false,
|
'absence' => false,
|
||||||
'main_scope' => false,
|
'main_scope' => false,
|
||||||
|
UserRender::SPLIT_LINE_BEFORE_CHARACTER => 30,
|
||||||
// options for person render
|
// options for person render
|
||||||
'addAge' => false,
|
'addAge' => false,
|
||||||
]),
|
]),
|
||||||
|
@ -15,7 +15,7 @@ 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\StoredObjectVoterInterface;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectVoterInterface;
|
||||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Security $security,
|
private readonly Security $security,
|
||||||
private readonly ?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
private readonly ?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
public function supports(StoredObjectRoleEnum $attribute, StoredObject $subject): bool
|
||||||
@ -46,24 +46,27 @@ abstract class AbstractStoredObjectVoter implements StoredObjectVoterInterface
|
|||||||
|
|
||||||
public function voteOnAttribute(StoredObjectRoleEnum $attribute, StoredObject $subject, TokenInterface $token): bool
|
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);
|
||||||
|
|
||||||
// Determine the attribute to pass to AccompanyingCourseDocumentVoter
|
// Determine the attribute to pass to the voter for argument
|
||||||
$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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AccompanyingCourseDocumentRepository;
|
|||||||
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
@ -24,7 +24,7 @@ final class AccompanyingCourseDocumentStoredObjectVoter extends AbstractStoredOb
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AccompanyingCourseDocumentRepository $repository,
|
private readonly AccompanyingCourseDocumentRepository $repository,
|
||||||
Security $security,
|
Security $security,
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||||
) {
|
) {
|
||||||
parent::__construct($security, $workflowDocumentService);
|
parent::__construct($security, $workflowDocumentService);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ use Chill\DocStoreBundle\Repository\AssociatedEntityToStoredObjectInterface;
|
|||||||
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
use Chill\DocStoreBundle\Repository\PersonDocumentRepository;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter;
|
||||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Symfony\Component\Security\Core\Security;
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
||||||
@ -24,7 +24,7 @@ class PersonDocumentStoredObjectVoter extends AbstractStoredObjectVoter
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PersonDocumentRepository $repository,
|
private readonly PersonDocumentRepository $repository,
|
||||||
Security $security,
|
Security $security,
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||||
) {
|
) {
|
||||||
parent::__construct($security, $workflowDocumentService);
|
parent::__construct($security, $workflowDocumentService);
|
||||||
}
|
}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Service;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an object, associated with a workflow, is blocked, or not, by this workflow.
|
|
||||||
*/
|
|
||||||
class WorkflowStoredObjectPermissionHelper
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly Security $security,
|
|
||||||
private readonly EntityWorkflowManager $entityWorkflowManager,
|
|
||||||
private readonly Registry $registry,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
|
||||||
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,26 +29,21 @@ use Symfony\Component\Security\Core\Security;
|
|||||||
*/
|
*/
|
||||||
class AbstractStoredObjectVoterTest extends TestCase
|
class AbstractStoredObjectVoterTest extends TestCase
|
||||||
{
|
{
|
||||||
private AssociatedEntityToStoredObjectInterface $repository;
|
use ProphecyTrait;
|
||||||
private Security $security;
|
|
||||||
private WorkflowStoredObjectPermissionHelper $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(WorkflowStoredObjectPermissionHelper::class);
|
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||||
}
|
): AbstractStoredObjectVoter {
|
||||||
|
|
||||||
private function buildStoredObjectVoter(bool $canBeAssociatedWithWorkflow, AssociatedEntityToStoredObjectInterface $repository, Security $security, ?WorkflowStoredObjectPermissionHelper $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(
|
||||||
private readonly bool $canBeAssociatedWithWorkflow,
|
private readonly bool $canBeAssociatedWithWorkflow,
|
||||||
private readonly AssociatedEntityToStoredObjectInterface $repository,
|
private readonly AssociatedEntityToStoredObjectInterface $repository,
|
||||||
Security $security,
|
Security $security,
|
||||||
?WorkflowStoredObjectPermissionHelper $workflowDocumentService = null,
|
?WorkflowRelatedEntityPermissionHelper $workflowDocumentService = null,
|
||||||
) {
|
) {
|
||||||
parent::__construct($security, $workflowDocumentService);
|
parent::__construct($security, $workflowDocumentService);
|
||||||
}
|
}
|
||||||
@ -74,95 +70,89 @@ class AbstractStoredObjectVoterTest extends TestCase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setupMockObjects(): array
|
|
||||||
{
|
|
||||||
$user = new User();
|
|
||||||
$token = $this->createMock(TokenInterface::class);
|
|
||||||
$subject = new StoredObject();
|
|
||||||
$entity = new \stdClass();
|
|
||||||
|
|
||||||
return [$user, $token, $subject, $entity];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function setupMocksForVoteOnAttribute(User $user, TokenInterface $token, bool $isGrantedForEntity, object $entity, bool $workflowAllowed): void
|
|
||||||
{
|
|
||||||
// Set up token to return user
|
|
||||||
$token->method('getUser')->willReturn($user);
|
|
||||||
|
|
||||||
// Mock the return of an AccompanyingCourseDocument by the repository
|
|
||||||
$this->repository->method('findAssociatedEntityToStoredObject')->willReturn($entity);
|
|
||||||
|
|
||||||
// Mock scenario where user is allowed to see_details of the AccompanyingCourseDocument
|
|
||||||
$this->security->method('isGranted')->willReturn($isGrantedForEntity);
|
|
||||||
|
|
||||||
// Mock case where user is blocked or not by workflow
|
|
||||||
$this->workflowDocumentService->method('notBlockedByWorkflow')->willReturn($workflowAllowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSupportsOnAttribute(): void
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $isGrantedWorkflowPermissionWrite) {
|
||||||
|
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)
|
||||||
|
->willReturn($isGrantedWorkflowPermissionWrite)->shouldBeCalled();
|
||||||
|
} else {
|
||||||
|
$workflowRelatedEntityPermissionHelper->isAllowedByWorkflowForWriteOperation($related)->shouldNotBeCalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
$voter = $this->buildStoredObjectVoter($canBeAssociatedWithWorkflow, $dummyRepository, $security->reveal(), $workflowRelatedEntityPermissionHelper->reveal());
|
||||||
|
self::assertEquals($expected, $voter->voteOnAttribute($attribute, $storedObject, $token), $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testVoteOnAttributeNotAllowed(): void
|
public static function dataProviderVoteOnAttribute(): iterable
|
||||||
{
|
{
|
||||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
// not associated on a workflow
|
||||||
|
yield [StoredObjectRoleEnum::SEE, true, false, true, null, null, 'not associated on a workflow, granted by regular access, must not rely on helper'];
|
||||||
|
yield [StoredObjectRoleEnum::SEE, false, false, false, null, null, 'not associated on a workflow, denied by regular access, must not rely on helper'];
|
||||||
|
|
||||||
// Setup mocks for voteOnAttribute method where isGranted() returns false
|
// associated on a workflow, read operation
|
||||||
$this->setupMocksForVoteOnAttribute($user, $token, false, $entity, true);
|
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'];
|
||||||
|
|
||||||
// The voteOnAttribute method should return True when workflow is allowed
|
// association on a workflow, write operation
|
||||||
self::assertFalse($voter->voteOnAttribute(StoredObjectRoleEnum::SEE, $subject, $token));
|
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'];
|
||||||
public function testVoteOnAttributeAllowedWorkflowNotAllowed(): void
|
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'];
|
||||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
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'];
|
||||||
|
}
|
||||||
// Setup mocks for voteOnAttribute method
|
}
|
||||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
|
|
||||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
class DummyRepository implements AssociatedEntityToStoredObjectInterface
|
||||||
|
{
|
||||||
// Test voteOnAttribute method
|
public function __construct(private readonly ?object $relatedEntity) {}
|
||||||
$attribute = StoredObjectRoleEnum::EDIT;
|
|
||||||
$result = $voter->voteOnAttribute($attribute, $subject, $token);
|
public function findAssociatedEntityToStoredObject(StoredObject $storedObject): ?object
|
||||||
|
{
|
||||||
// Assert that access is denied when workflow is not allowed
|
return $this->relatedEntity;
|
||||||
$this->assertFalse($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testVoteOnAttributeAllowedWorkflowAllowedToSeeDocument(): void
|
|
||||||
{
|
|
||||||
[$user, $token, $subject, $entity] = $this->setupMockObjects();
|
|
||||||
|
|
||||||
// Setup mocks for voteOnAttribute method
|
|
||||||
$this->setupMocksForVoteOnAttribute($user, $token, true, $entity, false);
|
|
||||||
$voter = $this->buildStoredObjectVoter(true, $this->repository, $this->security, $this->workflowDocumentService);
|
|
||||||
|
|
||||||
// Test voteOnAttribute method
|
|
||||||
$attribute = StoredObjectRoleEnum::SEE;
|
|
||||||
$result = $voter->voteOnAttribute($attribute, $subject, $token);
|
|
||||||
|
|
||||||
// Assert that access is denied when workflow is not allowed
|
|
||||||
$this->assertTrue($result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,182 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\DocStoreBundle\Tests\Service;
|
|
||||||
|
|
||||||
use Chill\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
|
||||||
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
|
||||||
use Chill\PersonBundle\Entity\Person;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
use Symfony\Component\Workflow\DefinitionBuilder;
|
|
||||||
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
|
||||||
use Symfony\Component\Workflow\Registry;
|
|
||||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
|
||||||
use Symfony\Component\Workflow\Workflow;
|
|
||||||
use Symfony\Component\Workflow\WorkflowInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class WorkflowStoredObjectPermissionHelperTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider provideDataNotBlockByWorkflow
|
|
||||||
*/
|
|
||||||
public function testNotBlockByWorkflow(EntityWorkflow $entityWorkflow, User $user, bool $expected, string $message): void
|
|
||||||
{
|
|
||||||
// all entities must have this workflow name, so we are ok to set it here
|
|
||||||
$entityWorkflow->setWorkflowName('dummy');
|
|
||||||
$object = new \stdClass();
|
|
||||||
$helper = $this->buildHelper($object, $entityWorkflow, $user);
|
|
||||||
|
|
||||||
self::assertEquals($expected, $helper->notBlockedByWorkflow($entityWorkflow), $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNoWorkflow(): void
|
|
||||||
{
|
|
||||||
$object = new \stdClass();
|
|
||||||
$helper = $this->buildHelper($object, null, $user = new User());
|
|
||||||
self::assertTrue($helper->notBlockedByWorkflow($object), "the user is not blocked by the user, as there aren't any user inside");
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildHelper(object $relatedEntity, ?EntityWorkflow $entityWorkflow, User $user): WorkflowStoredObjectPermissionHelper
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->getUser()->willReturn($user);
|
|
||||||
|
|
||||||
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
|
||||||
if (null !== $entityWorkflow) {
|
|
||||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([$entityWorkflow]);
|
|
||||||
} else {
|
|
||||||
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WorkflowStoredObjectPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function provideDataNotBlockByWorkflow(): iterable
|
|
||||||
{
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
|
|
||||||
|
|
||||||
yield [$entityWorkflow, new User(), false, 'blocked because the user is not present as a dest user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a dest user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, true, 'allowed because the user is present as a **previous** dest user'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), $user);
|
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, false, 'blocked because the step is final, and final positive'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, true, 'allowed because the step is final, and final negative'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
|
||||||
$step = $entityWorkflow->getCurrentStep();
|
|
||||||
new EntityWorkflowStepSignature($step, new Person());
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, true, 'allow, a signature is present but still pending'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), $user);
|
|
||||||
$step = $entityWorkflow->getCurrentStep();
|
|
||||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
|
||||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed'];
|
|
||||||
|
|
||||||
$entityWorkflow = new EntityWorkflow();
|
|
||||||
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
|
||||||
$dto->futureDestUsers[] = $user = new User();
|
|
||||||
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), $user);
|
|
||||||
$step = $entityWorkflow->getCurrentStep();
|
|
||||||
$signature = new EntityWorkflowStepSignature($step, new Person());
|
|
||||||
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED);
|
|
||||||
|
|
||||||
yield [$entityWorkflow, $user, false, 'blocked, a signature is present and signed, although the workflow is final negative'];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function buildRegistry(): Registry
|
|
||||||
{
|
|
||||||
$builder = new DefinitionBuilder();
|
|
||||||
$builder
|
|
||||||
->setInitialPlaces(['initial'])
|
|
||||||
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
|
||||||
->setMetadataStore(
|
|
||||||
new InMemoryMetadataStore(
|
|
||||||
placesMetadata: [
|
|
||||||
'final_positive' => [
|
|
||||||
'isFinal' => true,
|
|
||||||
'isFinalPositive' => true,
|
|
||||||
],
|
|
||||||
'final_negative' => [
|
|
||||||
'isFinal' => true,
|
|
||||||
'isFinalPositive' => false,
|
|
||||||
],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
|
||||||
$registry = new Registry();
|
|
||||||
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
|
||||||
public function supports(WorkflowInterface $workflow, object $subject): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return $registry;
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,7 +14,7 @@ namespace Chill\EventBundle\Security\Authorization;
|
|||||||
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\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Chill\EventBundle\Entity\Event;
|
use Chill\EventBundle\Entity\Event;
|
||||||
use Chill\EventBundle\Repository\EventRepository;
|
use Chill\EventBundle\Repository\EventRepository;
|
||||||
use Chill\EventBundle\Security\EventVoter;
|
use Chill\EventBundle\Security\EventVoter;
|
||||||
@ -25,7 +25,7 @@ class EventStoredObjectVoter extends AbstractStoredObjectVoter
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EventRepository $repository,
|
private readonly EventRepository $repository,
|
||||||
Security $security,
|
Security $security,
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||||
) {
|
) {
|
||||||
parent::__construct($security, $workflowDocumentService);
|
parent::__construct($security, $workflowDocumentService);
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,6 @@ namespace Chill\MainBundle\Templating\Entity;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use DateTime;
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use Symfony\Component\Clock\ClockInterface;
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use Twig\Error\LoaderError;
|
use Twig\Error\LoaderError;
|
||||||
@ -26,11 +24,17 @@ use Twig\Error\SyntaxError;
|
|||||||
*/
|
*/
|
||||||
class UserRender implements ChillEntityRenderInterface
|
class UserRender implements ChillEntityRenderInterface
|
||||||
{
|
{
|
||||||
|
public const SPLIT_LINE_BEFORE_CHARACTER = 'split_lines_before_characters';
|
||||||
final public const DEFAULT_OPTIONS = [
|
final public const DEFAULT_OPTIONS = [
|
||||||
'main_scope' => true,
|
'main_scope' => true,
|
||||||
'user_job' => true,
|
'user_job' => true,
|
||||||
'absence' => true,
|
'absence' => true,
|
||||||
'at_date' => null, // instanceof DateTimeInterface
|
'at_date' => null, // instanceof DateTimeInterface
|
||||||
|
/*
|
||||||
|
* when set, the jobs and service will be splitted in multiple lines. The line will be splitted
|
||||||
|
* before the given character. Only for renderString, renderBox is not concerned.
|
||||||
|
*/
|
||||||
|
self::SPLIT_LINE_BEFORE_CHARACTER => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@ -65,8 +69,6 @@ class UserRender implements ChillEntityRenderInterface
|
|||||||
{
|
{
|
||||||
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
||||||
|
|
||||||
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
|
|
||||||
|
|
||||||
if (null === $opts['at_date']) {
|
if (null === $opts['at_date']) {
|
||||||
$opts['at_date'] = $this->clock->now();
|
$opts['at_date'] = $this->clock->now();
|
||||||
} elseif ($opts['at_date'] instanceof \DateTime) {
|
} elseif ($opts['at_date'] instanceof \DateTime) {
|
||||||
@ -89,6 +91,28 @@ class UserRender implements ChillEntityRenderInterface
|
|||||||
$str .= ' ('.$this->translator->trans('absence.Absent').')';
|
$str .= ' ('.$this->translator->trans('absence.Absent').')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (null !== $opts[self::SPLIT_LINE_BEFORE_CHARACTER]) {
|
||||||
|
if (!is_int($opts[self::SPLIT_LINE_BEFORE_CHARACTER])) {
|
||||||
|
throw new \InvalidArgumentException('Only integer for option split_lines_before_characters is allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$characterPerLine = $opts[self::SPLIT_LINE_BEFORE_CHARACTER];
|
||||||
|
$exploded = explode(' ', $str);
|
||||||
|
$charOnLine = 0;
|
||||||
|
$str = '';
|
||||||
|
foreach ($exploded as $word) {
|
||||||
|
if ($charOnLine + strlen($word) > $characterPerLine) {
|
||||||
|
$str .= "\n";
|
||||||
|
$charOnLine = 0;
|
||||||
|
}
|
||||||
|
if ($charOnLine > 0) {
|
||||||
|
$str .= ' ';
|
||||||
|
}
|
||||||
|
$str .= $word;
|
||||||
|
$charOnLine += strlen($word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $str;
|
return $str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ class UserRenderTest extends TestCase
|
|||||||
public function testRenderUserWithJobAndScopeAtCertainDate(): void
|
public function testRenderUserWithJobAndScopeAtCertainDate(): void
|
||||||
{
|
{
|
||||||
// Create a user with a certain user job
|
// Create a user with a certain user job
|
||||||
|
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$userJobA = new UserJob();
|
$userJobA = new UserJob();
|
||||||
$scopeA = new Scope();
|
$scopeA = new Scope();
|
||||||
@ -106,4 +105,52 @@ class UserRenderTest extends TestCase
|
|||||||
$expectedStringC = 'BOB ISLA (directrice) (service B)';
|
$expectedStringC = 'BOB ISLA (directrice) (service B)';
|
||||||
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
|
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRenderStringWithSplitLines(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
// Create a user with a certain user job
|
||||||
|
$user = new User();
|
||||||
|
$userJobA = new UserJob();
|
||||||
|
$scopeA = new Scope();
|
||||||
|
|
||||||
|
$userJobA->setLabel(['fr' => 'assistant social en maison de service accompagné'])
|
||||||
|
->setActive(true);
|
||||||
|
$scopeA->setName(['fr' => 'service de l\'assistant professionnel']);
|
||||||
|
$user->setLabel('Robert Van Zorrizzeen Gorikke');
|
||||||
|
|
||||||
|
$userJobHistoryA = (new User\UserJobHistory())
|
||||||
|
->setUser($user)
|
||||||
|
->setJob($userJobA)
|
||||||
|
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||||
|
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||||
|
|
||||||
|
$userScopeHistoryA = (new User\UserScopeHistory())
|
||||||
|
->setUser($user)
|
||||||
|
->setScope($scopeA)
|
||||||
|
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||||
|
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||||
|
|
||||||
|
$user->getUserJobHistories()->add($userJobHistoryA);
|
||||||
|
$user->getUserScopeHistories()->add($userScopeHistoryA);
|
||||||
|
|
||||||
|
// Create renderer
|
||||||
|
$translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class);
|
||||||
|
$translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']);
|
||||||
|
|
||||||
|
$engineMock = $this->createMock(Environment::class);
|
||||||
|
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||||
|
$clock = new MockClock(new \DateTimeImmutable('2023-11-15 12:00:00'));
|
||||||
|
|
||||||
|
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
|
||||||
|
|
||||||
|
$actual = $renderer->renderString($user, ['split_lines_before_characters' => 30]);
|
||||||
|
self::assertEquals(<<<'STR'
|
||||||
|
Robert Van Zorrizzeen Gorikke
|
||||||
|
(assistant social en maison de
|
||||||
|
service accompagné) (service de
|
||||||
|
l'assistant professionnel)
|
||||||
|
STR, $actual);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,291 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow\Helper;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Workflow\DefinitionBuilder;
|
||||||
|
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||||
|
use Symfony\Component\Workflow\Workflow;
|
||||||
|
use Symfony\Component\Workflow\WorkflowInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class WorkflowRelatedEntityPermissionHelperTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataAllowedByWorkflowReadOperation
|
||||||
|
*
|
||||||
|
* @param list<EntityWorkflow> $entityWorkflows
|
||||||
|
*/
|
||||||
|
public function testAllowedByWorkflowRead(
|
||||||
|
array $entityWorkflows,
|
||||||
|
User $user,
|
||||||
|
string $expected,
|
||||||
|
?\DateTimeImmutable $atDate,
|
||||||
|
string $message,
|
||||||
|
): void {
|
||||||
|
// all entities must have this workflow name, so we are ok to set it here
|
||||||
|
foreach ($entityWorkflows as $entityWorkflow) {
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
}
|
||||||
|
$helper = $this->buildHelper($entityWorkflows, $user, $atDate);
|
||||||
|
|
||||||
|
self::assertEquals($expected, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideDataAllowedByWorkflowWriteOperation
|
||||||
|
*
|
||||||
|
* @param list<EntityWorkflow> $entityWorkflows
|
||||||
|
*/
|
||||||
|
public function testAllowedByWorkflowWrite(
|
||||||
|
array $entityWorkflows,
|
||||||
|
User $user,
|
||||||
|
string $expected,
|
||||||
|
?\DateTimeImmutable $atDate,
|
||||||
|
string $message,
|
||||||
|
): void {
|
||||||
|
// all entities must have this workflow name, so we are ok to set it here
|
||||||
|
foreach ($entityWorkflows as $entityWorkflow) {
|
||||||
|
$entityWorkflow->setWorkflowName('dummy');
|
||||||
|
}
|
||||||
|
$helper = $this->buildHelper($entityWorkflows, $user, $atDate);
|
||||||
|
|
||||||
|
self::assertEquals($expected, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoWorkflow(): void
|
||||||
|
{
|
||||||
|
$helper = $this->buildHelper([], new User(), null);
|
||||||
|
|
||||||
|
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForWriteOperation(new \stdClass()));
|
||||||
|
self::assertEquals(WorkflowRelatedEntityPermissionHelper::ABSTAIN, $helper->isAllowedByWorkflowForReadOperation(new \stdClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<EntityWorkflow> $entityWorkflows
|
||||||
|
*/
|
||||||
|
private function buildHelper(array $entityWorkflows, User $user, ?\DateTimeImmutable $atDateTime): WorkflowRelatedEntityPermissionHelper
|
||||||
|
{
|
||||||
|
$security = $this->prophesize(Security::class);
|
||||||
|
$security->getUser()->willReturn($user);
|
||||||
|
|
||||||
|
$entityWorkflowManager = $this->prophesize(EntityWorkflowManager::class);
|
||||||
|
$entityWorkflowManager->findByRelatedEntity(Argument::type('object'))->willReturn($entityWorkflows);
|
||||||
|
|
||||||
|
return new WorkflowRelatedEntityPermissionHelper($security->reveal(), $entityWorkflowManager->reveal(), $this->buildRegistry(), new MockClock($atDateTime ?? new \DateTimeImmutable()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideDataAllowedByWorkflowReadOperation(): iterable
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||||
|
'abstain because the user is not present as a dest user'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
|
'force grant because the user is a current user'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
|
'force grant because the user was a previous user'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futurePersonSignatures[] = new Person();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
|
'force grant because there is a signature for person'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futurePersonSignatures[] = new Person();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
|
||||||
|
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable('2024-01-01T12:00:00'));
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable('2024-01-10T12:00:00'),
|
||||||
|
'abstain because there is a signature for person, already signed, and for a long time ago'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futurePersonSignatures[] = new Person();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
|
||||||
|
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)
|
||||||
|
->setStateDate(new \DateTimeImmutable('2024-01-01T12:00:00'));
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::FORCE_GRANT,
|
||||||
|
new \DateTimeImmutable('2024-01-01T12:30:00'),
|
||||||
|
'force grant because there is a signature for person, already signed, a short time ago'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideDataAllowedByWorkflowWriteOperation(): iterable
|
||||||
|
{
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], new User(), WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||||
|
'abstain because the user is not present as a dest user'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
|
'force grant because the user is a current user'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
|
'force grant because the user was a previous user'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$entityWorkflow->setStep('final_positive', $dto, 'to_final_positive', new \DateTimeImmutable(), new User());
|
||||||
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
||||||
|
'force denied: user was a previous user, but it is finalized positive'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable());
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable());
|
||||||
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||||
|
'abstain: user was a previous user, it is finalized, but finalized negative'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futurePersonSignatures[] = new Person();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
|
||||||
|
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_DENIED, new \DateTimeImmutable(),
|
||||||
|
'force denied because there is a signature'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futurePersonSignatures[] = new Person();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::FORCE_GRANT, new \DateTimeImmutable(),
|
||||||
|
'force grant: there is a signature, but still pending'];
|
||||||
|
|
||||||
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futureDestUsers[] = $user = new User();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$dto->futurePersonSignatures[] = new Person();
|
||||||
|
$entityWorkflow->setStep('test', $dto, 'to_test', new \DateTimeImmutable(), new User());
|
||||||
|
$signature = $entityWorkflow->getCurrentStep()->getSignatures()->first();
|
||||||
|
$signature->setState(EntityWorkflowSignatureStateEnum::SIGNED)->setStateDate(new \DateTimeImmutable());
|
||||||
|
$dto = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
|
$entityWorkflow->setStep('final_negative', $dto, 'to_final_negative', new \DateTimeImmutable(), new User());
|
||||||
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
|
|
||||||
|
yield [[$entityWorkflow], $user, WorkflowRelatedEntityPermissionHelper::ABSTAIN, new \DateTimeImmutable(),
|
||||||
|
'abstain: there is a signature on a canceled workflow'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function buildRegistry(): Registry
|
||||||
|
{
|
||||||
|
$builder = new DefinitionBuilder();
|
||||||
|
$builder
|
||||||
|
->setInitialPlaces(['initial'])
|
||||||
|
->addPlaces(['initial', 'test', 'final_positive', 'final_negative'])
|
||||||
|
->setMetadataStore(
|
||||||
|
new InMemoryMetadataStore(
|
||||||
|
placesMetadata: [
|
||||||
|
'final_positive' => [
|
||||||
|
'isFinal' => true,
|
||||||
|
'isFinalPositive' => true,
|
||||||
|
],
|
||||||
|
'final_negative' => [
|
||||||
|
'isFinal' => true,
|
||||||
|
'isFinalPositive' => false,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$workflow = new Workflow($builder->build(), new EntityWorkflowMarkingStore(), name: 'dummy');
|
||||||
|
$registry = new Registry();
|
||||||
|
$registry->addWorkflow($workflow, new class () implements WorkflowSupportStrategyInterface {
|
||||||
|
public function supports(WorkflowInterface $workflow, object $subject): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Workflow\Helper;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
use Symfony\Component\Workflow\Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
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 Registry $registry,
|
||||||
|
private readonly ClockInterface $clock,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||||
|
*/
|
||||||
|
public function isAllowedByWorkflowForReadOperation(object $entity): string
|
||||||
|
{
|
||||||
|
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||||
|
|
||||||
|
if ($this->isUserInvolvedInAWorkflow($entityWorkflows)) {
|
||||||
|
return self::FORCE_GRANT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// give a view permission if there is a Person signature pending, or in the 12 hours following
|
||||||
|
// the signature last state
|
||||||
|
foreach ($entityWorkflows as $workflow) {
|
||||||
|
foreach ($workflow->getCurrentStep()->getSignatures() as $signature) {
|
||||||
|
if ('person' === $signature->getSignerKind()) {
|
||||||
|
if (EntityWorkflowSignatureStateEnum::PENDING === $signature->getState()) {
|
||||||
|
return self::FORCE_GRANT;
|
||||||
|
}
|
||||||
|
$signatureDate = $signature->getStateDate();
|
||||||
|
$visibleUntil = $signatureDate->add(new \DateInterval('PT12H'));
|
||||||
|
|
||||||
|
if ($visibleUntil > $this->clock->now()) {
|
||||||
|
return self::FORCE_GRANT;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::ABSTAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 'FORCE_GRANT'|'FORCE_DENIED'|'ABSTAIN'
|
||||||
|
*/
|
||||||
|
public function isAllowedByWorkflowForWriteOperation(object $entity): string
|
||||||
|
{
|
||||||
|
$entityWorkflows = $this->entityWorkflowManager->findByRelatedEntity($entity);
|
||||||
|
$runningWorkflows = [];
|
||||||
|
|
||||||
|
// if a workflow is finalized positive, we are not allowed to edit to document any more
|
||||||
|
|
||||||
|
foreach ($entityWorkflows as $entityWorkflow) {
|
||||||
|
if ($entityWorkflow->isFinal()) {
|
||||||
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
|
$marking = $workflow->getMarkingStore()->getMarking($entityWorkflow);
|
||||||
|
foreach ($marking->getPlaces() as $place => $int) {
|
||||||
|
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place);
|
||||||
|
if (true === ($placeMetadata['isFinalPositive'] ?? false)) {
|
||||||
|
// the workflow is final, and final positive, so we stop here.
|
||||||
|
return self::FORCE_DENIED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$runningWorkflows[] = $entityWorkflow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a signature on a **running workflow**, no one can edit the workflow any more
|
||||||
|
foreach ($runningWorkflows as $entityWorkflow) {
|
||||||
|
foreach ($entityWorkflow->getSteps() as $step) {
|
||||||
|
foreach ($step->getSignatures() as $signature) {
|
||||||
|
if (EntityWorkflowSignatureStateEnum::SIGNED === $signature->getState()) {
|
||||||
|
return self::FORCE_DENIED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow only the users involved
|
||||||
|
if ($this->isUserInvolvedInAWorkflow($runningWorkflows)) {
|
||||||
|
return self::FORCE_GRANT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::ABSTAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<EntityWorkflow> $entityWorkflows
|
||||||
|
*/
|
||||||
|
private function isUserInvolvedInAWorkflow(array $entityWorkflows): bool
|
||||||
|
{
|
||||||
|
$currentUser = $this->security->getUser();
|
||||||
|
|
||||||
|
foreach ($entityWorkflows as $entityWorkflow) {
|
||||||
|
// so, the workflow is running... We return true if the current user is involved
|
||||||
|
foreach ($entityWorkflow->getSteps() as $step) {
|
||||||
|
if ($step->getAllDestUser()->contains($currentUser)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ namespace Chill\PersonBundle\Security\Authorization\StoredObjectVoter;
|
|||||||
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\DocStoreBundle\Service\WorkflowStoredObjectPermissionHelper;
|
use Chill\MainBundle\Workflow\Helper\WorkflowRelatedEntityPermissionHelper;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
|
||||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
|
||||||
@ -25,7 +25,7 @@ class AccompanyingPeriodWorkEvaluationDocumentStoredObjectVoter extends Abstract
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
private readonly AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
|
||||||
Security $security,
|
Security $security,
|
||||||
WorkflowStoredObjectPermissionHelper $workflowDocumentService,
|
WorkflowRelatedEntityPermissionHelper $workflowDocumentService,
|
||||||
) {
|
) {
|
||||||
parent::__construct($security, $workflowDocumentService);
|
parent::__construct($security, $workflowDocumentService);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user