diff --git a/DELETE_PUBLIC_INDEX_HTML.txt b/DELETE_PUBLIC_INDEX_HTML.txt index 672c030..1bc52a7 100644 --- a/DELETE_PUBLIC_INDEX_HTML.txt +++ b/DELETE_PUBLIC_INDEX_HTML.txt @@ -1,7 +1,7 @@ -This patch intentionally removes public/index.html. +Important when applying this patch manually: -Reason: -If public/index.html remains in the document root, many web servers serve it before public/index.php and the Symfony route / will not reach App\Controller\Chat\ChatController. +Delete public/index.html from the target installation. -When applying this patch manually, delete: -public/index.html +The chat root route is now handled by Symfony via App\Controller\Chat\ChatController. +If public/index.html remains on disk, the web server may still serve the static file +before Symfony handles /, which would bypass the chat firewall and ROLE_CHAT_USER check. diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 9103aa5..431cce8 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -11,11 +11,12 @@ security: firewalls: - # 🔐 Admin zuerst! + # Admin area: same user provider, separate /admin route space. admin: pattern: ^/admin lazy: true provider: app_user_provider + context: retriex_user_area form_login: login_path: admin_login @@ -31,17 +32,49 @@ security: lifetime: 604800 path: /admin - # 🌍 Alles andere ist public (Chat etc.) + # Chat area: same user provider, separate route space and role gate. + chat: + pattern: ^/(?:$|chat(?:/|$)|ask-jobs(?:/|$)|ask-sse(?:/|$)|history(?:/|$)|chat-messages/frontend$) + lazy: true + provider: app_user_provider + context: retriex_user_area + + form_login: + login_path: chat_login + check_path: chat_login + default_target_path: chat_index + + logout: + path: chat_logout + target: chat_login + + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / + + # Everything outside Admin and Chat remains public/static. main: pattern: ^/ security: false role_hierarchy: - ROLE_SUPER_ADMIN: [ROLE_KNOWLEDGE_ADMIN, ROLE_EDITOR, ROLE_USER] - ROLE_KNOWLEDGE_ADMIN: [ROLE_EDITOR, ROLE_USER] - ROLE_EDITOR: [ROLE_USER] + ROLE_SUPER_ADMIN: [ROLE_KNOWLEDGE_ADMIN, ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER] + ROLE_KNOWLEDGE_ADMIN: [ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER] + ROLE_EDITOR: [ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER] + ROLE_ADMIN_AREA: [ROLE_USER] + ROLE_CHAT_USER: [ROLE_USER] access_control: - { path: ^/admin/login$, roles: PUBLIC_ACCESS } - { path: ^/admin/logout$, roles: PUBLIC_ACCESS } - - { path: ^/admin, roles: ROLE_USER } + - { path: ^/admin, roles: ROLE_ADMIN_AREA } + + - { path: ^/chat/login$, roles: PUBLIC_ACCESS } + - { path: ^/chat/logout$, roles: PUBLIC_ACCESS } + - { path: ^/$, roles: ROLE_CHAT_USER } + - { path: ^/chat$, roles: ROLE_CHAT_USER } + - { path: ^/ask-jobs, roles: ROLE_CHAT_USER } + - { path: ^/ask-sse, roles: ROLE_CHAT_USER } + - { path: ^/history, roles: ROLE_CHAT_USER } + - { path: ^/chat-messages/frontend$, roles: ROLE_CHAT_USER } diff --git a/patch_history/RETRIEX_PATCH_88_CHAT_SECURITY_ROLES_README.md b/patch_history/RETRIEX_PATCH_88_CHAT_SECURITY_ROLES_README.md new file mode 100644 index 0000000..22af4e8 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_88_CHAT_SECURITY_ROLES_README.md @@ -0,0 +1,156 @@ +# RetrieX Patch p88 - Chat Security Roles + +## Zweck + +p88 fuehrt eine getrennte Rollen-/Security-Schicht fuer den Chat ein, ohne eine zweite Userverwaltung anzulegen. Chat und Admin nutzen weiterhin denselben Symfony-User-Provider (`App\Entity\User`), werden aber ueber unterschiedliche Bereichsrollen freigeschaltet. + +Der Patch baut auf p87 auf: Der Chat wird bereits ueber einen eigenen Symfony-Controller gerendert. p88 schuetzt diesen Chat-Einstieg und die zugehoerigen Chat-API-Endpunkte nun mit `ROLE_CHAT_USER`. + +## Architekturentscheidung + +Die Userverwaltung bleibt zentral: + +- Entity: `App\Entity\User` +- Provider: `app_user_provider` +- Rollenfeld: bestehendes JSON-Feld `roles` +- Command: bestehender `mto:agent:user:create` + +Die Bereichsberechtigungen werden getrennt: + +- `ROLE_CHAT_USER` fuer Chat und Chat-API-Endpunkte +- `ROLE_ADMIN_AREA` fuer den Adminbereich +- `ROLE_USER` bleibt nur technische Basisrolle fuer eingeloggte User + +Wichtig: `ROLE_USER` ist keine Adminberechtigung mehr. Das ist noetig, weil `User::getRoles()` jedem User automatisch `ROLE_USER` gibt. + +## Aenderungen + +### `config/packages/security.yaml` + +- Admin-Firewall bleibt auf `^/admin` begrenzt. +- Neue Chat-Firewall fuer: + - `/` + - `/chat` + - `/chat/login` + - `/chat/logout` + - `/ask-jobs...` + - `/ask-sse...` + - `/history...` + - `/chat-messages/frontend` +- Beide Firewalls verwenden denselben Provider `app_user_provider`. +- Beide Firewalls verwenden denselben Security-Context `retriex_user_area`, damit die zentrale Userverwaltung und Session sauber gemeinsam genutzt werden koennen. +- Adminzugriff verlangt jetzt `ROLE_ADMIN_AREA` statt `ROLE_USER`. +- Chatzugriff verlangt `ROLE_CHAT_USER`. + +### Neue Rollenlogik + +```yaml +ROLE_SUPER_ADMIN: [ROLE_KNOWLEDGE_ADMIN, ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER] +ROLE_KNOWLEDGE_ADMIN: [ROLE_EDITOR, ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER] +ROLE_EDITOR: [ROLE_ADMIN_AREA, ROLE_CHAT_USER, ROLE_USER] +ROLE_ADMIN_AREA: [ROLE_USER] +ROLE_CHAT_USER: [ROLE_USER] +``` + +Damit gilt: + +- Chat-only User: `ROLE_CHAT_USER` +- Admin-only User: `ROLE_ADMIN_AREA` +- Editor/Knowledge/Super Admins behalten Adminzugriff und bekommen Chatzugriff ueber Hierarchie +- `ROLE_USER` alleine reicht fuer keinen Bereich + +### Neuer Chat-Login + +Neue Dateien: + +- `src/Controller/Chat/SecurityController.php` +- `templates/chat/security/login.html.twig` + +Routen: + +- `/chat/login` +- `/chat/logout` + +Der Controller liegt bewusst unter `App\Controller\Chat`, nicht unter `App\Controller\Admin`. + +### Admin-Login-Hardening + +`src/Controller/Admin/SecurityController.php` leitet eingeloggte User nur noch direkt zum Admin-Dashboard weiter, wenn sie `ROLE_ADMIN_AREA` haben. Ein reiner Chat-User wird dadurch nicht versehentlich in eine Admin-Weiterleitung geschickt. + +### Admin-Endpoint-Hardening + +`src/Controller/Admin/IngestJobController.php` nutzt fuer den Status-Endpunkt jetzt `ROLE_ADMIN_AREA` statt `ROLE_USER`. + +### User-Create-Command + +`src/Command/CreateUserCommand.php` wurde angepasst: + +- Beschreibung neutralisiert: erstellt nun einen Application-User, nicht nur Admin-User +- Rollenauswahl erweitert um `ROLE_ADMIN_AREA` und `ROLE_CHAT_USER` +- `ROLE_USER` aus der Auswahl entfernt, weil diese Rolle automatisch gesetzt wird und alleine keinen Bereich freischalten soll + +## Bewusst nicht geaendert + +- Keine neue User-Entity +- Keine neue User-Tabelle +- Keine neue Migration +- Keine Aenderung an RAG, Retrieval, Scoring, Ranking, Shop-Matching oder Prompt-Logik +- Keine Verschiebung bestehender Ask-/SSE-/History-Controller +- Keine Aenderung an `public/assets/js/base.js` +- Keine Aenderung an Chat-Frontend-Verhalten ausser Loginpflicht + +## Hinweis zu `public/index.html` + +Wie in p87 muss `public/index.html` geloescht sein. Wenn die Datei liegen bleibt, kann der Webserver `/` weiterhin statisch bedienen und Symfony samt Chat-Firewall umgehen. + +Im Full-ZIP ist `public/index.html` entfernt. Im Patch-only-ZIP liegt zusaetzlich `DELETE_PUBLIC_INDEX_HTML.txt` als manueller Hinweis. + +## Lokale Checks + +Ausgefuehrt: + +```bash +php -l src/Controller/Chat/SecurityController.php +php -l src/Controller/Chat/ChatController.php +php -l src/Controller/Admin/SecurityController.php +php -l src/Controller/Admin/IngestJobController.php +php -l src/Command/CreateUserCommand.php +python3 - <<'PY' +from pathlib import Path +import yaml +data = yaml.safe_load(Path('config/packages/security.yaml').read_text()) +assert data['security']['access_control'][2]['roles'] == 'ROLE_ADMIN_AREA' +assert data['security']['access_control'][5]['roles'] == 'ROLE_CHAT_USER' +assert not Path('public/index.html').exists() +print('p88 structural checks OK') +PY +``` + +Ergebnis: + +- PHP lint OK +- YAML parse OK +- Adminbereich ist nicht mehr ueber `ROLE_USER` freigeschaltet +- Chatbereich ist ueber `ROLE_CHAT_USER` freigeschaltet +- `public/index.html` ist im Full-ZIP entfernt + +## Noch in Zielumgebung ausfuehren + +```bash +php bin/console cache:clear +php bin/console debug:router | grep -E 'chat_login|chat_logout|chat_index|admin_login|admin_dashboard' +php bin/console debug:config security +php bin/console mto:agent:config:validate +php bin/console mto:agent:regression:test +php bin/console mto:agent:config:audit-source --details +php bin/console mto:agent:config:audit-patterns --details +``` + +Manuelle Smoke-Tests: + +1. Ohne Login `/` aufrufen: Redirect zu `/chat/login`. +2. User mit `ROLE_CHAT_USER` anlegen und einloggen: `/` und `/chat` oeffnen den Chat. +3. Chat-User ruft `/admin` auf: kein Adminzugriff. +4. Bestehender `ROLE_SUPER_ADMIN`, `ROLE_KNOWLEDGE_ADMIN` oder `ROLE_EDITOR` kann weiterhin `/admin` nutzen. +5. Admin-/Editor-User kann den Chat nutzen. +6. `/ask-jobs`, `/ask-sse/{jobId}`, `/history`, `/history/delete`, `/chat-messages/frontend` funktionieren nach Chat-Login. diff --git a/public/index.html b/public/index.html deleted file mode 100644 index c8a204a..0000000 --- a/public/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
-
- -
- -
- -
-
-
- -
- - -
-
-
- -
- -
- -
- - - -
-
-
- - - diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php index 1acfcc6..7bf1115 100644 --- a/src/Command/CreateUserCommand.php +++ b/src/Command/CreateUserCommand.php @@ -16,7 +16,7 @@ use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; #[AsCommand( name: 'mto:agent:user:create', - description: 'Creates a new admin user' + description: 'Creates a new application user' )] class CreateUserCommand extends Command { @@ -79,7 +79,8 @@ class CreateUserCommand extends Command 'ROLE_SUPER_ADMIN', 'ROLE_KNOWLEDGE_ADMIN', 'ROLE_EDITOR', - 'ROLE_USER', + 'ROLE_ADMIN_AREA', + 'ROLE_CHAT_USER', ], 0 ); diff --git a/src/Controller/Admin/IngestJobController.php b/src/Controller/Admin/IngestJobController.php index 256268f..66304df 100644 --- a/src/Controller/Admin/IngestJobController.php +++ b/src/Controller/Admin/IngestJobController.php @@ -50,7 +50,7 @@ final class IngestJobController extends AbstractController )] public function status(string $id, EntityManagerInterface $em): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_USER'); + $this->denyAccessUnlessGranted('ROLE_ADMIN_AREA'); $job = $this->findJob($id, $em); diff --git a/src/Controller/Admin/SecurityController.php b/src/Controller/Admin/SecurityController.php index 97ca73f..7d90a2e 100644 --- a/src/Controller/Admin/SecurityController.php +++ b/src/Controller/Admin/SecurityController.php @@ -14,7 +14,7 @@ final class SecurityController extends AbstractController public function login(AuthenticationUtils $authUtils): Response { // Wenn bereits eingeloggt → direkt ins Dashboard - if ($this->getUser()) { + if ($this->getUser() !== null && $this->isGranted('ROLE_ADMIN_AREA')) { return $this->redirectToRoute('admin_dashboard'); } diff --git a/src/Controller/Chat/SecurityController.php b/src/Controller/Chat/SecurityController.php new file mode 100644 index 0000000..22ade85 --- /dev/null +++ b/src/Controller/Chat/SecurityController.php @@ -0,0 +1,39 @@ +getUser() !== null && $this->isGranted('ROLE_CHAT_USER')) { + return $this->redirectToRoute('chat_index'); + } + + return $this->render('chat/security/login.html.twig', [ + 'last_username' => $authUtils->getLastUsername(), + 'error' => $authUtils->getLastAuthenticationError(), + ]); + } + + #[Route('/chat/logout', name: 'chat_logout', methods: ['GET'])] + public function logout(): void + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/templates/chat/security/login.html.twig b/templates/chat/security/login.html.twig new file mode 100644 index 0000000..cab8f26 --- /dev/null +++ b/templates/chat/security/login.html.twig @@ -0,0 +1,68 @@ + + + + + Chat Login + + + + + + + +
+
+
+
+
+
+ +

RetrieX Chat

+
Login fuer den Chatbereich
+
+ + {% if error %} +
+ {{ error.messageKey|trans(error.messageData, 'security') }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ + + + +
+
+
+ +
+ powered by mitho® 2026 +
+
+
+
+ + +