src/EventSubscriber/CrudControllerAllowlistSubscriber.php line 86

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  4. use Symfony\Component\HttpKernel\Event\RequestEvent;
  5. use Symfony\Component\HttpKernel\KernelEvents;
  6. use Symfony\Component\Security\Core\Exception\AccessDeniedException;
  7. use Symfony\Component\Security\Core\Security;
  8. /**
  9.  * Erzwingt pro EasyAdmin-Dashboard eine Allowlist der aufrufbaren CrudController.
  10.  *
  11.  * Hintergrund (AWM-1081): EasyAdmin 3 routet ALLE CRUD-Aktionen über die
  12.  * Dashboard-URL mit ?crudControllerFqcn=... — jeder CrudController ist damit
  13.  * über jedes Dashboard erreichbar, sobald der User die Dashboard-Route betreten
  14.  * darf. setPermission() im Menü versteckt nur den Menüpunkt, schützt aber nicht
  15.  * den direkten Aufruf per URL. Ohne diese Allowlist konnte ein ROLE_ADMIN über
  16.  * /admin?crudControllerFqcn=...SensorCrudController Super-Admin-Masken öffnen.
  17.  *
  18.  * Regel: Für die Route "dashboard" (/admin) ist je Controller die Mindestrolle
  19.  * hinterlegt (Stand des Menüs in Admin\DashboardController). Alles, was nicht
  20.  * gelistet ist, erfordert ROLE_SUPER_ADMIN (fail-closed).
  21.  */
  22. class CrudControllerAllowlistSubscriber implements EventSubscriberInterface
  23. {
  24.     /**
  25.      * Dashboard-Controller-FQCN => [CrudController-FQCN => erlaubte Rollen (eine reicht)].
  26.      * Nicht gelistete Controller fallen auf DEFAULT_ROLES zurück.
  27.      *
  28.      * Absichtlich am Dashboard-CONTROLLER festgemacht, nicht am Routennamen:
  29.      * /admin ist über zwei Routen erreichbar (dashboard aus routes.yaml UND die
  30.      * Annotation-Route app_admin_dashboard_index) — der Controller ist eindeutig.
  31.      */
  32.     private const ALLOWLIST = [
  33.         'App\Controller\Admin\DashboardController' => [
  34.             // Submenü "Admin" + "Aufzüge" (heute für ROLE_ADMIN sichtbar)
  35.             'App\Controller\Admin\UserCrudController' => ['ROLE_ADMIN'],
  36.             'App\Controller\Admin\CompanyCrudController' => ['ROLE_ADMIN'],
  37.             'App\Controller\Admin\ZuesMailboxCrudController' => ['ROLE_ADMIN'],
  38.             'App\Controller\Admin\ElevatorCrudController' => ['ROLE_ADMIN'],
  39.             'App\Controller\Admin\ManufacturerCrudController' => ['ROLE_ADMIN'],
  40.             // Adressen: im Aufzüge-Submenü (ROLE_ADMIN) UND im Nea-Submenü (ROLE_NEA)
  41.             'App\Controller\Admin\AddressCrudController' => ['ROLE_ADMIN''ROLE_NEA'],
  42.             // Submenü "Nea" (ROLE_NEA; ROLE_SUPER_ADMIN erbt ROLE_NEA über die Hierarchie)
  43.             'App\Controller\Admin\NeaCrudController' => ['ROLE_NEA'],
  44.             'App\Controller\Admin\GpioCrudController' => ['ROLE_NEA'],
  45.             'App\Controller\Admin\BuchungskreisCrudController' => ['ROLE_NEA'],
  46.         ],
  47.         // Kunden-Admin /kundenadmin (AWM-1081): NUR die Ka-Controller, alles
  48.         // andere (insb. die Super-Admin-Controller) bleibt hier gesperrt
  49.         'App\Controller\KundenAdmin\KundenAdminDashboardController' => [
  50.             'App\Controller\KundenAdmin\KaElevatorCrudController' => ['ROLE_ADMIN'],
  51.             'App\Controller\KundenAdmin\KaAddressCrudController' => ['ROLE_ADMIN'],
  52.             'App\Controller\KundenAdmin\KaUserCrudController' => ['ROLE_ADMIN'],
  53.             'App\Controller\KundenAdmin\KaCompanyCrudController' => ['ROLE_ADMIN'],
  54.             'App\Controller\KundenAdmin\KaContactCrudController' => ['ROLE_ADMIN'],
  55.             'App\Controller\KundenAdmin\KaDocumentCrudController' => ['ROLE_ADMIN'],
  56.             'App\Controller\KundenAdmin\Trash\KaTrashElevatorCrudController' => ['ROLE_ADMIN'],
  57.             'App\Controller\KundenAdmin\Trash\KaTrashAddressCrudController' => ['ROLE_ADMIN'],
  58.             'App\Controller\KundenAdmin\Trash\KaTrashContactCrudController' => ['ROLE_ADMIN'],
  59.             'App\Controller\KundenAdmin\Trash\KaTrashDocumentCrudController' => ['ROLE_ADMIN'],
  60.         ],
  61.     ];
  62.     private const DEFAULT_ROLES = ['ROLE_SUPER_ADMIN'];
  63.     private $security;
  64.     public function __construct(Security $security)
  65.     {
  66.         $this->security $security;
  67.     }
  68.     public static function getSubscribedEvents(): array
  69.     {
  70.         // Nach der Security-Firewall (Prio 8), damit isGranted() funktioniert,
  71.         // aber vor EasyAdmins AdminRouterSubscriber (Prio 0), damit die Ablehnung
  72.         // greift, bevor EA den AdminContext aufbaut.
  73.         // Hinweis: EA 3.5 prüft zusätzlich URL-Signaturen — die sind aber weder
  74.         // benutzergebunden noch ablaufend und in EA 4 ersatzlos entfernt; diese
  75.         // Allowlist ist die eigentliche Rechteprüfung.
  76.         return [KernelEvents::REQUEST => ['onKernelRequest'1]];
  77.     }
  78.     public function onKernelRequest(RequestEvent $event): void
  79.     {
  80.         if (!$event->isMainRequest()) {
  81.             return;
  82.         }
  83.         $request $event->getRequest();
  84.         $dashboardFqcn strstr((string) $request->attributes->get('_controller'), '::'true);
  85.         if (!isset(self::ALLOWLIST[$dashboardFqcn])) {
  86.             return;
  87.         }
  88.         $controllerFqcn $request->query->get('crudControllerFqcn');
  89.         if (!$controllerFqcn) {
  90.             // Dashboard-Startseite ohne CRUD-Kontext
  91.             return;
  92.         }
  93.         $allowedRoles self::ALLOWLIST[$dashboardFqcn][ltrim($controllerFqcn'\\')] ?? self::DEFAULT_ROLES;
  94.         foreach ($allowedRoles as $role) {
  95.             if ($this->security->isGranted($role)) {
  96.                 return;
  97.             }
  98.         }
  99.         throw new AccessDeniedException(sprintf('CrudController %s ist über das Dashboard %s für diesen Benutzer nicht erlaubt.'$controllerFqcn$dashboardFqcn));
  100.     }
  101. }