From f098a1a244b57b9095f242700fc0410802112981 Mon Sep 17 00:00:00 2001 From: team 1 Date: Mon, 11 May 2026 15:39:00 +0200 Subject: [PATCH] p96 follow up --- config/packages/security.yaml | 9 + config/retriex/chat-messages.yaml | 2 +- config/retriex/genre.yaml | 17 ++ ...H_95_ADMIN_ROLE_MATRIX_HARDENING_README.md | 200 ++++++++++++++++++ ...P_PRODUCT_IDENTITY_HISTORY_GUARD_README.md | 197 +++++++++++++++++ src/Agent/AgentRunner.php | 141 ++++++++++++ .../Admin/AdminSystemLogController.php | 5 +- .../Admin/AdminVectorLogController.php | 3 +- src/Controller/Admin/DocumentController.php | 17 +- .../Admin/DocumentTagController.php | 5 + src/Controller/Admin/IngestJobController.php | 5 +- .../Admin/IngestProfileController.php | 24 ++- .../Admin/ModelGenerationConfigController.php | 11 +- .../Admin/SystemAgentController.php | 3 +- .../Admin/SystemPromptController.php | 3 +- src/Controller/Admin/TagController.php | 9 + src/Controller/Admin/UserController.php | 9 +- src/Security/ApplicationRoles.php | 5 + templates/admin/base.html.twig | 90 ++++---- templates/admin/dashboard/index.html.twig | 28 ++- templates/admin/document/index.html.twig | 20 +- .../admin/document/new_version.html.twig | 2 +- templates/admin/document/show.html.twig | 62 +++--- templates/admin/ingest_profile/list.html.twig | 10 +- templates/admin/tag/index.html.twig | 44 ++-- 25 files changed, 790 insertions(+), 131 deletions(-) create mode 100644 patch_history/RETRIEX_PATCH_95_ADMIN_ROLE_MATRIX_HARDENING_README.md create mode 100644 patch_history/RETRIEX_PATCH_96_FOLLOWUP_PRODUCT_IDENTITY_HISTORY_GUARD_README.md diff --git a/config/packages/security.yaml b/config/packages/security.yaml index f279544..10eae60 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -73,6 +73,15 @@ security: - { path: ^/admin/login$, roles: PUBLIC_ACCESS } - { path: ^/admin/logout$, roles: PUBLIC_ACCESS } - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN } + - { path: ^/admin/system-logs, roles: ROLE_SUPER_ADMIN } + - { path: ^/admin/vector, roles: ROLE_SUPER_ADMIN } + - { path: '^/admin/system/(?:prompt|agent)', roles: ROLE_SUPER_ADMIN } + - { path: ^/admin/jobs/global-reindex, roles: ROLE_SUPER_ADMIN } + - { path: '^/admin/ingest-profiles/(?:create|activate|remove)', roles: ROLE_SUPER_ADMIN } + - { path: ^/admin/ingest-profiles, roles: ROLE_KNOWLEDGE_ADMIN } + - { path: ^/admin/model-config, roles: ROLE_KNOWLEDGE_ADMIN } + - { path: ^/admin/documents, roles: ROLE_EDITOR } + - { path: ^/admin/tags, roles: ROLE_EDITOR } - { path: ^/admin, roles: ROLE_ADMIN_AREA } - { path: ^/chat/login$, roles: PUBLIC_ACCESS } diff --git a/config/retriex/chat-messages.yaml b/config/retriex/chat-messages.yaml index 5b5e48c..55baae1 100644 --- a/config/retriex/chat-messages.yaml +++ b/config/retriex/chat-messages.yaml @@ -245,7 +245,7 @@ parameters: hide_when_material_query_matches_current: true shop_results: - label: Preis anzeigen - prompt: Zeige mir die Preise zu {shop_price_query}. + prompt: 'Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: {shop_price_query}.' action_type: price_details hide_when_answer_matches_any: - '/\b(?:preis(?:angabe|information|e)?|preise?)\b.{0,100}\b(?:nicht|kein(?:e|en)?|ohne)\b.{0,100}\b(?:angegeben|vorhanden|enthalten|ausgewiesen|gefunden|verfügbar|verfuegbar)\b/iu' diff --git a/config/retriex/genre.yaml b/config/retriex/genre.yaml index b03a90c..c3822d9 100644 --- a/config/retriex/genre.yaml +++ b/config/retriex/genre.yaml @@ -1208,6 +1208,14 @@ parameters: - kostet - shopsuche - shop-suche + - information + - informationen + - info + - infos + - details + - detail + - daten + - angaben context_fallback_filter_terms: - preis - preise @@ -1217,6 +1225,14 @@ parameters: - grenzwert - grenzwerte - grenzwerten + - information + - informationen + - info + - infos + - details + - detail + - daten + - angaben - welche - gut - geeignet @@ -1286,6 +1302,7 @@ parameters: - wuerde - will - brauche + - explizit - benötigen - benoetigen - benötige diff --git a/patch_history/RETRIEX_PATCH_95_ADMIN_ROLE_MATRIX_HARDENING_README.md b/patch_history/RETRIEX_PATCH_95_ADMIN_ROLE_MATRIX_HARDENING_README.md new file mode 100644 index 0000000..3d065bf --- /dev/null +++ b/patch_history/RETRIEX_PATCH_95_ADMIN_ROLE_MATRIX_HARDENING_README.md @@ -0,0 +1,200 @@ +# RetrieX Patch p95 - Admin Role Matrix Hardening + +## Ziel + +Dieser Patch gleicht die im Admin sichtbare Rollenlogik mit der serverseitigen Zugriffskontrolle ab. Rollen sollen nicht nur in der Userverwaltung auswählbar sein, sondern an den relevanten Routen, Controllern und UI-Aktionen konsistent wirksam werden. + +## Ausgangslage + +Nach p93/p94 waren Userverwaltung, Aktiv/Inaktiv-Login-Blocker und Access-Denied-Seiten funktionsfähig. Bei der Rollenprüfung fielen aber mehrere Inkonsistenzen auf: + +- `ROLE_EDITOR` existierte in der Hierarchie, wurde aber kaum als fachliche Serverberechtigung genutzt. +- Einige Aktionen waren im UI nur für Super-Admins sichtbar, serverseitig aber nur durch den allgemeinen `/admin`-Zugriff geschützt. +- Indexierungsprofile konnten serverseitig nicht streng genug zwischen Ansicht und kritischen Aktionen unterscheiden. +- `admin_ingest_profile_remove` erlaubte noch `GET` als Methode. +- Sidebar und Dashboard zeigten Links bzw. Aktionen, die für die jeweilige Rolle später nur in 403 endeten. + +## Umgesetzte Zielmatrix + +| Bereich | Effektive Rolle | +| --- | --- | +| Chat, Ask/SSE, History, Frontend-Messages | `ROLE_CHAT_USER` | +| Admin-Dashboard, Guides, allgemeine Adminbasis, Ingest-Job-Ansicht | `ROLE_ADMIN_AREA` | +| Dokumente, Dokumentversionen, Tags, Tag-Zuweisungen, Dokument-Ingest | `ROLE_EDITOR` | +| Modell-/Retrieval-Konfiguration lesen/testen, Ingest-Profile ansehen | `ROLE_KNOWLEDGE_ADMIN` | +| Userverwaltung, Logs, System Prompt, System Agent, Reset/Delete, Global Reindex, Profil-/Modell-Umschaltungen | `ROLE_SUPER_ADMIN` | + +Die bestehende Hierarchie bleibt erhalten: + +```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] +``` + +## Geänderte Dateien + +### Controller / Security + +- `config/packages/security.yaml` + - ergänzt konkrete `access_control`-Regeln vor dem generischen `/admin`-Fallback + - schützt Dokument-/Tag-Bereiche mit `ROLE_EDITOR` + - schützt Modell-/Ingest-Konfigbereiche mit `ROLE_KNOWLEDGE_ADMIN` + - schützt kritische Systembereiche mit `ROLE_SUPER_ADMIN` + +- `src/Security/ApplicationRoles.php` + - Dokumentation der zentralen Rollenquelle erweitert + +- `src/Controller/Admin/DocumentController.php` + - Dokumentliste/-details und alle Dokumentpflegeaktionen nun `ROLE_EDITOR` + - Reset/Delete bleiben `ROLE_SUPER_ADMIN` + - Rollenstrings auf `ApplicationRoles`-Konstanten umgestellt + +- `src/Controller/Admin/DocumentTagController.php` + - Tagbearbeitung auf Dokumentebene nun `ROLE_EDITOR` + +- `src/Controller/Admin/TagController.php` + - Tagliste, Taganlage, Löschung und Zuweisung nun `ROLE_EDITOR` + +- `src/Controller/Admin/IngestProfileController.php` + - Profilansicht nun `ROLE_KNOWLEDGE_ADMIN` + - Profilanlage, Aktivierung und Löschung nun `ROLE_SUPER_ADMIN` + - `remove` nur noch per `POST` + - Aktivieren/Löschen prüfen jetzt serverseitig CSRF-Token + +- `src/Controller/Admin/ModelGenerationConfigController.php` +- `src/Controller/Admin/IngestJobController.php` +- `src/Controller/Admin/AdminVectorLogController.php` +- `src/Controller/Admin/AdminSystemLogController.php` +- `src/Controller/Admin/SystemAgentController.php` +- `src/Controller/Admin/SystemPromptController.php` +- `src/Controller/Admin/UserController.php` + - Rollenstrings auf zentrale `ApplicationRoles`-Konstanten umgestellt + +### Templates + +- `templates/admin/base.html.twig` + - Sidebar blendet Rollenbereiche passend aus: + - User: Super-Admin + - Dokumente/Tags: Editor + - System Prompt/Agent/Logs: Super-Admin + - KI-/LLM-Setup und Ingest-Profile: Knowledge-Admin + +- `templates/admin/dashboard/index.html.twig` + - Global-Reindex-Button nur noch für Super-Admins sichtbar + - Nicht-Super-Admins sehen einen Hinweis statt eines Buttons, der in 403 läuft + +- `templates/admin/document/index.html.twig` +- `templates/admin/document/show.html.twig` +- `templates/admin/document/new_version.html.twig` + - Dokumentpflege- und Versionsaktionen auf `ROLE_EDITOR` ausgerichtet + - Dokumentlöschung bleibt `ROLE_SUPER_ADMIN` + +- `templates/admin/ingest_profile/list.html.twig` + - Profilanlage nur noch für Super-Admins sichtbar + - Aktivieren/Löschen bleiben Super-Admin-Aktionen + +- `templates/admin/tag/index.html.twig` + - Taganlage, Zuweisung und Löschung nur für Editor sichtbar + - Nicht-Editor sehen keine mutierenden Aktionen + +## Nicht geändert + +- RAG-/Retrieval-Logik +- Scoring +- Shop-Matching +- PromptBuilder +- AgentRunner +- Chat-Antwortlogik +- User-CRUD-Fachlogik aus p93 +- Error-Page-/Access-Denied-Grundlogik aus p94 + +## Lokale Checks + +Ausgeführt im ZIP-Stand ohne `vendor/`: + +```bash +find src/Controller/Admin src/Security src/Repository src/Service/Admin -type f -name '*.php' -print0 | xargs -0 -n1 php -l +python3 - <<'PY' +from pathlib import Path +import yaml + +yaml.safe_load(Path('config/packages/security.yaml').read_text()) +print('[OK] yaml parse') +PY +``` + +Zusätzlich wurde eine einfache Twig-Balance-Prüfung auf `{% if %}`, `{% for %}` und `{% block %}` für die geänderten Templates durchgeführt. + +## Empfohlene Checks in der Zielumgebung + +```bash +php bin/console cache:clear +php bin/console lint:yaml config/packages/security.yaml +php bin/console lint:twig templates/admin +php bin/console debug:router | grep admin_ +php bin/console mto:agent:config:validate +php bin/console mto:agent:regression:test +php bin/console mto:agent:config:audit-source --details +``` + +## Manuelle Rollentests + +### `ROLE_ADMIN_AREA` + +Soll können: + +- `/admin/dashboard` +- `/admin/guides` +- `/admin/jobs` + +Soll verweigert bekommen: + +- `/admin/documents` +- `/admin/tags` +- `/admin/model-config` +- `/admin/ingest-profiles` +- `/admin/users` + +### `ROLE_EDITOR` + +Soll können: + +- Dokumente ansehen/anlegen +- Dokumentversionen hochladen +- Versionen aktivieren +- Ingest für Dokumentversionen starten +- Tags anlegen/löschen/zuweisen + +Soll verweigert bekommen: + +- Userverwaltung +- System Prompt +- System Agent +- Systemlogs / Vectorlog +- Global Reindex +- Dokumentlöschung / kompletter Reset +- Ingest-Profil aktivieren/löschen + +### `ROLE_KNOWLEDGE_ADMIN` + +Soll können: + +- alles aus `ROLE_EDITOR` +- Modellkonfiguration ansehen/testen +- Ingest-Profile ansehen + +Soll verweigert bekommen: + +- Userverwaltung +- System Prompt/Agent +- Logs +- Profil-/Modell-Aktivierung und Löschung +- Global Reindex +- Reset/Delete + +### `ROLE_SUPER_ADMIN` + +Soll alles können. diff --git a/patch_history/RETRIEX_PATCH_96_FOLLOWUP_PRODUCT_IDENTITY_HISTORY_GUARD_README.md b/patch_history/RETRIEX_PATCH_96_FOLLOWUP_PRODUCT_IDENTITY_HISTORY_GUARD_README.md new file mode 100644 index 0000000..f211352 --- /dev/null +++ b/patch_history/RETRIEX_PATCH_96_FOLLOWUP_PRODUCT_IDENTITY_HISTORY_GUARD_README.md @@ -0,0 +1,197 @@ +# RetrieX Patch 96 - Follow-up Product Identity & Weak History Guard + +## Ziel + +Dieser Patch haertet zwei beobachtete Folgefrage-Fehlerklassen, ohne Retrieval, Scoring, Shop-Ranking oder Shop-Matching fachlich umzubauen: + +1. **Preis-Folgeaktionen nach mehreren sichtbaren Produkten** duerfen nicht mehr auf den ersten generischen Modellanker zurueckfallen. + - Fehlerfall: Antwort nennt `Testomat 2000 CLT` und `Testomat LAB CL`; der Button `Preis anzeigen` erzeugte nur `Testomat 2000`. + - Neu: Wenn mehrere in der Antwort sichtbare Produkte erkannt werden, wird die Preis-Folgeaktion aus den konkreten Produktidentitaeten mit Produktnummern gebaut. + +2. **Referenzielle Shop-Nachfragen mit schwacher Query** wie `suche im Shop nach der Information` duerfen den Produktfokus aus der History nicht verlieren. + - Fehlerfall: Nach `Testomat 2000 THCL` wurde die Shopquery zu `information` statt zum Verlaufanker `testomat 2000 thcl`. + - Neu: Schwache referenzielle Shopqueries, die nur aus Meta-/Info-/Noise-Tokens bestehen, werden mit dem letzten Produktmodellanker aus der History ersetzt. + +## Geaenderte Dateien + +- `src/Agent/AgentRunner.php` +- `config/retriex/chat-messages.yaml` +- `config/retriex/genre.yaml` + +## Technische Umsetzung + +### 1. Preis-Folgeaktionen behalten mehrere konkrete Produktidentitaeten + +`buildFollowUpActionPriceQuery()` faellt bei mehreren angezeigten Produkten nicht mehr auf `answer_anchor` zurueck. Stattdessen wird eine begrenzte Produktliste aus den sichtbaren Shopprodukten gebaut, inklusive Produktnummern. + +Beispiel erwarteter Button-Prompt: + +```text +Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: Testomat 2000® CLT 100137; Testomat® LAB CL 116106. +``` + +Der Prompt enthaelt bewusst `Produkt/Produkten` und `Preise`, damit die bestehende Product-List-Follow-up-Logik fuer mehrere Produkte greifen kann und einzelne Produktanker separat suchen kann. + +### 2. Schwache referenzielle Shopqueries nutzen History-Modellanker + +Neue Guard-Funktion: + +```php +guardWeakReferentialShopQueryWithHistoryModelAnchor() +``` + +Sie greift nur, wenn: + +- die aktuelle Frage referenziell genug ist, um History fuer Shopqueries zu nutzen, +- die Shopquery keinen eigenen Produktmodellanker enthaelt, +- der Prompt selbst keinen neuen Produktmodellanker enthaelt, +- es kein Product-List-Follow-up ist, +- es kein Indikator-/Zubehoer-/Reagenz-Follow-up ist, +- die Query nur aus schwachen Meta-/Info-/Noise-Tokens besteht, +- in der History ein Produktmodellanker gefunden wird. + +Die schwachen Info-Woerter werden in `genre.yaml` gepflegt: + +- `information` +- `informationen` +- `info` +- `infos` +- `details` +- `detail` +- `daten` +- `angaben` + +## Bewusst nicht geaendert + +- Keine neue fachliche Produktliste im PHP-Core. +- Keine neue Testomat-/THCL-/CLT-/LAB-CL-Sonderlogik im Core. +- Keine Aenderung an Retrieval, Scoring, Ranking, Shopware-Suche oder PromptBuilder-Faktenregeln. +- Bestehende Indikator-/Zubehoer-Follow-up-Guards bleiben priorisiert. + +## Lokale Checks + +Im Patch-Arbeitsverzeichnis ausgefuehrt: + +```bash +php -l src/Agent/AgentRunner.php +python3 - <<'PY' +import yaml, pathlib +for path in pathlib.Path('config/retriex').glob('*.yaml'): + with path.open(encoding='utf-8') as f: + yaml.safe_load(f) +print('all retriex yaml OK') +PY +``` + +Ergebnis: + +- `AgentRunner.php`: Syntax OK +- alle `config/retriex/*.yaml`: YAML OK + +Nicht lokal ausfuehrbar, weil `vendor/` im ZIP nicht enthalten ist: + +```bash +bin/console mto:agent:config:validate +bin/console mto:agent:regression:test +bin/console mto:agent:config:audit-source --details +bin/console mto:agent:config:audit-patterns --details +``` + +## Testprompts nach Einspielen + +### Fehlerfall 1: Preis-Folgeaktion nach zwei Chlor-Geraeten + +```text +mit welchem testomat kann ich freies chlor messen +``` + +Erwartung in der Antwort: + +- Hauptprodukte bleiben fokussiert auf die passenden Chlor-Geraete, insbesondere: + - `Testomat 2000® CLT` / `100137` + - `Testomat® LAB CL` / `116106` +- Folgeaktion `Preis anzeigen` darf nicht mehr nur `Testomat 2000` materialisieren. + +Danach den angezeigten Preis-Button klicken oder sinngleich testen: + +```text +Zeige mir die Preise zu folgendem Produkt bzw. den Produkten: Testomat 2000® CLT 100137; Testomat® LAB CL 116106. +``` + +Erwartung: + +- Shopquery/Split-Lookups fokussieren die konkreten Produktidentitaeten. +- Es duerfen nicht breit alle `Testomat 2000` Varianten wie Fe, Antox, Br2, ClO2 oder Wartungskoffer als Hauptantwort erscheinen. + +### Fehlerfall 2: schwache Info-Shopnachfrage verliert THCL-Anker + +```text +welche grenzwerte kann der testomat 2000 thcl messen +``` + +Dann: + +```text +suche im shop nach der information +``` + +Optional auch Tippfehler-Variante: + +```text +such eim shop nach der information +``` + +Erwartung: + +- Die finale Shopquery darf nicht `information` sein. +- Erwarteter Verlaufanker: `testomat 2000 thcl`. +- Shopantwort bleibt beim Geraet/Modellfokus und driftet nicht zu fremden Testomat-808-SiO2-Zubehoerteilen, Horiba-Testern oder allgemeinen Treffern ab. + +### Gruener Gegenflow: Brauwasser bleibt stabil + +```text +ich suche ein wasseranalyse messgerät für eine Brauerei, um das brauwasser messen zu können +``` + +Dann: + +```text +begründe deine auswahl +``` + +Dann: + +```text +was kostet das gerät denn +``` + +Erwartung: + +- Auswahl bleibt auf `Testomat 2000® CAL` fokussiert, sofern die Begruendung CAL als Hauptempfehlung gesetzt hat. +- Preis-Follow-up bleibt `testomat 2000 cal gerät` bzw. entsprechend CAL-fokussiert. +- Keine Regression hin zu breiten `testomat 2000` Varianten. + +### Bestehende Praezisionsregression: Indikator 300 bleibt stabil + +```text +Was ist der niedrigste Grenzwert für die Wasserhärte, welcher mit einem Testomaten überwacht werden kann? +``` + +Dann: + +```text +mit welchem indikator wird der wert gemessen +``` + +Dann: + +```text +was kostet der indikator +``` + +Erwartung: + +- `0,02 °dH` / `Testomat 808` +- `Indikatortyp 300` +- Shopquery weiterhin fokussiert, z. B. `testomat 808 300 indikator` +- Nur die zwei exakten Indikator-300-Produkte, keine `300 S`-Varianten und keine anderen Codes. diff --git a/src/Agent/AgentRunner.php b/src/Agent/AgentRunner.php index f153df8..a497ded 100644 --- a/src/Agent/AgentRunner.php +++ b/src/Agent/AgentRunner.php @@ -336,6 +336,26 @@ final readonly class AgentRunner $optimizedShopQuery = ''; } + $weakReferentialHistoryModelShopSearchQuery = $this->guardWeakReferentialShopQueryWithHistoryModelAnchor( + prompt: $originalPrompt, + shopSearchQuery: $shopSearchQuery, + commerceHistoryContext: $commerceHistoryContext + ); + + if ($weakReferentialHistoryModelShopSearchQuery !== $shopSearchQuery) { + $this->agentLogger->info('Replaced weak referential shop query with history model anchor', [ + 'userId' => $userId, + 'prompt' => $prompt, + 'routingPrompt' => $routingPrompt, + 'optimizedShopQuery' => $optimizedShopQuery, + 'shopSearchQuery' => $shopSearchQuery, + 'weakReferentialHistoryModelShopSearchQuery' => $weakReferentialHistoryModelShopSearchQuery, + ]); + + $shopSearchQuery = $weakReferentialHistoryModelShopSearchQuery; + $optimizedShopQuery = ''; + } + $productListAnchoredShopSearchQuery = $this->guardReferentialProductListShopQueryWithHistoryAnchors( prompt: $originalPrompt, shopSearchQuery: $shopSearchQuery, @@ -4148,6 +4168,84 @@ final readonly class AgentRunner : $modelAnchor; } + private function guardWeakReferentialShopQueryWithHistoryModelAnchor( + string $prompt, + string $shopSearchQuery, + string $commerceHistoryContext + ): string { + $shopSearchQuery = trim($shopSearchQuery); + + if ( + $shopSearchQuery === '' + || trim($commerceHistoryContext) === '' + || $this->referenceAnchorExtractor->extractFirstProductModelAnchor($prompt) !== '' + || $this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '' + || $this->isReferentialProductListShopFollowUpPrompt($prompt) + ) { + return $shopSearchQuery; + } + + if (!$this->shouldUseCommerceHistoryForShopQuery($prompt)) { + return $shopSearchQuery; + } + + // Accessory and consumable follow-ups have their own detail-anchor guard. + if ($this->containsConfiguredShopQueryAnchorTrigger(trim($prompt . ' ' . $shopSearchQuery))) { + return $shopSearchQuery; + } + + if (!$this->isWeakReferentialHistoryModelShopQuery($shopSearchQuery)) { + return $shopSearchQuery; + } + + $modelAnchor = $this->normalizeShopQueryAnchor( + $this->extractLatestHistoryProductModelAnchor($commerceHistoryContext) + ); + + if ($modelAnchor === '') { + return $shopSearchQuery; + } + + return $this->queryAlreadyContainsAllAnchorTokens($shopSearchQuery, $modelAnchor) + ? $shopSearchQuery + : $modelAnchor; + } + + private function isWeakReferentialHistoryModelShopQuery(string $shopSearchQuery): bool + { + if ($this->referenceAnchorExtractor->extractFirstProductModelAnchor($shopSearchQuery) !== '') { + return false; + } + + $tokens = $this->tokenizeShopQueryCandidate($shopSearchQuery); + if ($tokens === []) { + return true; + } + + $weakTokens = $this->buildShopQueryTokenSet($this->mergeUniqueStrings( + $this->mergeUniqueStrings( + $this->getShopQueryMetaGuardTerms(), + $this->getShopQueryContextFallbackFilterTerms() + ), + $this->mergeUniqueStrings( + $this->agentRunnerConfig->getShopQueryProductListFollowUpNoiseTerms(), + $this->agentRunnerConfig->getShopQueryStopwordCleanupTerms() + ) + )); + + if ($weakTokens === []) { + return false; + } + + foreach ($tokens as $token) { + if (!isset($weakTokens[$token])) { + return false; + } + } + + return true; + } + private function isMainDeviceReferentialShopQueryPrompt(string $prompt): bool { $tokens = $this->tokenizeShopQueryCandidate($prompt); @@ -6864,15 +6962,27 @@ final readonly class AgentRunner return $this->buildFollowUpActionProductQuery($numberedProducts[0]); } + if (count($numberedProducts) > 1) { + return $this->buildFollowUpActionProductListQuery($numberedProducts); + } + $focusedProducts = $this->filterFollowUpActionPrimaryDisplayedProducts($displayedProducts); if (count($focusedProducts) === 1) { return $this->buildFollowUpActionProductQuery($focusedProducts[0]); } + if (count($focusedProducts) > 1) { + return $this->buildFollowUpActionProductListQuery($focusedProducts); + } + if (count($displayedProducts) === 1) { return $this->buildFollowUpActionProductQuery($displayedProducts[0]); } + if (count($displayedProducts) > 1) { + return $this->buildFollowUpActionProductListQuery($displayedProducts); + } + $answerAnchor = $this->buildFollowUpActionAnswerAnchor($answerText); if ($answerAnchor !== '') { return $answerAnchor; @@ -6941,6 +7051,37 @@ final readonly class AgentRunner return $this->normalizeOneLine(implode(' ', array_filter($parts, static fn(string $part): bool => trim($part) !== ''))); } + /** + * @param ShopProductResult[] $products + */ + private function buildFollowUpActionProductListQuery(array $products): string + { + $queries = []; + $seen = []; + $maxProducts = max(1, $this->agentRunnerConfig->getShopQueryProductListFollowUpMaxAnchors()); + + foreach ($products as $product) { + if (!$product instanceof ShopProductResult) { + continue; + } + + $query = $this->buildFollowUpActionProductQuery($product); + $key = mb_strtolower($query, 'UTF-8'); + if ($query === '' || isset($seen[$key])) { + continue; + } + + $seen[$key] = true; + $queries[] = $query; + + if (count($queries) >= $maxProducts) { + break; + } + } + + return $this->normalizeOneLine(implode('; ', $queries)); + } + private function buildFollowUpActionAnswerAnchor(string $answerText): string { $anchors = []; diff --git a/src/Controller/Admin/AdminSystemLogController.php b/src/Controller/Admin/AdminSystemLogController.php index af0e56a..6f5f72b 100644 --- a/src/Controller/Admin/AdminSystemLogController.php +++ b/src/Controller/Admin/AdminSystemLogController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -21,7 +22,7 @@ final class AdminSystemLogController extends AbstractController #[Route('', name: 'admin_system_logs_index')] public function index(): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); $files = []; @@ -47,7 +48,7 @@ final class AdminSystemLogController extends AbstractController )] public function view(string $date): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if ($date === 'current') { $safeFilename = 'system.log'; diff --git a/src/Controller/Admin/AdminVectorLogController.php b/src/Controller/Admin/AdminVectorLogController.php index 7ce1bf1..ffefdc9 100644 --- a/src/Controller/Admin/AdminVectorLogController.php +++ b/src/Controller/Admin/AdminVectorLogController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -14,7 +15,7 @@ final class AdminVectorLogController extends AbstractController #[Route('/log', name: 'admin_vector_log')] public function view(): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); // ⚠️ Pfad ggf. anpassen falls bei dir anders konfiguriert $logFile = \dirname(__DIR__, 3) . '/var/log/vector_service.log'; diff --git a/src/Controller/Admin/DocumentController.php b/src/Controller/Admin/DocumentController.php index 60d2cf1..9b2f846 100644 --- a/src/Controller/Admin/DocumentController.php +++ b/src/Controller/Admin/DocumentController.php @@ -8,6 +8,7 @@ use App\Entity\Document; use App\Entity\DocumentVersion; use App\Entity\IngestJob; use App\Entity\User; +use App\Security\ApplicationRoles; use App\Service\DocumentService; use App\Service\FormatText; use App\Service\IngestJobService; @@ -33,6 +34,8 @@ final class DocumentController extends AbstractController #[Route('', name: 'admin_documents', methods: ['GET'])] public function index(EntityManagerInterface $em): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + $documents = $em->getRepository(Document::class) ->createQueryBuilder('d') ->leftJoin('d.versions', 'v') @@ -56,6 +59,8 @@ final class DocumentController extends AbstractController )] public function show(string $id, EntityManagerInterface $em): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + return $this->render('admin/document/show.html.twig', [ 'document' => $this->findDocument($id, $em), ]); @@ -70,6 +75,8 @@ final class DocumentController extends AbstractController ParameterBagInterface $params, EntityManagerInterface $em, ): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + if (!$request->isMethod('POST')) { return $this->render('admin/document/new.html.twig'); } @@ -150,6 +157,8 @@ final class DocumentController extends AbstractController ParameterBagInterface $params, FormatText $formatText, ): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + $document = $this->findDocument($id, $em); if (!$request->isMethod('POST')) { @@ -202,6 +211,8 @@ final class DocumentController extends AbstractController DocumentService $documentService, IngestJobService $jobService, ): RedirectResponse { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + if (!$this->isCsrfTokenValid('activate_version_' . $versionId, (string) $request->request->get('_token'))) { throw $this->createAccessDeniedException(); } @@ -260,6 +271,8 @@ final class DocumentController extends AbstractController EntityManagerInterface $em, IngestJobService $jobService, ): RedirectResponse { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, (string) $request->request->get('_token'))) { throw $this->createAccessDeniedException(); } @@ -329,7 +342,7 @@ final class DocumentController extends AbstractController ParameterBagInterface $params, Connection $connection, ): RedirectResponse { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if (!$this->isCsrfTokenValid('system_reset', (string) $request->request->get('_token'))) { $this->addFlash('danger', 'Ungültiges CSRF-Token.'); @@ -400,7 +413,7 @@ SQL; IngestJobService $jobService, LockService $lockService, ): RedirectResponse { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if (!$this->isCsrfTokenValid('delete_document_' . $id, (string) $request->request->get('_token'))) { throw $this->createAccessDeniedException(); diff --git a/src/Controller/Admin/DocumentTagController.php b/src/Controller/Admin/DocumentTagController.php index bc5710a..ad30c76 100644 --- a/src/Controller/Admin/DocumentTagController.php +++ b/src/Controller/Admin/DocumentTagController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Admin; use App\Entity\TagRebuildJob; +use App\Security\ApplicationRoles; use App\Service\Admin\DocumentTagAdminService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -19,6 +20,8 @@ final class DocumentTagController extends AbstractController #[Route('/{id}/tags', name: 'admin_document_tags_edit', methods: ['GET'])] public function edit(string $id, DocumentTagAdminService $svc): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + $id = trim($id); try { @@ -38,6 +41,8 @@ final class DocumentTagController extends AbstractController #[Route('/{id}/tags/save', name: 'admin_document_tags_save', methods: ['POST'])] public function save(string $id, Request $request, DocumentTagAdminService $svc): RedirectResponse { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + $id = trim($id); if (!$this->isCsrfTokenValid('admin_document_tags_save_' . $id, (string) $request->request->get('_token'))) { diff --git a/src/Controller/Admin/IngestJobController.php b/src/Controller/Admin/IngestJobController.php index 66304df..76cb656 100644 --- a/src/Controller/Admin/IngestJobController.php +++ b/src/Controller/Admin/IngestJobController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use App\Entity\IngestJob; use App\Service\IngestJobService; use Doctrine\ORM\EntityManagerInterface; @@ -50,7 +51,7 @@ final class IngestJobController extends AbstractController )] public function status(string $id, EntityManagerInterface $em): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ADMIN_AREA'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_ADMIN_AREA); $job = $this->findJob($id, $em); @@ -71,7 +72,7 @@ final class IngestJobController extends AbstractController IngestJobService $jobService, EntityManagerInterface $em, ): RedirectResponse { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if (!$this->isCsrfTokenValid('global_reindex', (string) $request->request->get('_token'))) { $this->addFlash('danger', 'Ungültiges CSRF-Token.'); diff --git a/src/Controller/Admin/IngestProfileController.php b/src/Controller/Admin/IngestProfileController.php index 99d8a11..f835fc3 100644 --- a/src/Controller/Admin/IngestProfileController.php +++ b/src/Controller/Admin/IngestProfileController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Admin; use App\Entity\IngestProfile; +use App\Security\ApplicationRoles; use App\Service\Admin\IngestProfileAdminService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -17,6 +18,8 @@ final class IngestProfileController extends AbstractController #[Route('/', name: 'admin_ingest_profile_list')] public function list(IngestProfileAdminService $svc): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN); + $data = $svc->listData(); return $this->render('admin/ingest_profile/list.html.twig', $data); @@ -25,6 +28,8 @@ final class IngestProfileController extends AbstractController #[Route('/create', name: 'admin_ingest_profile_create', methods: ['GET', 'POST'])] public function create(Request $request, IngestProfileAdminService $svc): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); + if ($request->isMethod('POST')) { try { $svc->create([ @@ -49,8 +54,15 @@ final class IngestProfileController extends AbstractController #[Route('/activate/{id}', name: 'admin_ingest_profile_activate', methods: ['POST'])] public function activate( IngestProfile $profile, - IngestProfileAdminService $svc + IngestProfileAdminService $svc, + Request $request ): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); + + if (!$this->isCsrfTokenValid('activate_ingest_profile_' . $profile->getId(), (string) $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + try { $svc->activate($profile); $this->addFlash('success', 'Ingest-Profil wurde aktiviert.'); @@ -61,9 +73,15 @@ final class IngestProfileController extends AbstractController return $this->redirectToRoute('admin_ingest_profile_list'); } - #[Route('/remove/{id}', name: 'admin_ingest_profile_remove', methods: ['POST', 'GET'])] - public function remove(string $id, IngestProfileAdminService $svc): Response + #[Route('/remove/{id}', name: 'admin_ingest_profile_remove', methods: ['POST'])] + public function remove(string $id, IngestProfileAdminService $svc, Request $request): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); + + if (!$this->isCsrfTokenValid('delete_ingest_profile_' . $id, (string) $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + try { $svc->remove($id); $this->addFlash('success', 'Ingest-Profil wurde entfernt.'); diff --git a/src/Controller/Admin/ModelGenerationConfigController.php b/src/Controller/Admin/ModelGenerationConfigController.php index 6a8cf12..640eac1 100644 --- a/src/Controller/Admin/ModelGenerationConfigController.php +++ b/src/Controller/Admin/ModelGenerationConfigController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use App\Entity\ModelGenerationConfig; use App\Service\Admin\ModelGenerationConfigAdminService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -17,7 +18,7 @@ final class ModelGenerationConfigController extends AbstractController #[Route('/', name: 'admin_model_config_list')] public function list(ModelGenerationConfigAdminService $svc): Response { - $this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN); return $this->render('admin/model_config/list.html.twig', [ 'configs' => $svc->list(), @@ -29,7 +30,7 @@ final class ModelGenerationConfigController extends AbstractController Request $request, ModelGenerationConfigAdminService $svc ): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if ($request->isMethod('POST')) { try { @@ -50,7 +51,7 @@ final class ModelGenerationConfigController extends AbstractController ModelGenerationConfig $config, ModelGenerationConfigAdminService $svc ): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); $svc->activate($config); @@ -63,7 +64,7 @@ final class ModelGenerationConfigController extends AbstractController Request $request, ModelGenerationConfigAdminService $svc ): Response { - $this->denyAccessUnlessGranted('ROLE_KNOWLEDGE_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_KNOWLEDGE_ADMIN); $prompt = ''; $results = []; @@ -86,7 +87,7 @@ final class ModelGenerationConfigController extends AbstractController Request $request, ModelGenerationConfigAdminService $svc ): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if (!$this->isCsrfTokenValid( 'delete_model_config_'.$config->getId(), diff --git a/src/Controller/Admin/SystemAgentController.php b/src/Controller/Admin/SystemAgentController.php index 5c12001..a65fa65 100644 --- a/src/Controller/Admin/SystemAgentController.php +++ b/src/Controller/Admin/SystemAgentController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use App\Service\Admin\IndexNdjsonInspector; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_SUPER_ADMIN')] +#[IsGranted(ApplicationRoles::ROLE_SUPER_ADMIN)] final class SystemAgentController extends AbstractController { #[Route('/admin/system/agent', name: 'admin_system_agent', methods: ['GET'])] diff --git a/src/Controller/Admin/SystemPromptController.php b/src/Controller/Admin/SystemPromptController.php index ff9a13b..2adad37 100644 --- a/src/Controller/Admin/SystemPromptController.php +++ b/src/Controller/Admin/SystemPromptController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use App\Entity\SystemPrompt; use App\Service\Admin\SystemPromptAdminService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -12,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; -#[IsGranted('ROLE_SUPER_ADMIN')] +#[IsGranted(ApplicationRoles::ROLE_SUPER_ADMIN)] final class SystemPromptController extends AbstractController { #[Route('/admin/system/prompt', name: 'admin_system_prompt', methods: ['GET', 'POST'])] diff --git a/src/Controller/Admin/TagController.php b/src/Controller/Admin/TagController.php index 317793d..4acec95 100644 --- a/src/Controller/Admin/TagController.php +++ b/src/Controller/Admin/TagController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Admin; use App\Entity\TagRebuildJob; +use App\Security\ApplicationRoles; use App\Service\Admin\TagAdminService; use App\Tag\TagTypes; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -19,6 +20,8 @@ final class TagController extends AbstractController #[Route('', name: 'admin_tags_index', methods: ['GET'])] public function index(TagAdminService $svc): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + return $this->render('admin/tag/index.html.twig', [ ...$svc->getIndexData(), ...$this->buildJobStatusViewData(), @@ -28,6 +31,8 @@ final class TagController extends AbstractController #[Route('/create', name: 'admin_tags_create', methods: ['POST'])] public function create(Request $request, TagAdminService $svc): RedirectResponse { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + if (!$this->isCsrfTokenValid('admin_tag_create', (string) $request->request->get('_token'))) { $this->addFlash('danger', 'Ungültiges CSRF-Token.'); @@ -53,6 +58,8 @@ final class TagController extends AbstractController #[Route('/{id}/delete', name: 'admin_tags_delete', methods: ['POST'])] public function delete(string $id, Request $request, TagAdminService $svc): RedirectResponse { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + if (!$this->isCsrfTokenValid('admin_tag_delete_' . $id, (string) $request->request->get('_token'))) { $this->addFlash('danger', 'Ungültiges CSRF-Token.'); @@ -72,6 +79,8 @@ final class TagController extends AbstractController #[Route('/{id}/assign', name: 'admin_tags_assign', methods: ['GET', 'POST'])] public function assign(string $id, Request $request, TagAdminService $svc): Response { + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_EDITOR); + $id = trim($id); if ($request->isMethod('POST')) { diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php index 6c3b904..5dc72db 100644 --- a/src/Controller/Admin/UserController.php +++ b/src/Controller/Admin/UserController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Admin; +use App\Security\ApplicationRoles; use App\Entity\User; use App\Service\Admin\UserAdminService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -19,7 +20,7 @@ final class UserController extends AbstractController #[Route('', name: 'admin_users_index', methods: ['GET'])] public function index(Request $request, UserAdminService $users): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); $query = trim((string) $request->query->get('q', '')); $status = (string) $request->query->get('status', 'all'); @@ -44,7 +45,7 @@ final class UserController extends AbstractController #[Route('/create', name: 'admin_users_create', methods: ['GET', 'POST'])] public function create(Request $request, UserAdminService $users): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if (!$request->isMethod('POST')) { return $this->render('admin/user/create.html.twig', [ @@ -80,7 +81,7 @@ final class UserController extends AbstractController #[Route('/{id}/edit', name: 'admin_users_edit', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['GET', 'POST'])] public function edit(string $id, Request $request, UserAdminService $users): Response { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); try { $user = $users->requireUser($id); @@ -127,7 +128,7 @@ final class UserController extends AbstractController #[Route('/{id}/toggle-active', name: 'admin_users_toggle_active', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['POST'])] public function toggleActive(string $id, Request $request, UserAdminService $users): RedirectResponse { - $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + $this->denyAccessUnlessGranted(ApplicationRoles::ROLE_SUPER_ADMIN); if (!$this->isCsrfTokenValid('admin_user_toggle_active_' . $id, (string) $request->request->get('_token'))) { $this->addFlash('danger', 'Ungültiges CSRF-Token.'); diff --git a/src/Security/ApplicationRoles.php b/src/Security/ApplicationRoles.php index aa65a10..4671418 100644 --- a/src/Security/ApplicationRoles.php +++ b/src/Security/ApplicationRoles.php @@ -17,6 +17,11 @@ final class ApplicationRoles public const ROLE_USER = 'ROLE_USER'; /** + * Roles that can be assigned from the Admin UI or console. + * + * Effective access is backed by config/packages/security.yaml and by + * controller-level guards for action-specific permissions. + * * @return array */ public static function assignableChoices(): array diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig index 50eee47..aaae130 100644 --- a/templates/admin/base.html.twig +++ b/templates/admin/base.html.twig @@ -77,23 +77,27 @@ RAG Dokumente & Wissen - - Dokumente - + {% if is_granted('ROLE_EDITOR') %} + + Dokumente + - {# ------------------------- #} - {# Tags (Document Routing) #} - {# ------------------------- #} - - Tags - + {# ------------------------- #} + {# Tags (Document Routing) #} + {# ------------------------- #} + + Tags + + {% endif %} - - Wissensbasis (Chunk-Index) - + {% if is_granted('ROLE_SUPER_ADMIN') %} + + Wissensbasis (Chunk-Index) + + {% endif %}
@@ -101,15 +105,19 @@ RAG System-Profile - - System Prompt - + {% if is_granted('ROLE_SUPER_ADMIN') %} + + System Prompt + + {% endif %} - - Indexierungsprofile (Ingest) - + {% if is_granted('ROLE_KNOWLEDGE_ADMIN') %} + + Indexierungsprofile (Ingest) + + {% endif %}
@@ -117,14 +125,16 @@ KI-Endpunkte - - KI-/LLM-Setup - - - KI-Agent Live-Test - + {% if is_granted('ROLE_KNOWLEDGE_ADMIN') %} + + KI-/LLM-Setup + + + KI-Agent Live-Test + + {% endif %}
System-Guiide @@ -142,14 +152,16 @@ href="{{ path('admin_jobs') }}"> Indexierungs-Log (Ingest Jobs) - - Vector-Log Python - - - System-Logs - + {% if is_granted('ROLE_SUPER_ADMIN') %} + + Vector-Log Python + + + System-Logs + + {% endif %} diff --git a/templates/admin/dashboard/index.html.twig b/templates/admin/dashboard/index.html.twig index 823d226..2bb126e 100644 --- a/templates/admin/dashboard/index.html.twig +++ b/templates/admin/dashboard/index.html.twig @@ -226,19 +226,25 @@ physischen Retrieval-Artefakte wieder gerade.
-
+ {% if is_granted('ROLE_SUPER_ADMIN') %} + - + - -
+ + + {% else %} +
+ Global Reindex ist Super-Admins vorbehalten. +
+ {% endif %} {% if anyHealthIssue %}
diff --git a/templates/admin/document/index.html.twig b/templates/admin/document/index.html.twig index bf326c9..3de31fd 100644 --- a/templates/admin/document/index.html.twig +++ b/templates/admin/document/index.html.twig @@ -14,10 +14,12 @@
- - Neues Dokument - + {% if is_granted('ROLE_EDITOR') %} + + Neues Dokument + + {% endif %} {% for message in app.flashes('success') %} @@ -189,10 +191,12 @@ - {% if is_granted('ROLE_SUPER_ADMIN') %} + {% if is_granted('ROLE_EDITOR') %}
- - Tags bearbeiten - + {% if is_granted('ROLE_EDITOR') %} + + Tags bearbeiten + + {% endif %} @@ -96,26 +98,30 @@
- {% if is_granted('ROLE_SUPER_ADMIN') %} + {% if is_granted('ROLE_EDITOR') or is_granted('ROLE_SUPER_ADMIN') %}
- - Neue Version - + {% if is_granted('ROLE_EDITOR') %} + + Neue Version + + {% endif %} -
- - -
+ {% if is_granted('ROLE_SUPER_ADMIN') %} +
+ + +
+ {% endif %}
{% endif %} @@ -128,10 +134,12 @@
Tags
- - Bearbeiten - + {% if is_granted('ROLE_EDITOR') %} + + Bearbeiten + + {% endif %}
{% if document.tags is empty %} @@ -172,7 +180,7 @@ - {% if is_granted('ROLE_SUPER_ADMIN') %} + {% if is_granted('ROLE_EDITOR') %} Neue Version @@ -258,7 +266,7 @@ {# ========================================================= #} diff --git a/templates/admin/tag/index.html.twig b/templates/admin/tag/index.html.twig index 50baff3..80ac288 100644 --- a/templates/admin/tag/index.html.twig +++ b/templates/admin/tag/index.html.twig @@ -106,9 +106,10 @@ }); -
-
-
Neuen Tag hinzufügen
+ {% if is_granted('ROLE_EDITOR') %} +
+
+
Neuen Tag hinzufügen
@@ -154,8 +155,9 @@
+
-
+ {% endif %}
@@ -207,23 +209,27 @@ {{ tag.description ?: '-' }} - - Zuweisen - + {% if is_granted('ROLE_EDITOR') %} + + Zuweisen + -
- + + - -
+ + + {% else %} + Nur Ansicht + {% endif %} {% else %}