From 4f3a6fbc899d4d509b49a9b2ff210bbb2d8a15b5 Mon Sep 17 00:00:00 2001 From: Marijn Doeve Date: Mon, 13 Jul 2026 09:09:04 +0200 Subject: [PATCH] Add authorization checks to PrepareEliminationController index and viewElimination had no IsGranted guard, unlike every other backoffice/elimination controller, so any authenticated user could prepare or overwrite another season's elimination screens. --- .../PrepareEliminationController.php | 4 +++ .../PrepareEliminationControllerTest.php | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Controller/Backoffice/PrepareEliminationController.php b/src/Controller/Backoffice/PrepareEliminationController.php index bf7953e..ec9acfc 100644 --- a/src/Controller/Backoffice/PrepareEliminationController.php +++ b/src/Controller/Backoffice/PrepareEliminationController.php @@ -11,18 +11,21 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Requirement\Requirement; use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Tvdt\Controller\AbstractController; use Tvdt\Entity\Elimination; use Tvdt\Entity\Quiz; use Tvdt\Entity\Season; use Tvdt\Enum\FlashType; use Tvdt\Factory\EliminationFactory; +use Tvdt\Security\Voter\SeasonVoter; final class PrepareEliminationController extends AbstractController { public function __construct(private readonly EliminationFactory $eliminationFactory, private readonly EntityManagerInterface $em) {} #[IsCsrfTokenValid('prepare_elimination')] + #[IsGranted(SeasonVoter::ELIMINATION, 'quiz')] #[Route( '/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare', name: 'tvdt_prepare_elimination', @@ -37,6 +40,7 @@ final class PrepareEliminationController extends AbstractController } #[IsCsrfTokenValid('prepare_elimination', methods: ['POST'])] + #[IsGranted(SeasonVoter::ELIMINATION, 'elimination')] #[Route( '/backoffice/elimination/{elimination}', name: 'tvdt_prepare_elimination_view', diff --git a/tests/Controller/Backoffice/PrepareEliminationControllerTest.php b/tests/Controller/Backoffice/PrepareEliminationControllerTest.php index 7e417ff..5b09c64 100644 --- a/tests/Controller/Backoffice/PrepareEliminationControllerTest.php +++ b/tests/Controller/Backoffice/PrepareEliminationControllerTest.php @@ -117,4 +117,34 @@ final class PrepareEliminationControllerTest extends AbstractControllerWebTestCa self::assertResponseRedirects(\sprintf('/elimination/%s', $elimination->id)); } + + public function testIndexIsDeniedForNonOwner(): void + { + $quiz = $this->getQuizByName('Quiz 1'); + $token = $this->getCsrfTokenFromPage(\sprintf('/backoffice/season/krtek/quiz/%s/result', $quiz->id), '/elimination/prepare'); + + $this->loginAs('test@example.org'); + + $this->client->request(Request::METHOD_POST, \sprintf('/backoffice/season/krtek/quiz/%s/elimination/prepare', $quiz->id), [ + '_token' => $token, + ]); + + self::assertResponseStatusCodeSame(403); + } + + public function testViewEliminationIsDeniedForNonOwner(): void + { + $quiz = $this->getQuizByName('Quiz 1'); + $elimination = new Elimination($quiz); + $elimination->data = ['Tom' => Elimination::SCREEN_GREEN]; + + $this->entityManager->persist($elimination); + $this->entityManager->flush(); + + $this->loginAs('test@example.org'); + + $this->client->request(Request::METHOD_GET, \sprintf('/backoffice/elimination/%s', $elimination->id)); + + self::assertResponseStatusCodeSame(403); + } }