add chat user roles and login
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 }
|
||||
|
||||
156
patch_history/RETRIEX_PATCH_88_CHAT_SECURITY_ROLES_README.md
Normal file
156
patch_history/RETRIEX_PATCH_88_CHAT_SECURITY_ROLES_README.md
Normal file
@@ -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.
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Markdown + Sanitizer -->
|
||||
<link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="/assets/styles/base.css">
|
||||
<link rel="shortcut icon" href="https://www.mitho-media.de/media/fc/16/42/1667224106/favicon.ico?ts=1767609928">
|
||||
|
||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/marked.min.js"></script>
|
||||
<script src="/assets/js/purify.min.js"></script>
|
||||
<script src="/assets/js/base.js"></script>
|
||||
</head>
|
||||
<body class="bg-black">
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
|
||||
<div>
|
||||
|
||||
<div class="d-flex">
|
||||
<img src="/assets/img/logo.png" style="max-height: 20px;">
|
||||
</div>
|
||||
<div class="text-info fw-bold" style="font-size: 12px" data-chat-message-text="ui.header_title"></div>
|
||||
</div>
|
||||
<img src="/assets/img/logo.svg" style="max-height: 20px;">
|
||||
<div class="spacer"></div>
|
||||
|
||||
<button id="clear" class="btn btn-trans" data-chat-message-text="ui.buttons.clear"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash-fill" viewBox="0 0 16 16">
|
||||
<path d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5M8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5m3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0"/>
|
||||
</svg></button>
|
||||
</div>
|
||||
<div id="ai-cloud" class="ai-cloud d-none"></div>
|
||||
<div id="chat" class="chat"></div>
|
||||
|
||||
<div id="retriex-chat-options" class="retriex-chat-options p-2" data-chat-message-aria-label="ui.options.aria_label">
|
||||
<label class="retriex-option-toggle" for="toggle-retriex-cards">
|
||||
<input id="toggle-retriex-cards" type="checkbox">
|
||||
<span data-chat-message-text="ui.options.status_info"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<textarea id="prompt" class="form-control bg-dark" data-chat-message-placeholder="ui.input.prompt_placeholder"></textarea>
|
||||
<button id="send" class="btn btn-trans" data-chat-message-text="ui.buttons.send"></button>
|
||||
<button id="abort" class="btn btn-trans" disabled data-chat-message-text="ui.buttons.abort"></button>
|
||||
</div>
|
||||
<div class="small mt-2 text-center text-secondary" data-chat-message-text="ui.footer_disclaimer"></div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
39
src/Controller/Chat/SecurityController.php
Normal file
39
src/Controller/Chat/SecurityController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Chat;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
/**
|
||||
* Login/logout endpoints for the chat area.
|
||||
*
|
||||
* The chat uses the existing App\Entity\User provider and only adds a separate
|
||||
* route space plus ROLE_CHAT_USER access control. It intentionally stays outside
|
||||
* App\Controller\Admin so Chat and Admin can evolve independently.
|
||||
*/
|
||||
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')) {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
68
templates/chat/security/login.html.twig
Normal file
68
templates/chat/security/login.html.twig
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Chat Login</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link href="{{ asset('assets/styles/bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link rel="shortcut icon" href="https://www.mitho-media.de/media/fc/16/42/1667224106/favicon.ico?ts=1767609928">
|
||||
</head>
|
||||
<body class="bg-black text-light">
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center" style="min-height:100vh;">
|
||||
<div class="col-12 col-md-5 col-lg-4">
|
||||
<div class="card bg-black border-secondary text-info">
|
||||
<div class="card-body">
|
||||
<header class="text-center mb-4">
|
||||
<img src="{{ asset('assets/img/logo.png') }}" style="max-width: 100px;" alt="">
|
||||
<h1 class="h5 mt-3 mb-1 text-info">RetrieX Chat</h1>
|
||||
<div class="small text-secondary">Login fuer den Chatbereich</div>
|
||||
</header>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ path('chat_login') }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input class="form-control bg-dark text-light border-secondary"
|
||||
name="_username"
|
||||
value="{{ last_username }}"
|
||||
autocomplete="email"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input class="form-control bg-dark text-light border-secondary"
|
||||
type="password"
|
||||
name="_password"
|
||||
autocomplete="current-password"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<input type="hidden"
|
||||
name="_csrf_token"
|
||||
value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<button class="btn btn-outline-info w-100" type="submit">
|
||||
In den Chat einloggen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3 small text-secondary">
|
||||
powered by mitho® 2026
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user