Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Sécurité] Cloisonnement des rôles #798

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/Controller/SignalementListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class SignalementListController extends AbstractController
{
#[Route('/bo/signalements', name: 'app_signalement_list')]
public function signalements(
SignalementManager $signalementManager,
TerritoireRepository $territoireRepository,
): Response {
$territoires = $territoireRepository->findAll();
Expand Down
11 changes: 10 additions & 1 deletion src/Controller/SignalementMessageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Factory\MessageFactory;
use App\Manager\MessageManager;
use App\Manager\MessageThreadManager;
use App\Repository\InterventionRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
Expand All @@ -34,7 +35,16 @@ public function sendMessageToUsager(
MessageManager $messageManager,
ValidatorInterface $validator,
SerializerInterface $serializer,
InterventionRepository $interventionRepository,
): JsonResponse {
$entrepriseIntervention = $interventionRepository->findBySignalementAndEntreprise(
$signalement,
$entreprise
);
if (!$entrepriseIntervention || !$entrepriseIntervention->isAccepted()) {
return $this->json(['status' => 'denied'], Response::HTTP_FORBIDDEN);
}

$data = $request->request->all();
$messageThread = $messageThreadManager->createOrGet($signalement, $entreprise);
$message = $messageFactory->createInstanceFrom(
Expand All @@ -60,7 +70,6 @@ public function sendMessageToUsager(
public function sendMessageToEntreprise(
Request $request,
MessageThread $messageThread,
MessageThreadManager $messageThreadManager,
MessageFactory $messageFactory,
MessageManager $messageManager,
ValidatorInterface $validator,
Expand Down
16 changes: 11 additions & 5 deletions src/Controller/SignalementResolveController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace App\Controller;

use App\Entity\Intervention;
use App\Entity\Signalement;
use App\Entity\User;
use App\Event\InterventionEntrepriseResolvedEvent;
use App\Form\SignalementHistoryType;
use App\Manager\InterventionManager;
use App\Manager\SignalementManager;
use App\Repository\InterventionRepository;
use App\Security\Voter\InterventionVoter;
use App\Service\Mailer\MailerProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
Expand All @@ -34,18 +36,22 @@ public function index(
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
$signalement->updateUuidPublic();
$signalementManager->save($signalement);

$this->addFlash('success', 'Le traitement a été marqué comme effectué. Un email de suivi sera envoyé à l\'usager dans 30 jours.');

/** @var User $user */
$user = $this->getUser();
$userEntreprise = $user->getEntreprise();
/** @var Intervention $intervention */
$intervention = $interventionRepository->findBySignalementAndEntreprise(
$signalement,
$userEntreprise
);

$this->denyAccessUnlessGranted(InterventionVoter::RESOLVE, $intervention);

$signalement->updateUuidPublic();
$signalementManager->save($signalement);

$this->addFlash('success', 'Le traitement a été marqué comme effectué. Un e-mail de suivi sera envoyé à l\'usager dans 30 jours.');

$intervention->setResolvedByEntrepriseAt(new \DateTimeImmutable());
$interventionManager->save($intervention);

Expand Down
57 changes: 39 additions & 18 deletions src/Controller/SignalementViewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use App\Repository\InterventionRepository;
use App\Repository\MessageThreadRepository;
use App\Security\Voter\FileVoter;
use App\Security\Voter\InterventionVoter;
use App\Security\Voter\SignalementVoter;
use App\Service\Mailer\MailerProvider;
use App\Service\Upload\UploadHandlerService;
use Doctrine\Common\Collections\Collection;
Expand Down Expand Up @@ -54,6 +56,16 @@ public function indexSignalement(
$entrepriseIntervention = null;
if (!$this->isGranted('ROLE_ADMIN')) {
$userEntreprise = $user->getEntreprise();

// Block if signalement historique and was created by other entreprise
if ($signalement->getEntreprise() && $signalement->getEntreprise() !== $user->getEntreprise()) {
return $this->render('signalement_view/not-found.html.twig');
}
// Block if signalement is not on same territory as entreprise
if (!$user->getEntreprise()->getTerritoires()->contains($signalement->getTerritoire())) {
return $this->render('signalement_view/not-found.html.twig');
}

$entrepriseIntervention = $interventionRepository->findBySignalementAndEntreprise(
$signalement,
$userEntreprise
Expand Down Expand Up @@ -91,7 +103,7 @@ public function indexSignalement(
'can_display_traitement' => null !== $signalement->getTypeIntervention(),
'can_display_messages' => !$this->isGranted('ROLE_ADMIN') && $entrepriseIntervention && $entrepriseIntervention->isAccepted(),
'can_display_adresse' => $this->isGranted('ROLE_ADMIN') || ($entrepriseIntervention && $entrepriseIntervention->isAcceptedByUsager()),
'can_send_estimation' => !$this->isGranted('ROLE_ADMIN') && $entrepriseIntervention && $entrepriseIntervention->isAccepted(),
'can_send_estimation' => $this->isGranted(InterventionVoter::SEND_ESTIMATION, $entrepriseIntervention),
'has_sent_estimation' => !$this->isGranted('ROLE_ADMIN') && $entrepriseIntervention && $entrepriseIntervention->getEstimationSentAt(),
'has_other_entreprise' => \count($acceptedEstimations) > 0 && !$entrepriseIntervention,
'accepted_interventions' => $interventionsAccepted,
Expand Down Expand Up @@ -123,6 +135,8 @@ public function signalementInterventionAccept(
$user->getEntreprise()
);

$this->denyAccessUnlessGranted(SignalementVoter::ACCEPT, $signalement);

if (null === $intervention) {
$intervention = new Intervention();
$intervention->setSignalement($signalement);
Expand Down Expand Up @@ -154,10 +168,14 @@ public function signalementInterventionRefuse(
EntrepriseManager $entrepriseManager,
ValidatorInterface $validator,
EventDispatcherInterface $eventDispatcher,
InterventionRepository $interventionRepository,
): Response {
if ($this->isCsrfTokenValid('signalement_intervention_refuse', $request->get('_csrf_token'))) {
$intervention = new Intervention();
$intervention->setSignalement($signalement);

$this->denyAccessUnlessGranted(SignalementVoter::ACCEPT, $signalement);

/** @var User $user */
$user = $this->getUser();
$intervention->setEntreprise($user->getEntreprise());
Expand Down Expand Up @@ -218,10 +236,14 @@ public function signalementInterventionEstimation(
/** @var User $user */
$user = $this->getUser();
$userEntreprise = $user->getEntreprise();
/** @var Intervention $intervention */
$intervention = $interventionRepository->findBySignalementAndEntreprise(
$signalement,
$userEntreprise
);

$this->denyAccessUnlessGranted(InterventionVoter::SEND_ESTIMATION, $intervention);

$intervention->setCommentaireEstimation($request->get('commentaire'));
$intervention->setMontantEstimation(ceil($montant));
$intervention->setEstimationSentAt(new \DateTimeImmutable());
Expand Down Expand Up @@ -252,6 +274,8 @@ public function signalementStop(
MailerProvider $mailerProvider,
): Response {
if ($this->isCsrfTokenValid('signalement_admin_stop', $request->get('_csrf_token'))) {
$this->denyAccessUnlessGranted(SignalementVoter::CLOSE, $signalement);

$this->addFlash('success', 'La procédure est terminée !');
$signalement->setClosedAt(new \DateTimeImmutable());
$signalement->updateUuidPublic();
Expand All @@ -278,28 +302,25 @@ public function intervention_stop(
EventDispatcherInterface $eventDispatcher,
): Response {
if ($this->isCsrfTokenValid('intervention_stop', $request->get('_csrf_token'))) {
$this->denyAccessUnlessGranted(InterventionVoter::STOP, $intervention);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sauf erreur de ma part ici je peux clôturer n'importe quelle intervention via son id. Il faut rajouter un check avec l'entreprise de l'user connecté (d'ailleurs je peux aussi en tant qu'admin alors que je ne suis pas censé en tant qu'admin mais ca à moins d'importance)


$this->addFlash('success', 'Votre procédure est terminée !');

$wasAccepted = $intervention->isAccepted();
if ($wasAccepted) {
$date = new \DateTimeImmutable();
$intervention->setCanceledByEntrepriseAt($date);
}
$date = new \DateTimeImmutable();
$intervention->setCanceledByEntrepriseAt($date);
$intervention->setAccepted(false);
$interventionManager->save($intervention);

if ($wasAccepted) {
/** @var User $user */
$user = $this->getUser();
$eventDispatcher->dispatch(
new InterventionEntrepriseCanceledEvent(
$intervention,
$user->getId(),
$date
),
InterventionEntrepriseCanceledEvent::NAME
);
}
/** @var User $user */
$user = $this->getUser();
$eventDispatcher->dispatch(
new InterventionEntrepriseCanceledEvent(
$intervention,
$user->getId(),
$date
),
InterventionEntrepriseCanceledEvent::NAME
);
}

return $this->redirectToRoute('app_signalement_view', ['uuid' => $intervention->getSignalement()->getUuid()]);
Expand Down
85 changes: 85 additions & 0 deletions src/Security/Voter/InterventionVoter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace App\Security\Voter;

use App\Entity\Intervention;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

class InterventionVoter extends Voter
{
public const SEND_ESTIMATION = 'INTERVENTION_SEND_ESTIMATION';
public const RESOLVE = 'INTERVENTION_RESOLVE';
public const STOP = 'INTERVENTION_STOP';

protected function supports(string $attribute, $subject): bool
{
return \in_array($attribute, [self::RESOLVE, self::SEND_ESTIMATION, self::STOP]) && $subject instanceof Intervention;
}

protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!($user instanceof UserInterface)) {
return false;
}

if (self::RESOLVE == $attribute) {
return $this->canResolve($subject);
}

if (self::SEND_ESTIMATION == $attribute) {
return $this->canSendEstimation($subject);
}

if (self::STOP == $attribute) {
return $this->canStop($subject);
}

return false;
}

private function canResolve(Intervention $intervention): bool
{
$signalement = $intervention->getSignalement();

if (!$signalement->getResolvedAt()
&& !$signalement->getClosedAt()
&& $intervention->isAccepted()
&& $intervention->getEstimationSentAt()
&& !$intervention->getCanceledByEntrepriseAt()
&& $intervention->isAcceptedByUsager()
&& !$intervention->getResolvedByEntrepriseAt()) {
return true;
}

return false;
}

private function canSendEstimation(Intervention $intervention): bool
{
if ($intervention->isAccepted()) {
return true;
}

return false;
}

private function canStop(Intervention $intervention): bool
{
$signalement = $intervention->getSignalement();

if (!$signalement->getResolvedAt()
&& !$signalement->getClosedAt()
&& $intervention->isAccepted()
&& $intervention->getEstimationSentAt()
&& !$intervention->getCanceledByEntrepriseAt()
&& ($intervention->getChoiceByEntrepriseAt() || $intervention->isAcceptedByUsager())
&& !$signalement->getTypeIntervention()) {
return true;
}

return false;
}
}
77 changes: 77 additions & 0 deletions src/Security/Voter/SignalementVoter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace App\Security\Voter;

use App\Entity\Enum\Role;
use App\Entity\Signalement;
use App\Repository\InterventionRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

class SignalementVoter extends Voter
{
public const ACCEPT = 'SIGNALEMENT_ACCEPT';
public const CLOSE = 'SIGNALEMENT_CLOSE';

public function __construct(
private Security $security,
private InterventionRepository $interventionRepository,
) {
}

protected function supports(string $attribute, $subject): bool
{
return \in_array($attribute, [self::ACCEPT, self::CLOSE]) && $subject instanceof Signalement;
}

protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!($user instanceof UserInterface)) {
return false;
}

if (self::ACCEPT == $attribute) {
return $this->canAccept($subject);
}

if (self::CLOSE == $attribute) {
return $this->canClose($subject);
}

return false;
}

private function canAccept(Signalement $signalement): bool
{
if ($this->security->isGranted(Role::ROLE_ADMIN->value)) {
return false;
}

$acceptedEstimations = $this->interventionRepository->findBy([
'signalement' => $signalement,
'acceptedByUsager' => true,
'canceledByEntrepriseAt' => null,
]);
if (!$signalement->getResolvedAt()
&& !$signalement->getClosedAt()
&& 0 == \count($acceptedEstimations)) {
return true;
}

return false;
}

private function canClose(Signalement $signalement): bool
{
if ($this->security->isGranted(Role::ROLE_ADMIN->value)
&& !$signalement->getResolvedAt()
&& !$signalement->getClosedAt()) {
return true;
}

return false;
}
}
Loading
Loading