From 9f1afb8423679e7c703dfb3935458406e6dd41ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 13 Sep 2024 17:04:57 +0200 Subject: [PATCH] Add access controls and permissions for signature steps Implemented a Voter to enforce permissions on signature steps, ensuring only authorized users can sign steps. Updated relevant controllers and templates to reflect these permissions, and added corresponding tests to validate the changes. --- .../Controller/SignatureRequestController.php | 6 +++ .../WorkflowAddSignatureController.php | 8 ++++ .../Entity/Workflow/EntityWorkflow.php | 6 ++- .../views/Workflow/_signature.html.twig | 20 +++++---- .../EntityWorkflowStepSignatureVoter.php | 41 +++++++++++++++++++ .../Entity/ChillEntityRenderManager.php | 4 +- .../WorkflowAddSignatureControllerTest.php | 8 +++- .../translations/messages.fr.yml | 3 +- 8 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowStepSignatureVoter.php diff --git a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php index 26fc1b098..c6c564be1 100644 --- a/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php +++ b/src/Bundle/ChillDocStoreBundle/Controller/SignatureRequestController.php @@ -18,10 +18,12 @@ use Chill\DocStoreBundle\Service\StoredObjectManagerInterface; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Templating\Entity\ChillEntityRenderManagerInterface; +use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; @@ -41,6 +43,10 @@ class SignatureRequestController #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')] public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse { + if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) { + throw new AccessDeniedHttpException('not authorized to sign this step'); + } + $entityWorkflow = $signature->getStep()->getEntityWorkflow(); if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) { diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php index deb90119b..2aa180b7a 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowAddSignatureController.php @@ -14,13 +14,16 @@ namespace Chill\MainBundle\Controller; use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; +use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Twig\Environment; @@ -32,11 +35,16 @@ final readonly class WorkflowAddSignatureController private NormalizerInterface $normalizer, private Environment $twig, private UrlGeneratorInterface $urlGenerator, + private Security $security, ) {} #[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')] public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response { + if (!$this->security->isGranted(EntityWorkflowStepSignatureVoter::SIGN, $signature)) { + throw new AccessDeniedHttpException('not authorized to sign this step'); + } + $entityWorkflow = $signature->getStep()->getEntityWorkflow(); if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) { diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php index 08b5ee41f..89efde7c0 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -318,7 +318,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface } } - return $usersInvolved; + return array_values($usersInvolved); } public function getWorkflowName(): string @@ -446,6 +446,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface $newStep->addDestUser($user); } + if (null !== $transitionContextDTO->futureUserSignature) { + $newStep->addDestUser($transitionContextDTO->futureUserSignature); + } + foreach ($transitionContextDTO->futureDestEmails as $email) { $newStep->addDestEmail($email); } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig index 50172089c..b0e401645 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_signature.html.twig @@ -23,14 +23,18 @@ {% if s.isSigned %} {{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }} {% else %} - + {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %} + + {% else %} + {{ 'workflow.waiting_for_signature'|trans }} + {% endif %} {% endif %} diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowStepSignatureVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowStepSignatureVoter.php new file mode 100644 index 000000000..683f1352f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowStepSignatureVoter.php @@ -0,0 +1,41 @@ +getSigner() instanceof Person) { + return true; + } + + if ($subject->getSigner() === $token->getUser()) { + return true; + } + + return false; + } +} diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/ChillEntityRenderManager.php b/src/Bundle/ChillMainBundle/Templating/Entity/ChillEntityRenderManager.php index 51315a6d6..daca1fb8a 100644 --- a/src/Bundle/ChillMainBundle/Templating/Entity/ChillEntityRenderManager.php +++ b/src/Bundle/ChillMainBundle/Templating/Entity/ChillEntityRenderManager.php @@ -18,8 +18,8 @@ final readonly class ChillEntityRenderManager implements ChillEntityRenderManage public function __construct(/** * @var iterable */ - private iterable $renders) - { + private iterable $renders, + ) { $this->defaultRender = new ChillEntityRender(); } diff --git a/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php b/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php index 4b6fd38e2..9b4507936 100644 --- a/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Controller/WorkflowAddSignatureControllerTest.php @@ -18,12 +18,14 @@ use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable; use Chill\MainBundle\Controller\WorkflowAddSignatureController; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; +use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter; use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Chill\PersonBundle\Entity\Person; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Twig\Environment; @@ -65,7 +67,11 @@ class WorkflowAddSignatureControllerTest extends TestCase $urlGenerator = $this->createMock(UrlGeneratorInterface::class); - $controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig, $urlGenerator); + $security = $this->createMock(Security::class); + $security->expects($this->once())->method('isGranted')->with(EntityWorkflowStepSignatureVoter::SIGN, $signature) + ->willReturn(true); + + $controller = new WorkflowAddSignatureController($entityWorkflowManager, $pdfSignatureZoneAvailable, $normalizer, $twig, $urlGenerator, $security); $actual = $controller($signature, new Request()); diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 0c0fce51c..003dcbee5 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -531,9 +531,10 @@ workflow: Remove hold: Enlever la mise en attente On hold: En attente Automated transition: Transition automatique + waiting_for_signature: En attente de signature signature_zone: - title: Appliquer les signatures électroniques + title: Signatures électroniques button_sign: Signer metadata: sign_by: 'Signature pour %name%'