From e13d584025a7e7d2aa7b676d2ed4e8a07f028612 Mon Sep 17 00:00:00 2001 From: team 1 Date: Mon, 11 May 2026 14:47:31 +0200 Subject: [PATCH] add user management --- config/packages/security.yaml | 2 + ...TCH_94_ERROR_PAGES_ACCESS_DENIED_README.md | 132 +++++++++++++++ public/assets/styles/base.css | 8 + src/Controller/Admin/SecurityController.php | 26 ++- src/Controller/Chat/SecurityController.php | 23 ++- src/Security/AccessDeniedHandler.php | 25 +++ src/Security/AccessDeniedPageRenderer.php | 153 ++++++++++++++++++ .../TwigBundle/Exception/error.html.twig | 25 +++ .../TwigBundle/Exception/error403.html.twig | 9 ++ .../TwigBundle/Exception/error404.html.twig | 29 ++++ .../TwigBundle/Exception/error500.html.twig | 27 ++++ templates/error/access_denied.html.twig | 50 ++++++ templates/error/layout.html.twig | 55 +++++++ 13 files changed, 556 insertions(+), 8 deletions(-) create mode 100644 patch_history/RETRIEX_PATCH_94_ERROR_PAGES_ACCESS_DENIED_README.md create mode 100644 src/Security/AccessDeniedHandler.php create mode 100644 src/Security/AccessDeniedPageRenderer.php create mode 100644 templates/bundles/TwigBundle/Exception/error.html.twig create mode 100644 templates/bundles/TwigBundle/Exception/error403.html.twig create mode 100644 templates/bundles/TwigBundle/Exception/error404.html.twig create mode 100644 templates/bundles/TwigBundle/Exception/error500.html.twig create mode 100644 templates/error/access_denied.html.twig create mode 100644 templates/error/layout.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index a540b31..f279544 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -17,6 +17,7 @@ security: lazy: true provider: app_user_provider user_checker: App\Security\ActiveUserChecker + access_denied_handler: App\Security\AccessDeniedHandler context: retriex_user_area form_login: @@ -39,6 +40,7 @@ security: lazy: true provider: app_user_provider user_checker: App\Security\ActiveUserChecker + access_denied_handler: App\Security\AccessDeniedHandler context: retriex_user_area form_login: diff --git a/patch_history/RETRIEX_PATCH_94_ERROR_PAGES_ACCESS_DENIED_README.md b/patch_history/RETRIEX_PATCH_94_ERROR_PAGES_ACCESS_DENIED_README.md new file mode 100644 index 0000000..2788234 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_94_ERROR_PAGES_ACCESS_DENIED_README.md @@ -0,0 +1,132 @@ +# RetrieX Patch p94 - Error Pages and Access Denied UX + +## Ziel + +Dieser Patch ergänzt benutzerfreundliche Fehlerseiten und behandelt insbesondere den Fall, dass ein bereits eingeloggter Benutzer in einen Bereich wechselt, für den ihm die passende Rolle fehlt. + +Beispiel: + +- Benutzer ist im Chat angemeldet, besitzt aber keine `ROLE_ADMIN_AREA`. +- Benutzer öffnet `/admin` oder `/admin/login`. +- Statt Symfony-Default-403 oder verwirrender Login-Seite erscheint eine klare Fehlerseite mit benötigter Rolle und Abmelde-Option. + +## Enthaltene Änderungen + +Neue Dateien: + +- `src/Security/AccessDeniedHandler.php` +- `src/Security/AccessDeniedPageRenderer.php` +- `templates/error/layout.html.twig` +- `templates/error/access_denied.html.twig` +- `templates/bundles/TwigBundle/Exception/error403.html.twig` +- `templates/bundles/TwigBundle/Exception/error404.html.twig` +- `templates/bundles/TwigBundle/Exception/error500.html.twig` +- `templates/bundles/TwigBundle/Exception/error.html.twig` + +Geänderte Dateien: + +- `config/packages/security.yaml` +- `src/Controller/Admin/SecurityController.php` +- `src/Controller/Chat/SecurityController.php` + +## Verhalten + +### 403 / fehlende Rolle + +Der neue `AccessDeniedHandler` rendert eine konsistente 403-Seite für Admin- und Chat-Firewalls. + +Die Seite zeigt: + +- Bereich, z. B. `Adminbereich` oder `Chatbereich` +- benötigte Rolle, z. B. `ROLE_ADMIN_AREA` oder `ROLE_CHAT_USER` +- angemeldeten Benutzer +- Link zurück in einen Bereich, für den der Benutzer Rechte hat +- Abmelde-Link, um sich mit einem anderen Benutzer anzumelden + +### Login mit falschem Bereich + +Die Login-Controller behandeln nun explizit den Fall: + +- Benutzer ist bereits authentifiziert +- Benutzer hat aber nicht die Zielrolle des Loginbereichs + +Dann wird direkt die 403-Seite gerendert, statt erneut die Login-Maske zu zeigen. + +### Generische Fehlerseiten + +Symfony/Twig Exception-Templates wurden ergänzt für: + +- `403` +- `404` +- `500` +- generische Fehler + +Diese greifen insbesondere im produktiven Fehlerhandling. Für Access-Denied-Fälle greift zusätzlich der Security-Handler auch unabhängig vom generischen Exception-Template. + +## Security-Konfiguration + +Beide Firewalls nutzen nun denselben Handler: + +```yaml +admin: + access_denied_handler: App\Security\AccessDeniedHandler + +chat: + access_denied_handler: App\Security\AccessDeniedHandler +``` + +## Nicht geändert + +Keine Änderungen an: + +- `AgentRunner` +- Retrieval +- Scoring +- Shop-Matching +- PromptBuilder +- RAG-/Commerce-YAMLs +- User-CRUD-Fachlogik aus p93 + +## Lokale Checks + +Ausgeführt: + +```bash +php -l src/Security/AccessDeniedPageRenderer.php +php -l src/Security/AccessDeniedHandler.php +php -l src/Controller/Admin/SecurityController.php +php -l src/Controller/Chat/SecurityController.php +python3 -c "import yaml; yaml.safe_load(open('config/packages/security.yaml'))" +``` + +Symfony-Console-Checks konnten lokal nicht ausgeführt werden, weil `vendor/` im ZIP nicht enthalten ist. + +## Empfohlene Prüfungen in der Zielumgebung + +```bash +php bin/console lint:yaml config/packages/security.yaml +php bin/console lint:twig templates/error templates/bundles/TwigBundle/Exception +php bin/console cache:clear +php bin/console debug:container App\Security\AccessDeniedHandler +php bin/console debug:router | grep -E 'admin_login|chat_login|admin_dashboard|chat_index' +php bin/console mto:agent:config:validate +php bin/console mto:agent:regression:test +php bin/console mto:agent:config:audit-source --details +``` + +## Manuelle Smoke-Tests + +1. Als reiner `ROLE_CHAT_USER` einloggen und `/admin` öffnen. + - Erwartung: 403-Seite mit benötigter Rolle `ROLE_ADMIN_AREA`. + +2. Als reiner `ROLE_ADMIN_AREA` ohne `ROLE_CHAT_USER` `/chat` öffnen. + - Erwartung: 403-Seite mit benötigter Rolle `ROLE_CHAT_USER`. + +3. Als deaktivierter User einloggen. + - Erwartung: Login wird weiterhin durch p93 `ActiveUserChecker` blockiert. + +4. Nicht existente Route öffnen, z. B. `/admin/foo-does-not-exist`. + - Erwartung: freundliche 404-Fehlerseite, sofern Exception-Handling im Prod-Modus aktiv ist. + +5. Fehler im Prod-Modus provozieren. + - Erwartung: freundliche 500-Seite mit Link zu System-Logs für Admin-User. diff --git a/public/assets/styles/base.css b/public/assets/styles/base.css index 1d4b2f2..ea8385f 100644 --- a/public/assets/styles/base.css +++ b/public/assets/styles/base.css @@ -315,6 +315,14 @@ body { background-color: #86b7fe !important; } +.btn-info{ + background-color: #86b7fe !important; +} + +.text-info{ + color: #86b7fe !important; +} + .card.bg-black, .card.bg-dark { color: #e2e2e2; diff --git a/src/Controller/Admin/SecurityController.php b/src/Controller/Admin/SecurityController.php index 7d90a2e..de2699c 100644 --- a/src/Controller/Admin/SecurityController.php +++ b/src/Controller/Admin/SecurityController.php @@ -1,9 +1,13 @@ getUser() !== null && $this->isGranted('ROLE_ADMIN_AREA')) { + public function login( + Request $request, + AuthenticationUtils $authUtils, + AccessDeniedPageRenderer $accessDeniedPageRenderer, + ): Response { + $user = $this->getUser(); + + if ($user !== null && $this->isGranted(ApplicationRoles::ROLE_ADMIN_AREA)) { return $this->redirectToRoute('admin_dashboard'); } + if ($user !== null) { + return $accessDeniedPageRenderer->renderForbidden( + $request, + 'admin', + ApplicationRoles::ROLE_ADMIN_AREA, + ); + } + return $this->render('admin/security/login.html.twig', [ 'last_username' => $authUtils->getLastUsername(), 'error' => $authUtils->getLastAuthenticationError(), @@ -27,7 +43,7 @@ final class SecurityController extends AbstractController #[Route('/admin/logout', name: 'admin_logout')] public function logout(): void { - // Symfony interceptet diese Route, daher bleibt sie leer. + // Symfony intercepts this route via the firewall logout configuration. throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); } } diff --git a/src/Controller/Chat/SecurityController.php b/src/Controller/Chat/SecurityController.php index 22ade85..60e2ebd 100644 --- a/src/Controller/Chat/SecurityController.php +++ b/src/Controller/Chat/SecurityController.php @@ -4,7 +4,10 @@ declare(strict_types=1); namespace App\Controller\Chat; +use App\Security\AccessDeniedPageRenderer; +use App\Security\ApplicationRoles; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; @@ -19,12 +22,25 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; final class SecurityController extends AbstractController { #[Route('/chat/login', name: 'chat_login', methods: ['GET', 'POST'])] - public function login(AuthenticationUtils $authUtils): Response - { - if ($this->getUser() !== null && $this->isGranted('ROLE_CHAT_USER')) { + public function login( + Request $request, + AuthenticationUtils $authUtils, + AccessDeniedPageRenderer $accessDeniedPageRenderer, + ): Response { + $user = $this->getUser(); + + if ($user !== null && $this->isGranted(ApplicationRoles::ROLE_CHAT_USER)) { return $this->redirectToRoute('chat_index'); } + if ($user !== null) { + return $accessDeniedPageRenderer->renderForbidden( + $request, + 'chat', + ApplicationRoles::ROLE_CHAT_USER, + ); + } + return $this->render('chat/security/login.html.twig', [ 'last_username' => $authUtils->getLastUsername(), 'error' => $authUtils->getLastAuthenticationError(), @@ -34,6 +50,7 @@ final class SecurityController extends AbstractController #[Route('/chat/logout', name: 'chat_logout', methods: ['GET'])] public function logout(): void { + // Symfony intercepts this route via the firewall logout configuration. throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); } } diff --git a/src/Security/AccessDeniedHandler.php b/src/Security/AccessDeniedHandler.php new file mode 100644 index 0000000..088b2ff --- /dev/null +++ b/src/Security/AccessDeniedHandler.php @@ -0,0 +1,25 @@ +renderer->renderForbidden($request); + } +} diff --git a/src/Security/AccessDeniedPageRenderer.php b/src/Security/AccessDeniedPageRenderer.php new file mode 100644 index 0000000..3a64ace --- /dev/null +++ b/src/Security/AccessDeniedPageRenderer.php @@ -0,0 +1,153 @@ +buildContext($request, $area, $requiredRole, $message); + + return new Response( + $this->twig->render('error/access_denied.html.twig', $context), + Response::HTTP_FORBIDDEN, + ); + } + + /** + * @return array + */ + private function buildContext( + Request $request, + ?string $area, + ?string $requiredRole, + ?string $message, + ): array { + $areaKey = $area ?? $this->detectArea($request); + $target = $this->targetContext($areaKey); + $currentUser = $this->security->getUser(); + $fallbackLink = $this->fallbackLink($target['home_route']); + + return [ + 'status_code' => Response::HTTP_FORBIDDEN, + 'status_text' => 'Zugriff verweigert', + 'area_label' => $target['label'], + 'required_role' => $requiredRole ?? $target['required_role'], + 'login_route' => $target['login_route'], + 'logout_route' => $target['logout_route'], + 'home_route' => $fallbackLink['route'], + 'home_label' => $fallbackLink['label'], + 'current_user_identifier' => $currentUser instanceof UserInterface ? $currentUser->getUserIdentifier() : null, + 'message' => $message ?? $this->defaultMessage($currentUser instanceof UserInterface, $target['label']), + ]; + } + + private function detectArea(Request $request): string + { + $path = $request->getPathInfo(); + + if (str_starts_with($path, '/admin')) { + return 'admin'; + } + + if ( + $path === '/' + || str_starts_with($path, '/chat') + || str_starts_with($path, '/ask-jobs') + || str_starts_with($path, '/ask-sse') + || str_starts_with($path, '/history') + || str_starts_with($path, '/chat-messages/frontend') + ) { + return 'chat'; + } + + return 'application'; + } + + /** + * @return array{label: string, required_role: string, login_route: string|null, logout_route: string|null, home_route: string|null} + */ + private function targetContext(string $area): array + { + return match ($area) { + 'admin' => [ + 'label' => 'Adminbereich', + 'required_role' => ApplicationRoles::ROLE_ADMIN_AREA, + 'login_route' => 'admin_login', + 'logout_route' => 'admin_logout', + 'home_route' => 'admin_dashboard', + ], + 'chat' => [ + 'label' => 'Chatbereich', + 'required_role' => ApplicationRoles::ROLE_CHAT_USER, + 'login_route' => 'chat_login', + 'logout_route' => 'chat_logout', + 'home_route' => 'chat_index', + ], + default => [ + 'label' => 'Anwendung', + 'required_role' => ApplicationRoles::ROLE_USER, + 'login_route' => null, + 'logout_route' => null, + 'home_route' => null, + ], + }; + } + + /** + * @return array{route: string|null, label: string|null} + */ + private function fallbackLink(?string $targetHomeRoute): array + { + if ($targetHomeRoute === 'admin_dashboard' && $this->security->isGranted(ApplicationRoles::ROLE_ADMIN_AREA)) { + return ['route' => 'admin_dashboard', 'label' => 'Zum Admin-Dashboard']; + } + + if ($targetHomeRoute === 'chat_index' && $this->security->isGranted(ApplicationRoles::ROLE_CHAT_USER)) { + return ['route' => 'chat_index', 'label' => 'Zum Chat']; + } + + if ($this->security->isGranted(ApplicationRoles::ROLE_CHAT_USER)) { + return ['route' => 'chat_index', 'label' => 'Zum Chat']; + } + + if ($this->security->isGranted(ApplicationRoles::ROLE_ADMIN_AREA)) { + return ['route' => 'admin_dashboard', 'label' => 'Zum Admin-Dashboard']; + } + + return ['route' => null, 'label' => null]; + } + + private function defaultMessage(bool $authenticated, string $areaLabel): string + { + if ($authenticated) { + return sprintf( + 'Du bist angemeldet, aber dein Benutzerkonto hat nicht die notwendige Berechtigung für den %s.', + $areaLabel, + ); + } + + return sprintf('Für den Zugriff auf den %s ist eine Anmeldung mit passender Rolle erforderlich.', $areaLabel); + } +} diff --git a/templates/bundles/TwigBundle/Exception/error.html.twig b/templates/bundles/TwigBundle/Exception/error.html.twig new file mode 100644 index 0000000..f7786a9 --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error.html.twig @@ -0,0 +1,25 @@ +{% extends 'error/layout.html.twig' %} + +{% set status_code = status_code|default(500) %} + +{% block title %}Fehler {{ status_code }}{% endblock %} +{% block heading %}Ein Fehler ist aufgetreten{% endblock %} +{% block message %}Die Anfrage konnte nicht verarbeitet werden. Falls der Fehler erneut auftritt, prüfe bitte die System-Logs.{% endblock %} + +{% block actions %} + {% if app.user and is_granted('ROLE_CHAT_USER') %} + + Zum Chat + + {% endif %} + {% if app.user and is_granted('ROLE_ADMIN_AREA') %} + + Zum Adminbereich + + {% endif %} + {% if not app.user %} + + Zur Anmeldung + + {% endif %} +{% endblock %} diff --git a/templates/bundles/TwigBundle/Exception/error403.html.twig b/templates/bundles/TwigBundle/Exception/error403.html.twig new file mode 100644 index 0000000..ddc6b6d --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error403.html.twig @@ -0,0 +1,9 @@ +{% extends 'error/access_denied.html.twig' %} + +{% set status_code = status_code|default(403) %} +{% set area_label = area_label|default('Anwendung') %} +{% set required_role = required_role|default('passende Berechtigung') %} +{% set message = message|default('Für diese Seite fehlen deinem Benutzerkonto die notwendigen Rechte.') %} +{% set current_user_identifier = current_user_identifier|default(app.user ? app.user.userIdentifier : null) %} +{% set home_route = home_route|default(app.user and is_granted('ROLE_CHAT_USER') ? 'chat_index' : (app.user and is_granted('ROLE_ADMIN_AREA') ? 'admin_dashboard' : null)) %} +{% set home_label = home_label|default(home_route == 'chat_index' ? 'Zum Chat' : (home_route == 'admin_dashboard' ? 'Zum Admin-Dashboard' : null)) %} diff --git a/templates/bundles/TwigBundle/Exception/error404.html.twig b/templates/bundles/TwigBundle/Exception/error404.html.twig new file mode 100644 index 0000000..87dc1f4 --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error404.html.twig @@ -0,0 +1,29 @@ +{% extends 'error/layout.html.twig' %} + +{% set status_code = status_code|default(404) %} + +{% block title %}Seite nicht gefunden · Fehler 404{% endblock %} +{% block icon %}{% endblock %} +{% block heading %}Seite nicht gefunden{% endblock %} +{% block message %}Die angeforderte Seite existiert nicht oder wurde verschoben.{% endblock %} + +{% block actions %} + {% if app.user and is_granted('ROLE_CHAT_USER') %} + + Zum Chat + + {% endif %} + {% if app.user and is_granted('ROLE_ADMIN_AREA') %} + + Zum Adminbereich + + {% endif %} + {% if not app.user %} + + Zur Chat-Anmeldung + + + Zur Admin-Anmeldung + + {% endif %} +{% endblock %} diff --git a/templates/bundles/TwigBundle/Exception/error500.html.twig b/templates/bundles/TwigBundle/Exception/error500.html.twig new file mode 100644 index 0000000..d82a896 --- /dev/null +++ b/templates/bundles/TwigBundle/Exception/error500.html.twig @@ -0,0 +1,27 @@ +{% extends 'error/layout.html.twig' %} + +{% set status_code = status_code|default(500) %} + +{% block title %}Systemfehler · Fehler 500{% endblock %} +{% block icon %}{% endblock %} +{% block heading %}Systemfehler{% endblock %} +{% block message %}Es ist ein unerwarteter Fehler aufgetreten. Bitte versuche es erneut oder prüfe die System-Logs im Adminbereich.{% endblock %} + +{% block actions %} + {% if app.user and is_granted('ROLE_ADMIN_AREA') %} + + System-Logs öffnen + + + Zum Adminbereich + + {% elseif app.user and is_granted('ROLE_CHAT_USER') %} + + Zum Chat + + {% else %} + + Zur Chat-Anmeldung + + {% endif %} +{% endblock %} diff --git a/templates/error/access_denied.html.twig b/templates/error/access_denied.html.twig new file mode 100644 index 0000000..839c734 --- /dev/null +++ b/templates/error/access_denied.html.twig @@ -0,0 +1,50 @@ +{% extends 'error/layout.html.twig' %} + +{% block title %}Zugriff verweigert · Fehler {{ status_code|default(403) }}{% endblock %} + +{% block icon %}{% endblock %} + +{% block heading %}Zugriff verweigert{% endblock %} + +{% block message %} + {{ message|default('Für diesen Bereich fehlen deinem Benutzerkonto die notwendigen Rechte.') }} +{% endblock %} + +{% block details %} +
+
+
+
Bereich
+
{{ area_label|default('Anwendung') }}
+
+
+
Benötigte Rolle
+ {{ required_role|default('ROLE_USER') }} +
+ {% if current_user_identifier|default(null) %} +
+
Angemeldeter Benutzer
+
{{ current_user_identifier }}
+
+ {% endif %} +
+
+{% endblock %} + +{% block actions %} + {% if home_route|default(null) and home_label|default(null) %} + + {{ home_label }} + + {% endif %} + + {% if logout_route|default(null) %} + + Abmelden und anderen Benutzer verwenden + + {% elseif login_route|default(null) %} + + Zur Anmeldung + + {% endif %} +{% endblock %} diff --git a/templates/error/layout.html.twig b/templates/error/layout.html.twig new file mode 100644 index 0000000..27f0f1b --- /dev/null +++ b/templates/error/layout.html.twig @@ -0,0 +1,55 @@ + + + + + + {% block title %}Fehler {{ status_code|default(500) }}{% endblock %} + + + + + + +
+
+
+
+
+
+
+ {% block icon %}{% endblock %} +
+
+
+ Fehler {{ status_code|default(500) }} +
+

{% block heading %}Ein Fehler ist aufgetreten{% endblock %}

+
+
+ +

{% block message %}Die Anfrage konnte nicht verarbeitet werden.{% endblock %}

+ + {% block details %}{% endblock %} + +
+ {% block actions %} + + Zum Chat + + + Zum Adminbereich + + {% endblock %} +
+
+
+ +
+ RAG-System · powered by mitho® +
+
+
+
+ +