Skip to content

Commit

Permalink
Restrict publication (#910)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois authored Aug 8, 2024
1 parent 1a4d4aa commit 9c63f39
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 27 deletions.
28 changes: 28 additions & 0 deletions src/Domain/User/Specification/CanUserPublishRegulation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Specification;

use App\Domain\Regulation\RegulationOrderRecord;
use App\Domain\User\Enum\OrganizationRolesEnum;
use App\Infrastructure\Security\SymfonyUser;

class CanUserPublishRegulation
{
public function isSatisfiedBy(RegulationOrderRecord $regulationOrderRecord, SymfonyUser $user): bool
{
$organization = $regulationOrderRecord->getOrganization();

foreach ($user->getOrganizationUsers() as $userOrganization) {
if ($userOrganization->uuid !== $organization->getUuid()) {
continue;
}

return \in_array(OrganizationRolesEnum::ROLE_ORGA_ADMIN->value, $userOrganization->roles)
|| \in_array(OrganizationRolesEnum::ROLE_ORGA_PUBLISHER->value, $userOrganization->roles);
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public function __invoke(Request $request, string $uuid): Response
context: [
'measure' => MeasureView::fromEntity($measure),
'regulationOrderRecordUuid' => $uuid,
'regulationOrderRecord' => $regulationOrderRecord,
'generalInfo' => $generalInfo,
'canDelete' => ($regulationOrderRecord->countMeasures() + 1) > 1,
'preExistingMeasureUuids' => $preExistingMeasureUuids,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Application\Regulation\Command\PublishRegulationCommand;
use App\Domain\Regulation\Exception\RegulationOrderRecordCannotBePublishedException;
use App\Domain\Regulation\Specification\CanOrganizationAccessToRegulation;
use App\Infrastructure\Security\Voter\RegulationOrderRecordVoter;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -49,6 +50,10 @@ public function __invoke(Request $request, string $uuid): Response

$regulationOrderRecord = $this->getRegulationOrderRecord($uuid);

if (!$this->security->isGranted(RegulationOrderRecordVoter::PUBLISH, $regulationOrderRecord)) {
throw new AccessDeniedHttpException();
}

try {
$this->commandBus->handle(new PublishRegulationCommand($regulationOrderRecord));
} catch (RegulationOrderRecordCannotBePublishedException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use App\Application\QueryBusInterface;
use App\Application\Regulation\Query\GetGeneralInfoQuery;
use App\Application\Regulation\Query\GetOrganizationUuidByRegulationOrderRecordQuery;
use App\Application\Regulation\Query\Measure\GetMeasuresQuery;
use App\Application\Regulation\View\GeneralInfoView;
use App\Domain\Regulation\ArrayRegulationMeasures;
Expand Down Expand Up @@ -55,7 +54,8 @@ public function __invoke(string $uuid): Response
throw new AccessDeniedHttpException();
}

$organizationUuid = $this->queryBus->handle(new GetOrganizationUuidByRegulationOrderRecordQuery($uuid));
$regulationOrderRecord = $this->getRegulationOrderRecord($uuid, requireUserSameOrg: false);
$organizationUuid = $regulationOrderRecord->getOrganizationUuid();
$measures = $this->queryBus->handle(new GetMeasuresQuery($uuid));
$isReadOnly = !($currentUser && $this->canOrganizationAccessToRegulation->isSatisfiedBy($organizationUuid, $currentUser->getOrganizationUuids()));

Expand All @@ -67,6 +67,7 @@ public function __invoke(string $uuid): Response
'isReadOnly' => $isReadOnly,
'generalInfo' => $generalInfo,
'measures' => $measures,
'regulationOrderRecord' => $regulationOrderRecord,
];

return new Response(
Expand Down
51 changes: 51 additions & 0 deletions src/Infrastructure/Security/Voter/RegulationOrderRecordVoter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Security\Voter;

use App\Domain\Regulation\RegulationOrderRecord;
use App\Domain\User\Specification\CanUserPublishRegulation;
use App\Infrastructure\Security\SymfonyUser;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

final class RegulationOrderRecordVoter extends Voter
{
public function __construct(
private readonly CanUserPublishRegulation $canUserPublishRegulation,
) {
}

public const PUBLISH = 'publish';

protected function supports(string $attribute, mixed $subject): bool
{
if (!\in_array($attribute, [self::PUBLISH])) {
return false;
}

if (!$subject instanceof RegulationOrderRecord) {
return false;
}

return true;
}

protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();

if (!$user instanceof SymfonyUser) {
return false;
}

/** @var RegulationOrderRecord $regulationOrderRecord */
$regulationOrderRecord = $subject;

return match ($attribute) {
self::PUBLISH => $this->canUserPublishRegulation->isSatisfiedBy($regulationOrderRecord, $user),
default => throw new \LogicException('This code should not be reached!')
};
}
}
20 changes: 11 additions & 9 deletions templates/regulation/detail.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
{% if isDraft %}
<li>
<div id="block_publication">
{% include 'regulation/fragments/_publication_button.html.twig' with { canPublish, uuid: generalInfo.uuid } only %}
{% include 'regulation/fragments/_publication_button.html.twig' with { canPublish, uuid: generalInfo.uuid, regulationOrderRecord } only %}
</div>
</li>
{% endif %}
Expand Down Expand Up @@ -127,14 +127,16 @@
{{ parent() }}
{% if not isReadOnly %}
{% if isDraft %}
{% include "common/confirmation_modal.html.twig" with {
id: 'regulation-publish-modal',
title: 'regulation.publish_modal.title'|trans,
buttons: [
{ label: 'common.publish'|trans, attr: {type: 'submit', class: 'fr-btn'} },
{ label: 'common.do_not_publish'|trans, attr: {value: 'close', class: 'fr-btn fr-btn--secondary'} },
],
} only %}
{% if is_granted(constant('App\\Infrastructure\\Security\\Voter\\RegulationOrderRecordVoter::PUBLISH'), regulationOrderRecord) %}
{% include "common/confirmation_modal.html.twig" with {
id: 'regulation-publish-modal',
title: 'regulation.publish_modal.title'|trans,
buttons: [
{ label: 'common.publish'|trans, attr: {type: 'submit', class: 'fr-btn'} },
{ label: 'common.do_not_publish'|trans, attr: {value: 'close', class: 'fr-btn fr-btn--secondary'} },
],
} only %}
{% endif %}

{% include "common/confirmation_modal.html.twig" with {
id: 'measure-delete-modal',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@

<turbo-stream action="update" target="block_publication">
<template>
{% include 'regulation/fragments/_publication_button.html.twig' with { canPublish: true, uuid: regulationOrderRecordUuid } only %}
{% include 'regulation/fragments/_publication_button.html.twig' with { canPublish: true, uuid: regulationOrderRecordUuid, regulationOrderRecord } only %}
</template>
</turbo-stream>
26 changes: 14 additions & 12 deletions templates/regulation/fragments/_publication_button.html.twig
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{% if canPublish %}
{% set publishCsrfToken = csrf_token('publish-regulation') %}
<form method="post" action="{{ path('app_regulation_publish', { uuid }) }}" data-controller="form-submit" data-action="modal-trigger:submit->form-submit#submit">
<d-modal-trigger modal="regulation-publish-modal" submitValue="publish">
<button class="fr-btn fr-btn--secondary" aria-controls="regulation-publish-modal">
{{ 'common.publish'|trans }}
</button>
{% if is_granted(constant('App\\Infrastructure\\Security\\Voter\\RegulationOrderRecordVoter::PUBLISH'), regulationOrderRecord) %}
{% if canPublish %}
{% set publishCsrfToken = csrf_token('publish-regulation') %}
<form method="post" action="{{ path('app_regulation_publish', { uuid }) }}" data-controller="form-submit" data-action="modal-trigger:submit->form-submit#submit">
<d-modal-trigger modal="regulation-publish-modal" submitValue="publish">
<button class="fr-btn fr-btn--secondary" aria-controls="regulation-publish-modal">
{{ 'common.publish'|trans }}
</button>

<input type="hidden" name="token" value="{{ publishCsrfToken }}"/>
</d-modal-trigger>
</form>
{% else %}
<button class="fr-btn fr-btn--secondary" disabled>{{ 'common.publish'|trans }}</button>
<input type="hidden" name="token" value="{{ publishCsrfToken }}"/>
</d-modal-trigger>
</form>
{% else %}
<button class="fr-btn fr-btn--secondary" disabled>{{ 'common.publish'|trans }}</button>
{% endif %}
{% endif %}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class PublishRegulationControllerTest extends AbstractWebTestCase

public function testPublish(): void
{
$client = $this->login();
$client = $this->login(UserFixture::MAIN_ORG_ADMIN_EMAIL);
$client->request('POST', '/regulations/' . RegulationOrderRecordFixture::UUID_TYPICAL . '/publish', [
'token' => $this->generateCsrfToken($client, 'publish-regulation'),
]);
Expand All @@ -25,6 +25,16 @@ public function testPublish(): void
$this->assertRouteSame('app_regulation_detail', ['uuid' => RegulationOrderRecordFixture::UUID_TYPICAL]);
}

public function testPublishAsContributor(): void
{
$client = $this->login();
$client->request('POST', '/regulations/' . RegulationOrderRecordFixture::UUID_TYPICAL . '/publish', [
'token' => $this->generateCsrfToken($client, 'publish-regulation'),
]);

$this->assertResponseStatusCodeSame(403);
}

public function testCannotBePublishedBecauseNoMeasures(): void
{
$client = $this->login();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

final class RegulationDetailControllerTest extends AbstractWebTestCase
{
public function testDraftRegulationDetail(): void
public function testDraftRegulationDetailAsAdmin(): void
{
$client = $this->login();
$client = $this->login(UserFixture::MAIN_ORG_ADMIN_EMAIL);
$crawler = $client->request('GET', '/regulations/' . RegulationOrderRecordFixture::UUID_TYPICAL);

$this->assertSecurityHeaders();
Expand Down Expand Up @@ -82,6 +82,14 @@ public function testDraftRegulationDetail(): void
$this->assertSame('/regulations?tab=temporary', $goBackLink->extract(['href'])[0]);
}

public function testDraftRegulationDetailAsContributor(): void
{
$client = $this->login();
$crawler = $client->request('GET', '/regulations/' . RegulationOrderRecordFixture::UUID_TYPICAL);

$this->assertSame(0, $crawler->selectButton('Publier')->count());
}

public function testPermanentRegulationDetail(): void
{
$client = $this->login();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace App\Tests\Unit\Domain\User\Specification;

use App\Application\User\View\OrganizationView;
use App\Domain\Regulation\RegulationOrderRecord;
use App\Domain\User\Enum\OrganizationRolesEnum;
use App\Domain\User\Organization;
use App\Domain\User\Specification\CanUserPublishRegulation;
use App\Infrastructure\Security\SymfonyUser;
use PHPUnit\Framework\TestCase;

final class CanUserPublishRegulationTest extends TestCase
{
public function testCanPublish(): void
{
$organization = $this->createMock(Organization::class);
$organization
->expects(self::once())
->method('getUuid')
->willReturn('c1790745-b915-4fb5-96e7-79b104092a55');

$regulationOrderRecord = $this->createMock(RegulationOrderRecord::class);
$regulationOrderRecord
->expects(self::once())
->method('getOrganization')
->willReturn($organization);

$symfonyUser = $this->createMock(SymfonyUser::class);
$symfonyUser
->expects(self::once())
->method('getOrganizationUsers')
->willReturn([
new OrganizationView('c1790745-b915-4fb5-96e7-79b104092a55', 'Dialog', [OrganizationRolesEnum::ROLE_ORGA_PUBLISHER->value]),
]);

$pattern = new CanUserPublishRegulation();
$this->assertTrue($pattern->isSatisfiedBy($regulationOrderRecord, $symfonyUser));
}

public function testCantPublish(): void
{
$organization = $this->createMock(Organization::class);
$organization
->expects(self::once())
->method('getUuid')
->willReturn('c1790745-b915-4fb5-96e7-79b104092a55');

$regulationOrderRecord = $this->createMock(RegulationOrderRecord::class);
$regulationOrderRecord
->expects(self::once())
->method('getOrganization')
->willReturn($organization);

$symfonyUser = $this->createMock(SymfonyUser::class);
$symfonyUser
->expects(self::once())
->method('getOrganizationUsers')
->willReturn([
new OrganizationView('c1790745-b915-4fb5-96e7-79b104092a55', 'Dialog', [OrganizationRolesEnum::ROLE_ORGA_CONTRIBUTOR->value]),
]);

$pattern = new CanUserPublishRegulation();
$this->assertFalse($pattern->isSatisfiedBy($regulationOrderRecord, $symfonyUser));
}
}

0 comments on commit 9c63f39

Please sign in to comment.