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.
This commit is contained in:
Julien Fastré 2024-09-13 17:04:57 +02:00
parent 1494c7ecd7
commit 9f1afb8423
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
8 changed files with 83 additions and 13 deletions

View File

@ -18,10 +18,12 @@ use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; 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\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;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security; 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')] #[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
public function processSignature(EntityWorkflowStepSignature $signature, Request $request): JsonResponse 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(); $entityWorkflow = $signature->getStep()->getEntityWorkflow();
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) { if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {

View File

@ -14,13 +14,16 @@ namespace Chill\MainBundle\Controller;
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable; use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum; use Chill\MainBundle\Entity\Workflow\EntityWorkflowSignatureStateEnum;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature; use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment; use Twig\Environment;
@ -32,11 +35,16 @@ final readonly class WorkflowAddSignatureController
private NormalizerInterface $normalizer, private NormalizerInterface $normalizer,
private Environment $twig, private Environment $twig,
private UrlGeneratorInterface $urlGenerator, private UrlGeneratorInterface $urlGenerator,
private Security $security,
) {} ) {}
#[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')] #[Route(path: '/{_locale}/main/workflow/signature/{id}/sign', name: 'chill_main_workflow_signature_add', methods: 'GET')]
public function __invoke(EntityWorkflowStepSignature $signature, Request $request): Response 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(); $entityWorkflow = $signature->getStep()->getEntityWorkflow();
if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) { if (EntityWorkflowSignatureStateEnum::PENDING !== $signature->getState()) {

View File

@ -318,7 +318,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
} }
} }
return $usersInvolved; return array_values($usersInvolved);
} }
public function getWorkflowName(): string public function getWorkflowName(): string
@ -446,6 +446,10 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
$newStep->addDestUser($user); $newStep->addDestUser($user);
} }
if (null !== $transitionContextDTO->futureUserSignature) {
$newStep->addDestUser($transitionContextDTO->futureUserSignature);
}
foreach ($transitionContextDTO->futureDestEmails as $email) { foreach ($transitionContextDTO->futureDestEmails as $email) {
$newStep->addDestEmail($email); $newStep->addDestEmail($email);
} }

View File

@ -23,14 +23,18 @@
{% if s.isSigned %} {% if s.isSigned %}
<span class="text-end">{{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span> <span class="text-end">{{ 'workflow.signature_zone.has_signed_statement'|trans({ 'datetime' : s.stateDate }) }}</span>
{% else %} {% else %}
<ul class="record_actions slim"> {% if is_granted('CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN', s) %}
<li> <ul class="record_actions slim">
<a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a> <li>
{% if s.state is same as('signed') %} <a class="btn btn-misc" href="{{ chill_path_add_return_path('chill_main_workflow_signature_metadata', { 'signature_id': s.id}) }}"><i class="fa fa-pencil-square-o"></i> {{ 'workflow.signature_zone.button_sign'|trans }}</a>
<p class="updatedBy">{{ s.stateDate }}</p> {% if s.state is same as('signed') %}
{% endif %} <p class="updatedBy">{{ s.stateDate }}</p>
</li> {% endif %}
</ul> </li>
</ul>
{% else %}
<span class="text-end">{{ 'workflow.waiting_for_signature'|trans }}</span>
{% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -0,0 +1,41 @@
<?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\Security\Authorization;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class EntityWorkflowStepSignatureVoter extends Voter
{
public const SIGN = 'CHILL_MAIN_ENTITY_WORKFLOW_SIGNATURE_SIGN';
protected function supports(string $attribute, $subject)
{
return $subject instanceof EntityWorkflowStepSignature && self::SIGN === $attribute;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
{
/** @var EntityWorkflowStepSignature $subject */
if ($subject->getSigner() instanceof Person) {
return true;
}
if ($subject->getSigner() === $token->getUser()) {
return true;
}
return false;
}
}

View File

@ -18,8 +18,8 @@ final readonly class ChillEntityRenderManager implements ChillEntityRenderManage
public function __construct(/** public function __construct(/**
* @var iterable<ChillEntityRenderInterface> * @var iterable<ChillEntityRenderInterface>
*/ */
private iterable $renders) private iterable $renders,
{ ) {
$this->defaultRender = new ChillEntityRender(); $this->defaultRender = new ChillEntityRender();
} }

View File

@ -18,12 +18,14 @@ use Chill\DocStoreBundle\Service\Signature\PDFSignatureZoneAvailable;
use Chill\MainBundle\Controller\WorkflowAddSignatureController; use Chill\MainBundle\Controller\WorkflowAddSignatureController;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Security\Authorization\EntityWorkflowStepSignatureVoter;
use Chill\MainBundle\Workflow\EntityWorkflowManager; use Chill\MainBundle\Workflow\EntityWorkflowManager;
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO; use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Twig\Environment; use Twig\Environment;
@ -65,7 +67,11 @@ class WorkflowAddSignatureControllerTest extends TestCase
$urlGenerator = $this->createMock(UrlGeneratorInterface::class); $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()); $actual = $controller($signature, new Request());

View File

@ -531,9 +531,10 @@ workflow:
Remove hold: Enlever la mise en attente Remove hold: Enlever la mise en attente
On hold: En attente On hold: En attente
Automated transition: Transition automatique Automated transition: Transition automatique
waiting_for_signature: En attente de signature
signature_zone: signature_zone:
title: Appliquer les signatures électroniques title: Signatures électroniques
button_sign: Signer button_sign: Signer
metadata: metadata:
sign_by: 'Signature pour %name%' sign_by: 'Signature pour %name%'