create voter which blocks deletion if a workflow exists

This commit is contained in:
Julien Fastré 2022-03-07 00:14:22 +01:00
parent c7f2eedd4b
commit 276b5ea394
11 changed files with 227 additions and 6 deletions

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Workflow; namespace Chill\DocStoreBundle\Workflow;
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument; use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
use Chill\DocStoreBundle\Security\Authorization\AccompanyingCourseDocumentVoter;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
@ -36,6 +37,13 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
$this->translator = $translator; $this->translator = $translator;
} }
public function getDeletionRoles(): array
{
return [
AccompanyingCourseDocumentVoter::DELETE,
];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{ {
$course = $this->getRelatedEntity($entityWorkflow) $course = $this->getRelatedEntity($entityWorkflow)
@ -66,6 +74,18 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
return $this->repository->find($entityWorkflow->getRelatedEntityId()); return $this->repository->find($entityWorkflow->getRelatedEntityId());
} }
/**
* @param AccompanyingCourseDocument $object
*
* @return array[]
*/
public function getRelatedObjects(object $object): array
{
return [
['entityClass' => AccompanyingCourseDocument::class, 'entityId' => $object->getId()],
];
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{ {
return null; return null;
@ -84,6 +104,11 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
]; ];
} }
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingCourseDocument;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{ {
return $entityWorkflow->getRelatedEntityClass() === AccompanyingCourseDocument::class; return $entityWorkflow->getRelatedEntityClass() === AccompanyingCourseDocument::class;

View File

@ -256,6 +256,13 @@ class ChillMainExtension extends Extension implements
'channels' => ['chill'], 'channels' => ['chill'],
]); ]);
$container->prependExtensionConfig('security', [
'access_decision_manager' => [
'strategy' => 'unanimous',
'allow_if_all_abstain' => false,
],
]);
//add crud api //add crud api
$this->prependCruds($container); $this->prependCruds($container);
} }

View File

@ -55,6 +55,30 @@ class EntityWorkflowRepository implements ObjectRepository
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
} }
public function countRelatedWorkflows(array $relateds): int
{
$qb = $this->repository->createQueryBuilder('w');
$orX = $qb->expr()->orX();
$i = 0;
foreach ($relateds as $related) {
$orX->add(
$qb->expr()->andX(
$qb->expr()->eq('w.relatedEntityClass', ':entity_class_' . $i),
$qb->expr()->eq('w.relatedEntityId', ':entity_id_' . $i)
)
);
$qb
->setParameter('entity_class_' . $i, $related['entityClass'])
->setParameter('entity_id_' . $i, $related['entityId']);
++$i;
}
$qb->where($orX);
return $qb->select('COUNT(w)')->getQuery()->getSingleScalarResult();
}
public function find($id): ?EntityWorkflow public function find($id): ?EntityWorkflow
{ {
return $this->repository->find($id); return $this->repository->find($id);

View File

@ -0,0 +1,65 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use RuntimeException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use function in_array;
use function is_object;
class WorkflowEntityDeletionVoter extends Voter
{
private EntityWorkflowRepository $entityWorkflowRepository;
/**
* @var iterable|EntityWorkflowHandlerInterface[]
*/
private iterable $handlers;
public function __construct($handlers, EntityWorkflowRepository $entityWorkflowRepository)
{
$this->handlers = $handlers;
$this->entityWorkflowRepository = $entityWorkflowRepository;
}
protected function supports($attribute, $subject)
{
if (!is_object($subject)) {
return false;
}
foreach ($this->handlers as $handler) {
if ($handler->isObjectSupported($subject)
&& in_array($attribute, $handler->getDeletionRoles($subject), true)) {
return true;
}
}
return false;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
foreach ($this->handlers as $handler) {
if ($handler->isObjectSupported($subject)) {
return 0 === $this->entityWorkflowRepository->countRelatedWorkflows(
$handler->getRelatedObjects($subject)
);
}
}
throw new RuntimeException('no handlers found');
}
}

View File

@ -15,12 +15,19 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
interface EntityWorkflowHandlerInterface interface EntityWorkflowHandlerInterface
{ {
/**
* @return array|string[]
*/
public function getDeletionRoles(): array;
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array; public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array;
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string; public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object; public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
public function getRelatedObjects(object $object): array;
/** /**
* Return a string representing the role required for seeing the workflow. * Return a string representing the role required for seeing the workflow.
* *
@ -33,6 +40,8 @@ interface EntityWorkflowHandlerInterface
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array; public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array;
public function isObjectSupported(object $object): bool;
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool; public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool; public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;

View File

@ -75,3 +75,9 @@ services:
$locker: '@Chill\MainBundle\Security\PasswordRecover\PasswordRecoverLocker' $locker: '@Chill\MainBundle\Security\PasswordRecover\PasswordRecoverLocker'
tags: tags:
- { name: security.voter } - { name: security.voter }
Chill\MainBundle\Security\Authorization\WorkflowEntityDeletionVoter:
autoconfigure: true
autowire: true
arguments:
$handlers: !tagged_iterator chill_main.workflow_handler

View File

@ -130,11 +130,13 @@
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}" href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"
></a> ></a>
</li> </li>
<li> {% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE', w) %}
<a class="btn btn-delete" title="{{ 'Delete'|trans }}" <li>
href="{{ path('chill_person_accompanying_period_work_delete', { 'id': w.id } ) }}" <a class="btn btn-delete" title="{{ 'Delete'|trans }}"
></a> href="{{ path('chill_person_accompanying_period_work_delete', { 'id': w.id } ) }}"
</li> ></a>
</li>
{% endif %}
</ul> </ul>
{% endif %} {% endif %}
</div> </div>

View File

@ -24,6 +24,8 @@ class AccompanyingPeriodWorkVoter extends Voter
{ {
public const CREATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE'; public const CREATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE';
public const DELETE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE';
public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE'; public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE';
public const UPDATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE'; public const UPDATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE';
@ -60,6 +62,7 @@ class AccompanyingPeriodWorkVoter extends Voter
case self::CREATE: case self::CREATE:
case self::UPDATE: case self::UPDATE:
case self::DELETE:
return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $subject->getAccompanyingPeriod()); return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $subject->getAccompanyingPeriod());
default: default:
@ -86,6 +89,6 @@ class AccompanyingPeriodWorkVoter extends Voter
private function getRoles(): array private function getRoles(): array
{ {
return [self::SEE, self::CREATE, self::UPDATE]; return [self::SEE, self::CREATE, self::UPDATE, self::DELETE];
} }
} }

View File

@ -37,6 +37,13 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
$this->translator = $translator; $this->translator = $translator;
} }
public function getDeletionRoles(): array
{
return [
'_',
];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{ {
$doc = $this->getRelatedEntity($entityWorkflow); $doc = $this->getRelatedEntity($entityWorkflow);
@ -63,6 +70,18 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
return $this->repository->find($entityWorkflow->getRelatedEntityId()); return $this->repository->find($entityWorkflow->getRelatedEntityId());
} }
/**
* @param AccompanyingPeriodWorkEvaluationDocument $object
*
* @return array[]
*/
public function getRelatedObjects(object $object): array
{
return [
['entityClass' => AccompanyingPeriodWorkEvaluationDocument::class, $object->getId()],
];
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{ {
return AccompanyingPeriodWorkEvaluationDocumentVoter::SEE; return AccompanyingPeriodWorkEvaluationDocumentVoter::SEE;
@ -84,6 +103,11 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
]; ];
} }
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingPeriodWorkEvaluationDocument;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{ {
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluationDocument::class; return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluationDocument::class;

View File

@ -15,6 +15,7 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@ -37,6 +38,11 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
$this->translator = $translator; $this->translator = $translator;
} }
public function getDeletionRoles(): array
{
return ['_'];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{ {
$evaluation = $this->getRelatedEntity($entityWorkflow); $evaluation = $this->getRelatedEntity($entityWorkflow);
@ -61,6 +67,20 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
return $this->repository->find($entityWorkflow->getRelatedEntityId()); return $this->repository->find($entityWorkflow->getRelatedEntityId());
} }
/**
* @param AccompanyingPeriodWorkEvaluation $object
*/
public function getRelatedObjects(object $object): array
{
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluation::class, 'entityId' => $object->getId()];
foreach ($object->getDocuments() as $doc) {
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluationDocument::class, 'entityId' => $doc->getId()];
}
return $relateds;
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{ {
return AccompanyingPeriodWorkEvaluationVoter::SEE; return AccompanyingPeriodWorkEvaluationVoter::SEE;
@ -79,6 +99,11 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
]; ];
} }
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingPeriodWorkEvaluation;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{ {
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluation::class; return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluation::class;

View File

@ -15,7 +15,10 @@ use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface
@ -36,6 +39,11 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
$this->translator = $translator; $this->translator = $translator;
} }
public function getDeletionRoles(): array
{
return [AccompanyingPeriodWorkVoter::DELETE];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{ {
return [ return [
@ -58,6 +66,24 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
return $this->repository->find($entityWorkflow->getRelatedEntityId()); return $this->repository->find($entityWorkflow->getRelatedEntityId());
} }
/**
* @param AccompanyingPeriodWork $object
*/
public function getRelatedObjects(object $object): array
{
$relateds[] = ['entityClass' => AccompanyingPeriodWork::class, 'entityId' => $object->getId()];
foreach ($object->getAccompanyingPeriodWorkEvaluations() as $evaluation) {
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluation::class, 'entityId' => $evaluation->getId()];
foreach ($evaluation->getDocuments() as $doc) {
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluationDocument::class, 'entityId' => $doc->getId()];
}
}
return $relateds;
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{ {
return null; return null;
@ -76,6 +102,11 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
]; ];
} }
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingPeriodWork;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{ {
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWork::class; return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWork::class;