<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
/**
* Erzwingt pro EasyAdmin-Dashboard eine Allowlist der aufrufbaren CrudController.
*
* Hintergrund (AWM-1081): EasyAdmin 3 routet ALLE CRUD-Aktionen über die
* Dashboard-URL mit ?crudControllerFqcn=... — jeder CrudController ist damit
* über jedes Dashboard erreichbar, sobald der User die Dashboard-Route betreten
* darf. setPermission() im Menü versteckt nur den Menüpunkt, schützt aber nicht
* den direkten Aufruf per URL. Ohne diese Allowlist konnte ein ROLE_ADMIN über
* /admin?crudControllerFqcn=...SensorCrudController Super-Admin-Masken öffnen.
*
* Regel: Für die Route "dashboard" (/admin) ist je Controller die Mindestrolle
* hinterlegt (Stand des Menüs in Admin\DashboardController). Alles, was nicht
* gelistet ist, erfordert ROLE_SUPER_ADMIN (fail-closed).
*/
class CrudControllerAllowlistSubscriber implements EventSubscriberInterface
{
/**
* Dashboard-Controller-FQCN => [CrudController-FQCN => erlaubte Rollen (eine reicht)].
* Nicht gelistete Controller fallen auf DEFAULT_ROLES zurück.
*
* Absichtlich am Dashboard-CONTROLLER festgemacht, nicht am Routennamen:
* /admin ist über zwei Routen erreichbar (dashboard aus routes.yaml UND die
* Annotation-Route app_admin_dashboard_index) — der Controller ist eindeutig.
*/
private const ALLOWLIST = [
'App\Controller\Admin\DashboardController' => [
// Submenü "Admin" + "Aufzüge" (heute für ROLE_ADMIN sichtbar)
'App\Controller\Admin\UserCrudController' => ['ROLE_ADMIN'],
'App\Controller\Admin\CompanyCrudController' => ['ROLE_ADMIN'],
'App\Controller\Admin\ZuesMailboxCrudController' => ['ROLE_ADMIN'],
'App\Controller\Admin\ElevatorCrudController' => ['ROLE_ADMIN'],
'App\Controller\Admin\ManufacturerCrudController' => ['ROLE_ADMIN'],
// Adressen: im Aufzüge-Submenü (ROLE_ADMIN) UND im Nea-Submenü (ROLE_NEA)
'App\Controller\Admin\AddressCrudController' => ['ROLE_ADMIN', 'ROLE_NEA'],
// Submenü "Nea" (ROLE_NEA; ROLE_SUPER_ADMIN erbt ROLE_NEA über die Hierarchie)
'App\Controller\Admin\NeaCrudController' => ['ROLE_NEA'],
'App\Controller\Admin\GpioCrudController' => ['ROLE_NEA'],
'App\Controller\Admin\BuchungskreisCrudController' => ['ROLE_NEA'],
],
// Kunden-Admin /kundenadmin (AWM-1081): NUR die Ka-Controller, alles
// andere (insb. die Super-Admin-Controller) bleibt hier gesperrt
'App\Controller\KundenAdmin\KundenAdminDashboardController' => [
'App\Controller\KundenAdmin\KaElevatorCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\KaAddressCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\KaUserCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\KaCompanyCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\KaContactCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\KaDocumentCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\Trash\KaTrashElevatorCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\Trash\KaTrashAddressCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\Trash\KaTrashContactCrudController' => ['ROLE_ADMIN'],
'App\Controller\KundenAdmin\Trash\KaTrashDocumentCrudController' => ['ROLE_ADMIN'],
],
];
private const DEFAULT_ROLES = ['ROLE_SUPER_ADMIN'];
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public static function getSubscribedEvents(): array
{
// Nach der Security-Firewall (Prio 8), damit isGranted() funktioniert,
// aber vor EasyAdmins AdminRouterSubscriber (Prio 0), damit die Ablehnung
// greift, bevor EA den AdminContext aufbaut.
// Hinweis: EA 3.5 prüft zusätzlich URL-Signaturen — die sind aber weder
// benutzergebunden noch ablaufend und in EA 4 ersatzlos entfernt; diese
// Allowlist ist die eigentliche Rechteprüfung.
return [KernelEvents::REQUEST => ['onKernelRequest', 1]];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$dashboardFqcn = strstr((string) $request->attributes->get('_controller'), '::', true);
if (!isset(self::ALLOWLIST[$dashboardFqcn])) {
return;
}
$controllerFqcn = $request->query->get('crudControllerFqcn');
if (!$controllerFqcn) {
// Dashboard-Startseite ohne CRUD-Kontext
return;
}
$allowedRoles = self::ALLOWLIST[$dashboardFqcn][ltrim($controllerFqcn, '\\')] ?? self::DEFAULT_ROLES;
foreach ($allowedRoles as $role) {
if ($this->security->isGranted($role)) {
return;
}
}
throw new AccessDeniedException(sprintf('CrudController %s ist über das Dashboard %s für diesen Benutzer nicht erlaubt.', $controllerFqcn, $dashboardFqcn));
}
}