From a1d1fdde144bd59f73428aa6886d9b9dfdec47da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 15 Sep 2026 15:06:58 +0200 Subject: [PATCH] feat: gate functionality for customer expert users --- config/packages/security.yaml | 4 ++ .../BookingEditDraft/DeleteController.php | 2 +- .../BookingEditDraft/ExportController.php | 2 +- .../BookingEditDraft/FilterController.php | 2 +- .../BookingEditDraft/IndexController.php | 2 +- .../Admin/BookingEditDraft/ShowController.php | 2 +- .../Admin/Log/DownloadController.php | 2 +- src/Controller/Admin/Log/FilterController.php | 2 +- src/Controller/Admin/Log/IndexController.php | 2 +- .../Admin/Log/XmlDumpController.php | 2 +- .../Widget/RecentLogEntriesWidgetProvider.php | 2 +- src/Menu/AbstractMenuBuilder.php | 3 +- src/Menu/AdminMenuBuilder.php | 64 ++++++++++--------- .../Voter/AdministrativeAccessVoter.php | 3 +- .../RecentLogEntriesWidgetProviderTest.php | 4 +- 15 files changed, 55 insertions(+), 43 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index ada0f1a..e96018f 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -17,8 +17,12 @@ security: roles: [ ROLE_MAILJET_WEBHOOK ] role_hierarchy: + # ROLE_CUSTOMER_EXPERT gates the booking-draft and log surfaces, which used to be + # ROLE_ADMIN only. Inheriting it here is what keeps those surfaces open to administrators + # after the #[IsGranted] attributes moved to the narrower role. ROLE_ADMIN: - ROLE_GROUPS_ADMIN + - ROLE_CUSTOMER_EXPERT ROLE_GROUPS_ADMIN: - ROLE_GROUPS_MANAGER firewalls: diff --git a/src/Controller/Admin/BookingEditDraft/DeleteController.php b/src/Controller/Admin/BookingEditDraft/DeleteController.php index f79138c..3e974b2 100644 --- a/src/Controller/Admin/BookingEditDraft/DeleteController.php +++ b/src/Controller/Admin/BookingEditDraft/DeleteController.php @@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class DeleteController extends AbstractController { use ReturnUrlTrait; diff --git a/src/Controller/Admin/BookingEditDraft/ExportController.php b/src/Controller/Admin/BookingEditDraft/ExportController.php index b0c0aaa..ed1c473 100644 --- a/src/Controller/Admin/BookingEditDraft/ExportController.php +++ b/src/Controller/Admin/BookingEditDraft/ExportController.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class ExportController extends AbstractController { use ReturnUrlTrait; diff --git a/src/Controller/Admin/BookingEditDraft/FilterController.php b/src/Controller/Admin/BookingEditDraft/FilterController.php index b2b812e..d235860 100644 --- a/src/Controller/Admin/BookingEditDraft/FilterController.php +++ b/src/Controller/Admin/BookingEditDraft/FilterController.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class FilterController extends AbstractController { use ListFilterTrait; diff --git a/src/Controller/Admin/BookingEditDraft/IndexController.php b/src/Controller/Admin/BookingEditDraft/IndexController.php index 7872e8e..68c0a49 100644 --- a/src/Controller/Admin/BookingEditDraft/IndexController.php +++ b/src/Controller/Admin/BookingEditDraft/IndexController.php @@ -16,7 +16,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class IndexController extends AbstractController { use ListFilterTrait; diff --git a/src/Controller/Admin/BookingEditDraft/ShowController.php b/src/Controller/Admin/BookingEditDraft/ShowController.php index e068933..42e4cc0 100644 --- a/src/Controller/Admin/BookingEditDraft/ShowController.php +++ b/src/Controller/Admin/BookingEditDraft/ShowController.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class ShowController extends AbstractController { use ReturnUrlTrait; diff --git a/src/Controller/Admin/Log/DownloadController.php b/src/Controller/Admin/Log/DownloadController.php index ce026a0..2c887ec 100644 --- a/src/Controller/Admin/Log/DownloadController.php +++ b/src/Controller/Admin/Log/DownloadController.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class DownloadController extends AbstractController { public function __construct(private readonly XmlDumpReader $xmlDumpReader) diff --git a/src/Controller/Admin/Log/FilterController.php b/src/Controller/Admin/Log/FilterController.php index 3dc92c2..e303f74 100644 --- a/src/Controller/Admin/Log/FilterController.php +++ b/src/Controller/Admin/Log/FilterController.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class FilterController extends AbstractController { use ListFilterTrait; diff --git a/src/Controller/Admin/Log/IndexController.php b/src/Controller/Admin/Log/IndexController.php index 5733768..ffdfbee 100644 --- a/src/Controller/Admin/Log/IndexController.php +++ b/src/Controller/Admin/Log/IndexController.php @@ -15,7 +15,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class IndexController extends AbstractController { use ListFilterTrait; diff --git a/src/Controller/Admin/Log/XmlDumpController.php b/src/Controller/Admin/Log/XmlDumpController.php index 9d9c7cc..0e07acc 100644 --- a/src/Controller/Admin/Log/XmlDumpController.php +++ b/src/Controller/Admin/Log/XmlDumpController.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_ADMIN')] +#[IsGranted('ROLE_CUSTOMER_EXPERT')] class XmlDumpController extends AbstractController { public function __construct(private readonly XmlDumpReader $xmlDumpReader) diff --git a/src/Dashboard/Widget/RecentLogEntriesWidgetProvider.php b/src/Dashboard/Widget/RecentLogEntriesWidgetProvider.php index c363e11..9656c12 100644 --- a/src/Dashboard/Widget/RecentLogEntriesWidgetProvider.php +++ b/src/Dashboard/Widget/RecentLogEntriesWidgetProvider.php @@ -37,7 +37,7 @@ class RecentLogEntriesWidgetProvider implements DashboardWidgetProviderInterface public function getRequiredRole(): string { - return Role::ADMIN; + return Role::CUSTOMER_EXPERT; } public function getPriority(): int diff --git a/src/Menu/AbstractMenuBuilder.php b/src/Menu/AbstractMenuBuilder.php index dd71df0..aeffb78 100644 --- a/src/Menu/AbstractMenuBuilder.php +++ b/src/Menu/AbstractMenuBuilder.php @@ -2,6 +2,7 @@ namespace App\Menu; +use App\Security\Voter\AdministrativeAccessVoter; use Knp\Menu\FactoryInterface; use Knp\Menu\ItemInterface; use Symfony\Bundle\SecurityBundle\Security; @@ -39,7 +40,7 @@ abstract class AbstractMenuBuilder protected function addAdminItem(ItemInterface $menu): void { - if ($this->security->isGranted('ROLE_ADMIN')) { + if ($this->security->isGranted(AdministrativeAccessVoter::ADMINISTRATIVE_ACCESS)) { $this->addDivider($menu); $menu->addChild('zum Adminbereich', [ 'route' => 'app_admin_index', diff --git a/src/Menu/AdminMenuBuilder.php b/src/Menu/AdminMenuBuilder.php index bfcb0df..dc0f735 100644 --- a/src/Menu/AdminMenuBuilder.php +++ b/src/Menu/AdminMenuBuilder.php @@ -22,16 +22,18 @@ class AdminMenuBuilder extends AbstractMenuBuilder 'icon' => 'chart', ], ]); - $menu->addChild('Buchungsentwürfe', [ - 'route' => 'app_admin_bookingeditdraft', - 'linkAttributes' => [ - 'title' => 'Buchungsentwürfe', - ], - 'extras' => [ - 'icon' => 'edit', - 'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']], - ], - ]); + if ($this->security->isGranted('ROLE_CUSTOMER_EXPERT')) { + $menu->addChild('Buchungsentwürfe', [ + 'route' => 'app_admin_bookingeditdraft', + 'linkAttributes' => [ + 'title' => 'Buchungsentwürfe', + ], + 'extras' => [ + 'icon' => 'edit', + 'routes' => [['pattern' => '/^app_admin_bookingeditdraft/']], + ], + ]); + } if ($this->security->isGranted('ROLE_GROUPS_ADMIN')) { $menu->addChild('Gruppenbuchungen', [ @@ -56,25 +58,29 @@ class AdminMenuBuilder extends AbstractMenuBuilder ]); } - $menu->addChild('Benutzer', [ - 'route' => 'app_admin_user', - 'linkAttributes' => [ - 'title' => 'Benutzer', - ], - 'extras' => [ - 'icon' => 'users', - ], - ]); - $menu->addChild('Logs', [ - 'route' => 'app_admin_log', - 'linkAttributes' => [ - 'title' => 'Logs', - ], - 'extras' => [ - 'icon' => 'list', - 'routes' => [['pattern' => '/^app_admin_log/']], - ], - ]); + if ($this->security->isGranted('ROLE_ADMIN')) { + $menu->addChild('Benutzer', [ + 'route' => 'app_admin_user', + 'linkAttributes' => [ + 'title' => 'Benutzer', + ], + 'extras' => [ + 'icon' => 'users', + ], + ]); + } + if ($this->security->isGranted('ROLE_CUSTOMER_EXPERT')) { + $menu->addChild('Logs', [ + 'route' => 'app_admin_log', + 'linkAttributes' => [ + 'title' => 'Logs', + ], + 'extras' => [ + 'icon' => 'list', + 'routes' => [['pattern' => '/^app_admin_log/']], + ], + ]); + } $this->addLogoutItem($menu); diff --git a/src/Security/Voter/AdministrativeAccessVoter.php b/src/Security/Voter/AdministrativeAccessVoter.php index 0e04d24..d57f490 100644 --- a/src/Security/Voter/AdministrativeAccessVoter.php +++ b/src/Security/Voter/AdministrativeAccessVoter.php @@ -29,6 +29,7 @@ class AdministrativeAccessVoter extends Voter protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool { return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']) - || $this->accessDecisionManager->decide($token, ['ROLE_GROUPS_MANAGER']); + || $this->accessDecisionManager->decide($token, ['ROLE_GROUPS_MANAGER']) + || $this->accessDecisionManager->decide($token, ['ROLE_CUSTOMER_EXPERT']); } } diff --git a/tests/Dashboard/RecentLogEntriesWidgetProviderTest.php b/tests/Dashboard/RecentLogEntriesWidgetProviderTest.php index 9cf866b..a8211d3 100644 --- a/tests/Dashboard/RecentLogEntriesWidgetProviderTest.php +++ b/tests/Dashboard/RecentLogEntriesWidgetProviderTest.php @@ -46,7 +46,7 @@ class RecentLogEntriesWidgetProviderTest extends TestCase $this->assertStringEndsWith(str_repeat('a', 60).'…', $widget->entries[0]->label); } - public function testAsksForFiveEntriesAndRequiresAdmin(): void + public function testAsksForFiveEntriesAndRequiresCustomerExpert(): void { $repository = $this->createMock(LogEntryRepository::class); $repository->expects($this->once()) @@ -57,7 +57,7 @@ class RecentLogEntriesWidgetProviderTest extends TestCase $provider = new RecentLogEntriesWidgetProvider($repository, $this->urlGenerator()); - $this->assertSame(Role::ADMIN, $provider->getRequiredRole()); + $this->assertSame(Role::CUSTOMER_EXPERT, $provider->getRequiredRole()); $this->assertNotNull($provider->build()); }