fixed proper splitting sysadmin/platformadmin and proper logic for mandate name(slug) and label(user)
This commit is contained in:
parent
9c9b1cdd10
commit
2596bd60c0
10 changed files with 1146 additions and 4 deletions
|
|
@ -33,6 +33,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
|
|||
|-------|-------|------------|
|
||||
| Neutralisierung | b-reference/platform/neutralization.md | Datenschutz, AI-Call-Pipeline, Failsafe |
|
||||
| RBAC | b-reference/platform/rbac.md | 4-Stufen-Modell, Template-Rollen, Resolution, Datenmodell |
|
||||
| Mandate-Identifier | b-reference/platform/mandate.md | `name` (Kurzzeichen) vs. `label` (Voller Name), Slug-Generierung, RBAC-Editierbarkeit, Bootstrap-Migration |
|
||||
| Datenbank-Architektur | b-reference/platform/database-architecture.md | Interface-Pattern, Connector, Auto-Init, DB-Liste, Database Health, Orphan-Scanner |
|
||||
| Navigation | b-reference/platform/navigation.md | Menü-Struktur, Admin-Seiten, API |
|
||||
| Compliance & AI-Audit | b-reference/platform/audit.md | AI-Datenfluss-Log, Security-Audit, Statistiken, RBAC |
|
||||
|
|
|
|||
96
b-reference/platform/mandate.md
Normal file
96
b-reference/platform/mandate.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<!-- status: canonical -->
|
||||
<!-- lastReviewed: 2026-04-18 -->
|
||||
<!-- verifiedAgainst: gateway + frontend_nyla (codebase audit 2026-04-18) -->
|
||||
|
||||
# Mandate-Identifier: `name` (Kurzzeichen) und `label` (Voller Name)
|
||||
|
||||
## Ueberblick
|
||||
|
||||
Ein **Mandate** (Mandant / Tenant) hat zwei semantisch getrennte Bezeichner und einen technischen Primaerschluessel:
|
||||
|
||||
| Feld | Rolle | Aenderbarkeit | Format |
|
||||
| ------- | --------------------------------------------- | -------------------------------------- | ------------------------------- |
|
||||
| `id` | Technischer PK / FK-Anker (UUID) | Niemals (auch nicht durch SysAdmin) | UUID v4 |
|
||||
| `name` | **Kurzzeichen** — global eindeutiger Slug, technischer Identifier in URLs/Logs/Audit | Nur **PlatformAdmin** oder **SysAdmin** | `^[a-z0-9]+(-[a-z0-9]+)*$`, Laenge 2–32 |
|
||||
| `label` | **Voller Name** — Anzeige-Name fuer das UI | **MandateAdmin** und hoeher | non-empty String, beliebige Zeichen |
|
||||
|
||||
`id` bleibt der einzige FK-Anker. Saemtliche Tabellen (`Role.mandateId`, `UserMandate.mandateId`, `MandateSubscription.mandateId`, `BillingSettings.mandateId`, …) referenzieren `id`, **nicht** `name`. Rebranding (Label-Wechsel) bricht damit niemals Audit-Trails oder Foreign Keys.
|
||||
|
||||
## Warum zwei Felder?
|
||||
|
||||
- **Audit-Stabilitaet:** Audit-Reports und Logs zeigen `name` als kompakten, stabilen Code. Wenn ein Kunde sein Anzeige-Label aendert ("Mueller AG" → "Mueller Holding"), bleibt der Code (`mueller-ag`) gleich. Sonst wuerden historische Reports unleserlich werden.
|
||||
- **URL-/API-Friendliness:** `name` ist URL-tauglich (lowercase, keine Leerzeichen, keine Sonderzeichen). Zukuenftige Subdomain- oder Pfad-Routing-Features koennen `name` direkt verwenden.
|
||||
- **Klare Rollen-Trennung:** MandateAdmin darf sein Label umbenennen, ohne den globalen Identifier zu beeinflussen; nur Plattform-Governance darf den Identifier touchieren.
|
||||
|
||||
## Format-Regeln
|
||||
|
||||
`name` muss erfuellen:
|
||||
|
||||
- nur `a-z`, `0-9`, `-`
|
||||
- nicht mit `-` beginnen oder enden
|
||||
- keine doppelten Bindestriche
|
||||
- Laenge 2–32 Zeichen
|
||||
|
||||
`label` muss:
|
||||
|
||||
- nicht-leer (nach Trim)
|
||||
- darf alle Zeichen enthalten (Umlaute, Akzente, Punkte, Leerzeichen)
|
||||
|
||||
## Slug-Generierung (Auto-Allokation)
|
||||
|
||||
Wenn `name` bei einem POST nicht angegeben oder leer ist, generiert der Server ihn aus `label`:
|
||||
|
||||
1. **Transliteration**: `ae | oe | ue | ss` fuer `ä | ö | ü | ß` (auch Grossvarianten).
|
||||
2. **Lowercasen** und alles, was nicht `[a-z0-9]` ist, zu `-` ersetzen.
|
||||
3. **Bindestriche kollabieren** und an Raendern trimmen.
|
||||
4. **Min-Laenge sichern** (Auffuellen mit `x`).
|
||||
5. **Max-Laenge erzwingen** (an letztem Bindestrich ueberhalb der Laengenschranke abschneiden).
|
||||
6. **Kollisions-Suffix** `-2`, `-3`, … wenn der Basis-Slug bereits existiert.
|
||||
|
||||
Beispiele:
|
||||
|
||||
| Label | Erzeugter `name` |
|
||||
| ------------------------- | ------------------------ |
|
||||
| `Müller AG` | `mueller-ag` |
|
||||
| `Home Patrick.Möller` | `home-patrick-moeller` |
|
||||
| `Müller AG` (zweites Mal) | `mueller-ag-2` |
|
||||
| ` ` (leer) | `mn` (Fallback) |
|
||||
|
||||
Code: `gateway/modules/shared/mandateNameUtils.py` (Backend) und `frontend_nyla/src/utils/slugUtils.ts` + `mandateNameUtils.ts` (Frontend, mirror).
|
||||
|
||||
## RBAC und Editierbarkeit
|
||||
|
||||
| Operation | Rolle | Verhalten |
|
||||
| ------------------------- | -------------------------------------- | ------------------------------------------------- |
|
||||
| `POST /api/mandates/` | PlatformAdmin | `label` mandatory; `name` optional → Server generiert |
|
||||
| `PUT /api/mandates/{id}` `name` | PlatformAdmin oder SysAdmin | Format + Uniqueness validiert (excl. self) |
|
||||
| `PUT /api/mandates/{id}` `name` | MandateAdmin | **Ignoriert** (Whitelist `_MANDATE_ADMIN_EDITABLE_FIELDS = {"label"}`) |
|
||||
| `PUT /api/mandates/{id}` `label` | MandateAdmin oder hoeher | Pflicht: nicht-leer (nach Trim) |
|
||||
| `PUT /api/mandates/{id}` `isSystem` | SysAdmin | Nur SysAdmin |
|
||||
| `DELETE /api/mandates/{id}?force=true` | PlatformAdmin (X-Confirm-Name=`name`) | Hard-Delete mit Cascade |
|
||||
|
||||
Im UI ist das Kurzzeichen-Feld in den Mandate-Formularen vom Typ `slug` (Live-Maskierung, Auto-Vorschlag aus `label` im Create-Modus, im Edit fuer non-PlatformAdmin disabled).
|
||||
|
||||
## Bootstrap-Migration
|
||||
|
||||
`interfaceBootstrap._migrateMandateNameLabelSlugRules()` laeuft idempotent beim Boot und bringt Bestandsdaten auf das neue Schema:
|
||||
|
||||
1. Wenn `label` leer/None → `label := name`.
|
||||
2. Wenn `name` nicht regex-konform → Slug aus `label` generieren mit Kollisions-Suffix.
|
||||
3. Stable order ueber `id`-String, deterministisch.
|
||||
4. Zweiter Lauf ist No-op.
|
||||
|
||||
Tests: `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`.
|
||||
|
||||
## UI-Anzeige (Konvention)
|
||||
|
||||
- Listen / Auswahlen / Header → `mandateDisplayLabel(m)` = `label || name || id` (Frontend-Util `mandateDisplayUtils.ts`).
|
||||
- Detail-Pages mit Identifier-Bezug → `mandateDisplayLineLabelThenSlug(m)` = `Voller Name (kurzzeichen)` wenn beide vorhanden und unterschiedlich.
|
||||
- Hard-Delete-Confirm → fragt explizit nach `name` (Kurzzeichen) ab.
|
||||
|
||||
## Verwandte Dateien
|
||||
|
||||
- Backend: `gateway/modules/datamodels/datamodelUam.py` (Pydantic-Modell), `gateway/modules/interfaces/interfaceDbApp.py` (`createMandate`, `updateMandate`, `_provisionMandateForUser`, `_generateUniqueMandateName`), `gateway/modules/routes/routeDataMandates.py`, `gateway/modules/shared/mandateNameUtils.py`.
|
||||
- Frontend: `frontend_nyla/src/types/mandate.ts`, `frontend_nyla/src/api/mandateApi.ts`, `frontend_nyla/src/utils/slugUtils.ts`, `frontend_nyla/src/utils/mandateNameUtils.ts`, `frontend_nyla/src/utils/mandateDisplayUtils.ts`, `frontend_nyla/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` (slug-Type Renderer).
|
||||
- Tests: `gateway/tests/unit/shared/test_mandateNameUtils.py`, `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`, `gateway/tests/integration/mandates/`.
|
||||
- Plan-Doku: `wiki/c-work/1-plan/2026-04-mandate-name-label-logic.md`.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- status: canonical -->
|
||||
<!-- lastReviewed: 2026-04-05 -->
|
||||
<!-- verifiedAgainst: gateway (codebase audit 2026-04-05) -->
|
||||
<!-- lastReviewed: 2026-04-17 -->
|
||||
<!-- verifiedAgainst: gateway (codebase audit 2026-04-17) -->
|
||||
|
||||
# RBAC-System
|
||||
|
||||
|
|
@ -8,6 +8,29 @@
|
|||
|
||||
Das Role-Based Access Control baut auf vier Stufen auf: **System**, **Mandant**, **Feature** und **Feature-Instanz**. Berechtigungen werden in **Access Rules** pro Rollenlabel und Kontext (DATA, UI, RESOURCE) gebunden. Auswertung erfolgt zentral ueber `interfaceRbac.py`; Zielbild: moeglichst **filternd in SQL** statt vollstaendiger Tabellen-Scans in Python. Nutzer koennen mehrere Rollenlabels gleichzeitig tragen; die effektive Berechtigung entsteht aus **Oeffnungslogik (Union)** ueber alle Rollen.
|
||||
|
||||
## Platform-Governance-Autoritaet (zwei orthogonale Flags)
|
||||
|
||||
Neben den Mandanten-scoped Rollen kennt das System zwei **plattformweite User-Flags** auf der `User`-Tabelle. Sie sind **einzeln vergebbar** und decken zwei unabhaengige Autoritaets-Achsen ab:
|
||||
|
||||
| Flag | Zweck | RBAC-Bypass | FastAPI-Dependency |
|
||||
| --------------------- | -------------------------------------------------------------- | ----------- | ------------------------- |
|
||||
| `isSysAdmin` | Infrastruktur-Operator (Logs, Tokens, DB-Health, i18n-Master, global-scoped Files/Sources) | **ja** | `requireSysAdmin` |
|
||||
| `isPlatformAdmin` | Cross-Mandate-Governance (User-/Mandate-/RBAC-/Feature-Registry-Verwaltung ueber alle Mandanten) | **nein** | `requirePlatformAdmin` |
|
||||
|
||||
Wichtig:
|
||||
|
||||
- `isSysAdmin` wirkt als **harter RBAC-Engine-Bypass** in `rbac.py:getUserPermissions`. Reserviert fuer Infrastruktur-Operationen, die unabhaengig vom RBAC-Schema funktionieren muessen.
|
||||
- `isPlatformAdmin` gibt **keinen** impliziten Daten-Zugriff. Er erlaubt nur den Zugriff auf Admin-Routen, die explizit `requirePlatformAdmin` deklarieren (z.B. `routeDataMandates`, `routeAdminRbacRules`, `routeBilling`-Cross-Mandate, `routeAdminUserAccessOverview`).
|
||||
- Die historische `sysadmin`-Rolle im Root-Mandant wurde **eliminiert**. Eine einmalige idempotente Migration in `interfaceBootstrap._migrateAndDropSysAdminRole()` befoerdert ehemalige Rolleninhaber zu `isPlatformAdmin=True` und entfernt Rolle, AccessRules und `UserMandateRole`-Eintraege.
|
||||
|
||||
Beispiel-Profile:
|
||||
|
||||
- "Operations-Engineer" → `isSysAdmin=true`, `isPlatformAdmin=false` (kann Logs und DB pruefen, aber keine User/Mandate verwalten).
|
||||
- "Customer-Success-Admin" → `isSysAdmin=false`, `isPlatformAdmin=true` (kann Mandanten deblockieren, User-Access-Overview sehen, aber nicht in Server-Logs schauen).
|
||||
- "Plattform-Super-Admin" → beide auf `true`.
|
||||
|
||||
Details + Migrations-Plan: `wiki/c-work/4-done/2026-04-sysadmin-authority-split.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4-Stufen-Hierarchie
|
||||
|
|
@ -44,8 +67,8 @@ System (PowerOn Platform)
|
|||
|
||||
### Sonderfaelle
|
||||
|
||||
- **SysAdmin**: Eine globale Rolle auf Root-Mandant-Ebene. Wird in `_initSysAdminRole()` beim Bootstrap angelegt -- keine Template-Kopie.
|
||||
- **SystemUser / EventUser**: Technische System-Accounts, nicht an echte Benutzer gebunden.
|
||||
- **SysAdmin / PlatformAdmin**: Plattformweite Autoritaet wird ueber **User-Flags** geregelt (`isSysAdmin`, `isPlatformAdmin`), nicht ueber Rollen. Siehe Abschnitt "Platform-Governance-Autoritaet".
|
||||
- **SystemUser / EventUser**: Technische System-Accounts, nicht an echte Benutzer gebunden. Beide bekommen `isSysAdmin=true`; nur `admin` bekommt zusaetzlich `isPlatformAdmin=true`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
0
c-work/0-ideas/.gitkeep
Normal file
0
c-work/0-ideas/.gitkeep
Normal file
404
c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md
Normal file
404
c-work/1-plan/2026-04-pwg-pilot-mietzinsbestaetigung-workflow.md
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
<!-- status: plan -->
|
||||
<!-- started: 2026-04-16 -->
|
||||
<!-- component: gateway | frontend-nyla | platform -->
|
||||
<!-- relatedTo: c-work/0-ideas/2026-04-pm-consolidated-customer-requirements.md (1.9c) -->
|
||||
|
||||
# PWG-Pilot: Jahresmietzinsbestätigungs-Workflow + Workflow-File-IO
|
||||
|
||||
## Beschreibung und Kontext
|
||||
|
||||
**Quelle:** Konsolidierter Kundenwünsche-Plan, Abschnitt 1.9c — PWG-Workshop 16.04.2026.
|
||||
|
||||
**Business Case:** PWG verschickt 4 × 800 = 3'200 Jahresmietzinsbestätigungen pro Jahr. Rückantworten (Scans) werden aktuell manuell verarbeitet. Pilot-Ziel: AI-gestützte Verarbeitung mit Antwortvorschlägen, Versand Sommer 2026.
|
||||
|
||||
**Dieser Plan deckt drei zusammenhängende Themen ab:**
|
||||
|
||||
1. **Workflow-File-IO** — Workflows als File exportieren (in UDB speichern) und aus UDB-File in den Graph-Editor laden. Voraussetzung, damit der Pilot-Workflow als versionierbares Asset im Repo geliefert werden kann.
|
||||
2. **Agent-Tools Full-CRUD** — Der AI-Agent erhält im `workflow`-Toolbox die vollständigen Operationen `createWorkflow`, `createWorkflowFromFile`, `exportWorkflowToFile`, `deleteWorkflow` (zusätzlich zu den bestehenden Lese-/Edit-Tools).
|
||||
3. **Pilot-Workflow als File** — Konkrete Lieferung: Workflow-JSON nach Schema, Step-5 AI-Prompt, Datenextraktion aus Trustee-DB.
|
||||
|
||||
**Risiko bei Nicht-Umsetzung:** PWG-Pilot kann nicht termingerecht (Sommer 2026) aufgesetzt werden. Workflows bleiben nicht portabel zwischen Mandanten/Instanzen, was Demo-Vorbereitungen und Customer-Onboarding deutlich verlangsamt.
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- Abacus-Testmandant (extern, Patrick koordiniert) — nicht Blocker für Workflow-Bau, nur für End-to-End-Test.
|
||||
- Bestehende Komponenten (alle ✅): Trigger, SharePoint-Nodes, `flow.loop`, `trustee.extractFromFiles`, `ai.prompt`, `email.send`.
|
||||
|
||||
## Fokus und kritische Details
|
||||
|
||||
- **Trustee-DB hat kein Mieter-/Mietzins-Modell** (siehe Codebase-Audit). Stattdessen: Matching-Logik liegt im AI-Prompt von Step 5. Daten werden via Sub-Agent (`queryFeatureInstance` / `aggregateTable` auf `TrusteeDataContact` + `TrusteeDataJournalLine`) oder via dediziertem Daten-Extract-Node bereitgestellt. **Kein neues DB-Schema nötig.**
|
||||
- **Graph-Format-Inkonsistenz:** Bootstrap-Templates verwenden Node-Felder `x` / `y` (top-level), Agent-Tool `addNode` schreibt `position: {x, y}`. Beim Import muss normalisiert werden.
|
||||
- **Workflow-Envelope-Felder:** `AutoWorkflow` hat zahlreiche optionale Felder (`tags`, `invocations`, `templateScope`, `sharedReadOnly`, `notifyOnFailure`). Beim Export müssen Mandanten-spezifische Felder (`mandateId`, `featureInstanceId`, `id`, `currentVersionId`, `eventId`) **rausgefiltert** werden, sonst sind Files nicht portabel.
|
||||
- **Schema-Versionierung:** Wir starten mit `$schemaVersion: "1.0"`. Loader prüft Version und lehnt unbekannte ab oder migriert.
|
||||
- **UDB-Erkennung:** Workflow-Files identifizieren wir per Dateiendung `.workflow.json` (case-insensitive) **und** Content-Sniffing (Top-Level-Keys `$schemaVersion` + `graph` + `nodes`).
|
||||
- **Sicherheit beim Import:** Geladene Workflows werden NICHT automatisch ausgeführt. Nutzer muss aktiv via "Speichern + Aktivieren" die `invocations` aktivieren. `active` wird beim Import auf `false` gesetzt.
|
||||
- **Node-Type-Validierung:** Beim Import gegen `STATIC_NODE_TYPES` validieren. Unbekannte Node-Typen → Fehler mit klarer Meldung (welcher Typ fehlt).
|
||||
|
||||
## Ziel und Nicht-Ziele
|
||||
|
||||
**Ziel:**
|
||||
- Workflows können als File (JSON) aus dem Graph-Editor exportiert und in UDB gespeichert werden.
|
||||
- Workflow-Files in UDB können im Graph-Editor wieder geladen werden (neuer Workflow oder Update bestehender).
|
||||
- AI-Agent kann Workflows lesen, erstellen, aus File importieren, exportieren und löschen.
|
||||
- Der PWG-Pilot-Workflow `pwg-mietzinsbestaetigung-pilot.workflow.json` liegt im Repo, kann von einer leeren PWG-Demo-Instanz geladen und (mit Test-Daten) ausgeführt werden.
|
||||
- Step-5-Prompt liefert pro Scan: Status (`bestaetigt` / `abweichung` / `unleserlich` / `keine_unterschrift`) und einen Antwortvorschlag bei Abweichung.
|
||||
|
||||
**Explizit NICHT:**
|
||||
- Kein neues DB-Schema für Mietzins/Mieter (Matching erfolgt im Prompt gegen bestehende Trustee-Daten).
|
||||
- Keine Persistenz der Pilot-Run-Ergebnisse in einer Trustee-Sub-Tabelle (CSV im Mail reicht für Pilot).
|
||||
- Keine Auto-Aktivierung importierter Workflows (sicherheitsrelevant).
|
||||
- Keine Migration-Logik für `$schemaVersion > 1.0` (kommt später).
|
||||
- Kein Versions-Branching/Diff von Workflow-Files (späteres Thema).
|
||||
- Keine SharePoint-OCR-Verbesserungen (extractFromFiles wird wie bestehend genutzt).
|
||||
|
||||
## Betroffene Module
|
||||
|
||||
- **Gateway:**
|
||||
- `features/graphicalEditor/routeFeatureGraphicalEditor.py` — neue Routen `POST .../workflows/import` und `GET .../workflows/{id}/export`.
|
||||
- `features/graphicalEditor/interfaceFeatureGraphicalEditor.py` — neue Methoden `importWorkflowFromDict`, `exportWorkflowToDict`.
|
||||
- `features/graphicalEditor/_workflowFileSchema.py` (NEU) — Schema-Definition + Validierung + Normalisierung (`x/y` ↔ `position`).
|
||||
- `serviceCenter/services/serviceAgent/workflowTools.py` — neue Tools `createWorkflow`, `createWorkflowFromFile`, `exportWorkflowToFile`, `deleteWorkflow`.
|
||||
- `serviceCenter/services/serviceAgent/toolboxRegistry.py` — Tool-Liste in `workflow`-Toolbox erweitern.
|
||||
- **Frontend:**
|
||||
- `components/UnifiedDataBar/FilesTab.tsx` — Workflow-File-Erkennung + Action "In Graph-Editor laden".
|
||||
- `pages/views/workflow/` (Graph-Editor-View) — Buttons "Aus Datei importieren" + "Als Datei exportieren" (Editor-Toolbar).
|
||||
- `api/workflowApi.ts` (oder analog) — neue Endpoint-Wrapper.
|
||||
- **DB-Migration:** **nein**.
|
||||
- **Repo-Asset:** `gateway/demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json` (NEU).
|
||||
- **Andere:** Step-5-Prompt-Template als Konstante in `gateway/modules/features/trustee/promptTemplates/_pwgMietzinsCheck.py` oder direkt im Workflow-File (Decision unten).
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Datum | Entscheidung | Begründung |
|
||||
|-------|-------------|------------|
|
||||
| 2026-04-16 | Workflow-File-Format = Envelope + Schema-Version (1.0) | Portabel, zukunftssicher, ein Round-Trip ohne Verlust von Metadaten |
|
||||
| 2026-04-16 | Matching-Logik im AI-Prompt von Step 5 (nicht neue DB-Tabelle) | Trustee-DB hat kein Mietzins-Modell; Pilot kommt mit AI + bestehende Sub-Agent-Queries aus |
|
||||
| 2026-04-16 | Result-Output = CSV als E-Mail-Anhang (kein DB-Schema) | Schlankster Pilot-Pfad; Persistenz später bei Bedarf nachrüstbar |
|
||||
| 2026-04-16 | Agent-Tools = Full-CRUD inkl. `deleteWorkflow` | Agent soll Workflows komplett verwalten können (User-Wunsch) |
|
||||
| 2026-04-16 | File-Endung `.workflow.json` + Content-Sniffing | Klare UDB-Identifikation ohne neuen MIME-Type |
|
||||
| 2026-04-16 | Import setzt `active: false`; Aktivierung manuell | Sicherheits-Default — kein versehentliches Auto-Run nach Import |
|
||||
| 2026-04-16 | Step-5-Prompt im Workflow-File (Parameter von `ai.prompt`-Node) | Das File ist self-contained und im Repo lesbar; keine Code-Änderung beim Tunen |
|
||||
| 2026-04-16 | CSV-Sammlung über bestehende `data.aggregate` (collect) + `data.consolidate` (csvJoin) | Idiomatisch, bereits implementiert; kein neuer Node nötig |
|
||||
| 2026-04-16 | Neuer Node `trustee.queryData` ist Pflicht | Kein bestehender Node liest Trustee-DB; `ai.prompt` hat keinen Sub-Agent-Tool-Zugriff (verifiziert in `methodAi/actions/process.py`) — `context`-Parameter muss vorher befüllt werden |
|
||||
| 2026-04-16 | E-Mail mit Attachment via Erweiterung von `email.draftEmail` (neuer optionaler `attachments`-Parameter), kein neuer Node | Minimal-invasiv; `email.draftEmail` ist bereits gemappt auf `methodOutlook.composeAndDraftEmailWithContext` und kann erweitert werden. Im Pilot wird ein Draft erstellt (kein Auto-Versand), das passt zur "Sicherheits-Default-keine-Auto-Aktion"-Linie |
|
||||
|
||||
---
|
||||
|
||||
## Architektur
|
||||
|
||||
### Workflow-File Schema 1.0
|
||||
|
||||
```json
|
||||
{
|
||||
"$schemaVersion": "1.0",
|
||||
"$exportedAt": "2026-04-16T10:00:00Z",
|
||||
"$gatewayVersion": "0.x.y",
|
||||
"$kind": "poweron.workflow",
|
||||
"label": "PWG Pilot: Jahresmietzinsbestätigung",
|
||||
"description": "Verarbeitet gescannte Rückantworten der Mietzinsbestätigungen ...",
|
||||
"tags": ["pwg", "pilot", "mietzins"],
|
||||
"templateScope": "instance",
|
||||
"sharedReadOnly": false,
|
||||
"notifyOnFailure": true,
|
||||
"graph": {
|
||||
"nodes": [
|
||||
{ "id": "n1", "type": "trigger.manual", "x": 50, "y": 200, "title": "Manueller Start", "parameters": {} }
|
||||
],
|
||||
"connections": [
|
||||
{ "source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0 }
|
||||
]
|
||||
},
|
||||
"invocations": [
|
||||
{ "type": "schedule", "cronExpression": "0 22 * * *" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Felder, die NIE im File stehen:** `id`, `mandateId`, `featureInstanceId`, `currentVersionId`, `eventId`, `createdAt`, `updatedAt`, `active` (wird beim Import auf `false` gesetzt).
|
||||
|
||||
**Normalisierung beim Import:** Falls Node `position: {x, y}` enthält → in `x`/`y` top-level umwandeln (oder umgekehrt — wir entscheiden uns für **`x`/`y` top-level** als kanonische Form, weil Bootstrap-Templates so aussehen).
|
||||
|
||||
### API-Endpunkte (neu)
|
||||
|
||||
| Methode | Pfad | Zweck |
|
||||
|---------|------|-------|
|
||||
| `POST /api/workflows/{instanceId}/workflows/import` | Body: `{ "fileId": "...", "mode": "create" \| "updateGraph", "targetWorkflowId"?: "..." }` | Lädt File aus UDB, validiert, erstellt neuen Workflow oder ersetzt graph eines bestehenden |
|
||||
| `GET /api/workflows/{instanceId}/workflows/{workflowId}/export` | Query: `?asFileId=true&folderId=...` | Exportiert Workflow als File. Wenn `asFileId=true`: speichert in UDB und gibt `fileId` zurück. Sonst: gibt JSON-Body direkt zurück (Browser-Download) |
|
||||
|
||||
### Frontend-Erweiterungen
|
||||
|
||||
**UDB FilesTab** (`FilesTab.tsx`):
|
||||
- Erkennt Workflow-Files (`.workflow.json` oder Top-Level `$kind === "poweron.workflow"`).
|
||||
- Workflow-File-Eintrag erhält Icon (z. B. Workflow/Diagramm) und Context-Menu-Eintrag **"In Graph-Editor laden"**.
|
||||
- Bei Klick → Modal: Ziel-Editor-Instanz wählen + Mode (`create` / `updateGraph` für aktiven Workflow) → POST `/import`.
|
||||
|
||||
**Graph-Editor Toolbar** (Editor-View):
|
||||
- Button **"Importieren"** → File-Picker (UDB-Files mit Filter `.workflow.json`) → `POST /import`.
|
||||
- Button **"Exportieren"** → Modal: Ziel = Download oder UDB-Folder → `GET /export?asFileId=...`.
|
||||
|
||||
### AI-Agent Tools (Full-CRUD im `workflow`-Toolbox)
|
||||
|
||||
Erweiterung von `workflowTools.py` und Toolbox-Registry:
|
||||
|
||||
| Tool | Bestehend? | Beschreibung |
|
||||
|------|-----------|--------------|
|
||||
| `readWorkflowGraph` | ✅ | Graph eines Workflows lesen |
|
||||
| `addNode`, `removeNode`, `connectNodes`, `setNodeParameter` | ✅ | Bestehende Edit-Tools |
|
||||
| `listAvailableNodeTypes`, `validateGraph` | ✅ | Bestehende Helpers |
|
||||
| `listWorkflowHistory`, `readWorkflowMessages` | ✅ | Bestehende Read-Tools |
|
||||
| `createWorkflow` | **NEU** | Args: `label`, `description?`, `tags?`, `graph`, `invocations?`. Ergebnis: `{ workflowId }` |
|
||||
| `createWorkflowFromFile` | **NEU** | Args: `fileId`, `mode` (default `create`). Ergebnis: `{ workflowId, importedNodes, warnings }` |
|
||||
| `exportWorkflowToFile` | **NEU** | Args: `workflowId`, `targetFolderId?`. Ergebnis: `{ fileId, fileName }` |
|
||||
| `deleteWorkflow` | **NEU** | Args: `workflowId`, `confirm: true`. Sicherheits-Confirm-Flag |
|
||||
|
||||
Implementierung: alle neuen Tools rufen die neuen Routen / Interface-Methoden auf, kein direkter DB-Zugriff aus den Tools.
|
||||
|
||||
---
|
||||
|
||||
## Pilot-Workflow: Inhalt
|
||||
|
||||
### Datei: `gateway/demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json`
|
||||
|
||||
**Knoten-Übersicht (10 Nodes — basierend auf real verfügbaren Nodes nach Codebase-Audit):**
|
||||
|
||||
| ID | Type | Title | Wichtige Parameter |
|
||||
|----|------|-------|---------------------|
|
||||
| `n1` | `trigger.manual` | Manueller Start | (alternativ `trigger.schedule` mit Cron `0 22 * * *`) |
|
||||
| `n2` | `sharepoint.listFiles` | Scan-Ordner auflisten | `connectionReference`, `sharepointFolder: "PWG/Mietzinsbestaetigungen/Scans-{year}-{quarter}"`, `filter: { extensions: ["pdf", "tif", "jpg"] }` |
|
||||
| `n3` | `flow.loop` | Pro Dokument | `inputArrayPath`, `itemAlias: "scanFile"` |
|
||||
| `n4` | `sharepoint.downloadFile` | Datei laden | `fileReference: "{{loop.item}}"` |
|
||||
| `n5` | `trustee.extractFromFiles` | OCR & Extraktion | `featureInstanceId`, `prompt: <ExtractionPrompt für Mietzinsbestätigung — siehe unten>` |
|
||||
| `n6` | `trustee.queryData` ⚠️ NEU | Referenzdaten holen | `featureInstanceId`, `mode: "lookup"`, `entity: "tenantWithRent"`, `tenantNameRef: "{{n5.output.tenantName}}"`, `tenantAddressRef: "{{n5.output.tenantAddress}}"`, `period: "{{currentYear}}"` |
|
||||
| `n7` | `ai.prompt` | Prüfung & Klassifikation | `aiPrompt: <Step-5-Prompt-Template>`, `outputFormat: "json"`, `documentList: <Wire von n5>`, `context: <Wire von n6>` |
|
||||
| `n8` | `data.aggregate` | Ergebnisse sammeln (im Loop) | `mode: "collect"` — sammelt `n7.output` über Loop-Iterationen |
|
||||
| `n9` | `data.consolidate` | CSV bauen (nach Loop) | `mode: "csvJoin"`, `separator: "\n"` — wandelt gesammelte JSON-Items in CSV |
|
||||
| `n10` | `email.draftEmail` | Draft mit Anhang | `connectionReference`, `to: "sachbearbeiter@pwg.ch"`, `subject: "Mietzinsbestätigungen Auswertung {{date}}"`, `body: <Zusammenfassung>`, `attachments: [{ name: "ergebnisse.csv", contentRef: "{{n9.output}}" }]` ⚠️ `attachments`-Param muss in `email.draftEmail` ergänzt werden |
|
||||
|
||||
**Nicht-existierende Nodes/Params, die als Sub-Aufgabe gebaut werden müssen** (siehe Phase 4):
|
||||
1. **`trustee.queryData`** — neuer Node in `nodeDefinitions/trustee.py` + Action in `methodTrustee/actions/queryData.py`. Wrappt `FeatureDataProvider.queryTable` / `aggregateTable`. Mode `lookup` mit Entity `tenantWithRent` macht intern: Match in `TrusteeDataContact` (Debitor) → Aggregat über `TrusteeDataJournalLine` für Mietzins-Konten in der Periode.
|
||||
2. **`email.draftEmail.attachments`** — optionaler Parameter `attachments` in `nodeDefinitions/email.py` ergänzen + im Executor `composeAndDraftEmailWithContext` Attachment-Handling implementieren.
|
||||
|
||||
**Datenfluss-Anmerkungen:**
|
||||
- Der `context`-Parameter von `ai.prompt` muss VOR dem Node befüllt werden (verifiziert: `methodAi/actions/process.py` macht direkten `AiCallRequest` ohne Sub-Agent-Tools). Daher liegt `n6` zwingend vor `n7` im Wire-Pfad.
|
||||
- `data.aggregate` (mode `collect`) sammelt im Loop-Body, `data.consolidate` (mode `csvJoin`) führt nach dem Loop zusammen — Standardmuster für CSV-Output aus Loop-Iterationen.
|
||||
- `helpers/csvProcessing.py` (in `methodAi`) kann von `data.consolidate` und/oder `email.draftEmail`-Attachment-Builder wiederverwendet werden.
|
||||
|
||||
### Extraktionsschema für `n5` (`extractionSchema: "mietzinsbestaetigung"`)
|
||||
|
||||
Erwartete Felder im OCR-Output:
|
||||
|
||||
```json
|
||||
{
|
||||
"tenantName": "string",
|
||||
"tenantAddress": "string",
|
||||
"objectAddress": "string",
|
||||
"confirmedRentAmount": "number|null",
|
||||
"currency": "CHF",
|
||||
"period": "string (z.B. 2026)",
|
||||
"tenantNotes": "string|null",
|
||||
"hasSignature": "boolean",
|
||||
"documentDate": "string (ISO date)|null",
|
||||
"ocrConfidence": "number (0-1)"
|
||||
}
|
||||
```
|
||||
|
||||
### Step-5-Prompt (Inhalt für `n7.parameters.prompt`)
|
||||
|
||||
```text
|
||||
Du bist ein Sachbearbeitungs-Assistent der Stiftung PWG. Deine Aufgabe ist es,
|
||||
eine eingescannte und OCR-extrahierte Jahresmietzinsbestätigung gegen die
|
||||
Stammdaten der Buchhaltung (Trustee-Feature) abzugleichen.
|
||||
|
||||
Eingaben:
|
||||
1. SCAN_DATEN (extrahiert per OCR aus dem Rückantwort-Dokument):
|
||||
{{scan}}
|
||||
|
||||
2. REFERENZ_DATEN (aus Trustee-DB für diesen Mieter; ggf. leer wenn nicht
|
||||
eindeutig zuordenbar):
|
||||
{{reference}}
|
||||
|
||||
Vorgehen:
|
||||
1. Prüfe Identität: Stimmt SCAN_DATEN.tenantName + SCAN_DATEN.tenantAddress mit
|
||||
einem Datensatz in REFERENZ_DATEN.contacts überein? (Toleranz: kleine
|
||||
Tippfehler, Umlaute, Abkürzungen)
|
||||
2. Prüfe Mietzinsbetrag: Stimmt SCAN_DATEN.confirmedRentAmount mit dem aus
|
||||
REFERENZ_DATEN.journalLines abgeleiteten erwarteten Mietzins überein?
|
||||
(Toleranz: ±1 CHF Rundung)
|
||||
3. Prüfe Unterschrift: hasSignature muss true sein.
|
||||
4. Prüfe OCR-Qualität: ocrConfidence < 0.6 → "unleserlich".
|
||||
|
||||
Klassifiziere in EXAKT EINEN Status:
|
||||
- "bestaetigt": Identität stimmt, Betrag stimmt, Unterschrift vorhanden.
|
||||
- "abweichung_betrag": Identität ok, Unterschrift ok, Betrag weicht ab.
|
||||
- "abweichung_anmerkung": tenantNotes enthält substantielle Anmerkung
|
||||
(nicht leer, nicht reine Bestätigung).
|
||||
- "keine_unterschrift": hasSignature == false.
|
||||
- "unleserlich": OCR-Qualität ungenügend ODER Pflichtfelder fehlen.
|
||||
- "kein_match": Mieter nicht in REFERENZ_DATEN auffindbar.
|
||||
|
||||
Bei Status != "bestaetigt": Generiere einen kurzen, höflichen
|
||||
Antwortvorschlag (deutsch, Sie-Form, max. 5 Sätze, PWG-Stil) für die
|
||||
Sachbearbeitung. Bei "bestaetigt": antwortVorschlag = null.
|
||||
|
||||
Antworte AUSSCHLIESSLICH als JSON nach folgendem Schema:
|
||||
{
|
||||
"tenantName": string,
|
||||
"objectAddress": string,
|
||||
"status": "bestaetigt" | "abweichung_betrag" | "abweichung_anmerkung"
|
||||
| "keine_unterschrift" | "unleserlich" | "kein_match",
|
||||
"scanRentAmount": number | null,
|
||||
"expectedRentAmount": number | null,
|
||||
"delta": number | null,
|
||||
"tenantNotes": string | null,
|
||||
"antwortVorschlag": string | null,
|
||||
"matchConfidence": number,
|
||||
"auditEvidence": string
|
||||
}
|
||||
```
|
||||
|
||||
`outputSchema` im Node erzwingt JSON-Form (existierende `ai.prompt`-Capability nutzen).
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungs-Checkliste
|
||||
|
||||
### Phase 1 — Workflow-File-IO Backend
|
||||
|
||||
- [ ] **Schema-Modul `_workflowFileSchema.py`** mit `WORKFLOW_FILE_SCHEMA_VERSION = "1.0"`, Funktionen `_validateFileEnvelope()`, `_normalizeNodePositions()`, `_stripPersistenceFields()`, `_buildFileFromWorkflow()`.
|
||||
- [ ] **Interface-Methoden** in `interfaceFeatureGraphicalEditor.py`:
|
||||
- `importWorkflowFromDict(envelope, mandateId, featureInstanceId, mode, targetWorkflowId?) -> workflowId`
|
||||
- `exportWorkflowToDict(workflowId) -> envelope`
|
||||
- [ ] **Routen** in `routeFeatureGraphicalEditor.py`:
|
||||
- `POST /{instanceId}/workflows/import` (Body: `fileId`, `mode`, `targetWorkflowId?`).
|
||||
- `GET /{instanceId}/workflows/{workflowId}/export` (Query: `asFileId`, `folderId?`).
|
||||
- [ ] Validierungen:
|
||||
- Bekannte Schema-Version, Pflichtfelder, Node-Typen vorhanden in `STATIC_NODE_TYPES`.
|
||||
- Bei Import: `active=false` erzwingen.
|
||||
- [ ] Unit-Tests: Round-Trip (Export → Import → erneuter Export, Bytes identisch nach Normalisierung).
|
||||
|
||||
### Phase 2 — UDB-Erkennung & Frontend-IO
|
||||
|
||||
- [ ] **`FilesTab.tsx`**:
|
||||
- Workflow-File-Detection (Endung + Content-Sniffing über `useFiles.isJsonContent`).
|
||||
- Workflow-Icon, Context-Menu-Eintrag "In Graph-Editor laden" → öffnet Modal.
|
||||
- [ ] **Modal "Workflow importieren"**: Editor-Instanz-Auswahl, Mode-Wahl (Neu / Bestehenden ersetzen), Submit → API.
|
||||
- [ ] **Graph-Editor-Toolbar**: Buttons "Importieren" (Files-Picker) und "Exportieren" (Download-vs-UDB-Modal).
|
||||
- [ ] **`api/workflowApi.ts`** (oder analog): `importWorkflowFromFile(fileId, …)`, `exportWorkflowToFile(workflowId, …)`.
|
||||
|
||||
### Phase 3 — Agent-Tools Full-CRUD
|
||||
|
||||
- [ ] In `workflowTools.py` neue Funktionen: `_createWorkflow`, `_createWorkflowFromFile`, `_exportWorkflowToFile`, `_deleteWorkflow` (alle mit `_`-Prefix gemäss Naming-Konvention).
|
||||
- [ ] Tool-Definitionen in `getWorkflowToolDefinitions()` ergänzen.
|
||||
- [ ] `toolboxRegistry.py` — Tool-Liste in `workflow`-Toolbox erweitern.
|
||||
- [ ] Sicherheits-Confirm: `deleteWorkflow` verlangt `confirm: true`.
|
||||
- [ ] Integration-Test: Agent erstellt Workflow → exportiert → löscht → re-importiert via fileId.
|
||||
|
||||
### Phase 4 — Fehlende Nodes für Pilot ergänzen (konkretisiert nach Codebase-Audit)
|
||||
|
||||
**Audit-Ergebnis** (siehe auch Tabelle in Abschnitt "Findings" weiter oben):
|
||||
- ✅ `data.aggregate` (mode `collect`) und `data.consolidate` (mode `csvJoin`) existieren bereits → CSV-Sammlung ist abgedeckt, **kein neuer Daten-Node nötig**.
|
||||
- ❌ Kein Node liest Trustee-DB-Daten → `trustee.queryData` muss neu gebaut werden.
|
||||
- ❌ `email.draftEmail` hat keinen `attachments`-Parameter → muss erweitert werden.
|
||||
- ✅ `ai.prompt` mit `outputFormat: "json"` und `context`-Parameter ist passgenau für Step 5.
|
||||
|
||||
**Sub-Task 4a — Neuer Node `trustee.queryData`:**
|
||||
- [ ] Definition in `gateway/modules/features/graphicalEditor/nodeDefinitions/trustee.py` ergänzen (Pattern wie `trustee.refreshAccountingData`).
|
||||
- Parameter: `featureInstanceId` (hidden), `mode` (select: `lookup`, `aggregate`, `raw`), `entity` (select: `tenantWithRent`, `contact`, `journalLines`, `accounts`), `tenantNameRef`/`tenantAddressRef`/`period` (text, optional je nach mode), `extraFilter` (json, optional).
|
||||
- Inputs: 1, Outputs: 1, `outputPorts: { 0: { schema: "QueryResult" } }`.
|
||||
- `_method: "trustee"`, `_action: "queryData"`.
|
||||
- [ ] Action-Implementierung `gateway/modules/workflows/methods/methodTrustee/actions/queryData.py`:
|
||||
- Mode `lookup` mit Entity `tenantWithRent` → fuzzy match in `TrusteeDataContact` (Debitor-Filter), dann Aggregat in `TrusteeDataJournalLine` über die für Mietzins relevanten Konten in der Periode.
|
||||
- Mode `raw` → direkter `FeatureDataProvider.queryTable` mit `entity` als Tabellenname.
|
||||
- Mode `aggregate` → `FeatureDataProvider.aggregateTable` mit `extraFilter`.
|
||||
- Output-Format: `{ matched: bool, contacts: [...], journalLines: [...], expectedRentAmount: number|null, matchConfidence: number }` — direkt verwendbar als `context` in `ai.prompt`.
|
||||
- [ ] Registrierung in `methodTrustee/__init__.py` und `methodTrustee.py` (analog zu existierenden Actions).
|
||||
- [ ] Unit-Test `tests/methods/methodTrustee/test_queryData.py`: Mock `FeatureDataProvider`, prüfe Match-Logik mit Tippfehler-Toleranz und Mietzins-Aggregation.
|
||||
|
||||
**Sub-Task 4b — Attachment-Support in `email.draftEmail`:**
|
||||
- [ ] In `nodeDefinitions/email.py` neuen optionalen Parameter ergänzen:
|
||||
```python
|
||||
{"name": "attachments", "type": "json", "required": False, "frontendType": "attachmentBuilder",
|
||||
"description": t("Anhänge (Liste von { name, contentRef | csvFromVariable | base64Content })"),
|
||||
"default": []},
|
||||
```
|
||||
- [ ] In `gateway/modules/workflows/methods/methodOutlook/actions/composeAndDraftEmailWithContext.py` (oder dem aktuellen Pfad) Attachment-Handling implementieren:
|
||||
- `contentRef` → resolved aus Wire/Variable, als Bytes/String.
|
||||
- `csvFromVariable` → resolved aus Variable (z. B. Output von `data.consolidate`) und als CSV-Anhang gehängt.
|
||||
- `base64Content` → direkt dekodieren.
|
||||
- Hochladen als Outlook-Draft-Attachment via Graph-API (`/me/messages/{id}/attachments`).
|
||||
- [ ] Frontend: Attachment-Builder im Property-Panel des Editors (kann minimal sein — JSON-Editor reicht für Pilot).
|
||||
- [ ] Unit-Test mit gemocktem Outlook-Connector: prüfe dass Attachment korrekt im erstellten Draft enthalten ist.
|
||||
|
||||
**Sub-Task 4c — Frontend-Anpassungen für neue Nodes:**
|
||||
- [ ] `trustee.queryData` automatisch in der Node-Palette sichtbar (kommt durch Definition gratis).
|
||||
- [ ] Property-Panel: Bei `mode: "lookup"` und `entity: "tenantWithRent"` die Tenant-Felder anzeigen, sonst ausblenden (`dependsOn`-Mechanismus wie in `sharepointFolder`).
|
||||
- [ ] Falls beide Wege zu aufwändig: Pilot-Workflow auf 7 Nodes vereinfachen — Step 5 macht ALLES (Sub-Agent-Query + Klassifikation + Antwort) in einem `ai.prompt`-Node mit `featureSubAgent`-Tool-Zugriff.
|
||||
|
||||
### Phase 5 — Pilot-Workflow als File
|
||||
|
||||
- [ ] Datei `gateway/demoData/workflows/pwg-mietzinsbestaetigung-pilot.workflow.json` erstellen (Inhalt wie oben spezifiziert).
|
||||
- [ ] Datei via Test in eine PWG-Demo-Instanz importieren (manueller Smoke-Test).
|
||||
- [ ] Test-Lauf mit 2–3 fiktiven Scan-PDFs (in `gateway/demoData/pwg/scans/` ablegen — Sub-Aufgabe).
|
||||
- [ ] CSV-Ergebnis prüfen: enthält pro Scan eine Zeile mit allen Feldern aus dem Output-Schema.
|
||||
|
||||
### Querschnitt
|
||||
|
||||
- [ ] **API-Endpunkte:** ja, 2 neue (Import + Export).
|
||||
- [ ] **DB-Schema / Migration:** **nein**.
|
||||
- [ ] **Frontend-Komponenten:** FilesTab-Erweiterung, Graph-Editor-Toolbar-Buttons, Import-Modal.
|
||||
- [ ] **RBAC / Permissions:** Import/Export benötigen gleiche Rechte wie `update_workflow`. `deleteWorkflow`-Tool nur wenn User-Rolle `delete_workflow` darf.
|
||||
- [ ] **Neutralisierung betroffen?** Indirekt ja: Workflow-Files können sensible Parameter (Connection-Refs, E-Mail-Adressen) enthalten. **Doku-Hinweis** beim Export-Modal: "Vor Weitergabe Datei prüfen". Keine automatische Neutralisierung im Pilot-Scope.
|
||||
- [ ] **Navigation / Routing:** keine Änderung.
|
||||
- [ ] **Billing-Impact:** Pilot-Workflow verbraucht Tokens (1 AI-Call pro Scan, ggf. Sub-Agent-Calls). Kalkulation für PWG-Pilot: `~3'200 Schreiben/Jahr × ~3 Calls × ~2k Tokens` als Grössenordnung — gehört in PWG-Preismodell-Action-Item AI3.
|
||||
|
||||
---
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
| # | Kriterium (Given-When-Then) | Prio |
|
||||
|---|---------------------------|------|
|
||||
| 1 | Given Workflow im Graph-Editor, When User auf "Exportieren → UDB" klickt, Then liegt eine `<label>.workflow.json` im gewählten UDB-Folder mit Schema-Version 1.0 | must |
|
||||
| 2 | Given Workflow-File in UDB, When User in FilesTab "In Graph-Editor laden" wählt + Mode `create` bestätigt, Then existiert ein neuer Workflow mit identischem Graph und `active=false` | must |
|
||||
| 3 | Given Workflow-File mit unbekanntem Node-Typ, When Import, Then Fehler-Response listet konkret den fehlenden Typ; kein Workflow wird erstellt | must |
|
||||
| 4 | Given Workflow A in Editor, When `exportWorkflowToFile` → `createWorkflowFromFile`, Then ist der neu erstellte Workflow nach Normalisierung byte-äquivalent (Round-Trip-Stabilität) | should |
|
||||
| 5 | Given AI-Agent mit aktivem `workflow`-Toolbox, When Agent ruft `createWorkflow` mit gültigem Graph, Then wird Workflow erstellt und `workflowId` zurückgegeben | must |
|
||||
| 6 | Given AI-Agent, When Agent ruft `deleteWorkflow` ohne `confirm: true`, Then Tool antwortet mit Fehler "confirm flag missing" und löscht nichts | must |
|
||||
| 7 | Given PWG-Pilot-Workflow geladen + 1 Test-Scan-PDF in SharePoint-Ordner, When manueller Trigger, Then enthält die E-Mail-CSV exakt 1 Zeile mit gefülltem `status`-Feld | must |
|
||||
| 8 | Given Test-Scan ohne Unterschrift, When Workflow läuft, Then `status == "keine_unterschrift"` und `antwortVorschlag` ist gefüllt | should |
|
||||
| 9 | Given Test-Scan mit korrekten Daten, When Workflow läuft + Trustee-DB enthält passenden Debitor + Journal-Line, Then `status == "bestaetigt"` und `delta == 0` | should |
|
||||
| 10 | Given importierter Workflow, When User versucht ihn ohne weitere Aktion zu schedulen, Then schlägt es fehl bzw. `active` ist `false` und User muss explizit aktivieren | must |
|
||||
|
||||
## Testplan
|
||||
|
||||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||||
|----|----|-----|--------------|-----------|--------|
|
||||
| T1 | 1, 4 | unit | ja | `gateway/tests/unit/graphicalEditor/test_workflow_file_io.py` | pending |
|
||||
| T2 | 2, 3, 10 | api | ja | `gateway/tests/integration/graphicalEditor/test_workflow_import.py` | pending |
|
||||
| T3 | 5, 6 | unit | ja | `gateway/tests/unit/serviceAgent/test_workflow_tools_crud.py` | pending |
|
||||
| T4 | 7, 8, 9 | e2e | manuell | PWG-Demo-Instanz mit 3 Test-Scans | pending |
|
||||
| T5 | 1, 2 | manuell UI | nein | Frontend FilesTab + Graph-Editor in lokaler Dev-Umgebung | pending |
|
||||
|
||||
## Links
|
||||
|
||||
- Quellplan (0-ideas): `wiki/c-work/0-ideas/2026-04-pm-consolidated-customer-requirements.md` (Abschnitt 1.9c)
|
||||
- PWG-Workshop-Inputs: `pamocreate/projects/poweron/customer-pwg/20260415-inputs-pwg.txt`
|
||||
- Graph-Editor Models: `gateway/modules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py`
|
||||
- Graph-Editor Routes: `gateway/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py`
|
||||
- System-Templates Bootstrap: `gateway/modules/interfaces/interfaceBootstrap.py` (`_buildSystemTemplates`)
|
||||
- Agent Workflow-Tools: `gateway/modules/serviceCenter/services/serviceAgent/workflowTools.py`
|
||||
- Toolbox-Registry: `gateway/modules/serviceCenter/services/serviceAgent/toolboxRegistry.py`
|
||||
- UDB Files-Tab: `frontend_nyla/src/components/UnifiedDataBar/FilesTab.tsx`
|
||||
- File-Upload-Route: `gateway/modules/routes/routeDataFiles.py`
|
||||
- Trustee-Modelle: `gateway/modules/features/trustee/datamodelFeatureTrustee.py`
|
||||
- Abacus-Connector: `gateway/modules/features/trustee/accounting/connectors/accountingConnectorAbacus.py`
|
||||
- Feature-Sub-Agent: `gateway/modules/serviceCenter/services/serviceAgent/featureDataAgent.py`
|
||||
|
||||
- PR: ...
|
||||
- Issue: ...
|
||||
|
||||
## Abschluss
|
||||
|
||||
- [ ] `b-reference/gateway/architecture.md` ergänzen (Workflow-File-IO Sektion)
|
||||
- [ ] `b-reference/frontend-nyla/architecture.md` ergänzen (FilesTab Workflow-Handling)
|
||||
- [ ] `TOPICS.md` ergänzen (neues Thema "Workflow Portability")
|
||||
- [ ] PWG-Pilot-Workflow-Datei in `gateway/demoData/workflows/` committed und im Demo-Config (`pwgDemo2026.py`) referenziert
|
||||
- [ ] Dieses Dokument → `c-work/2-build/` verschieben sobald Phase 1 startet
|
||||
184
c-work/4-done/2026-04-mandate-name-label-logic.md
Normal file
184
c-work/4-done/2026-04-mandate-name-label-logic.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<!-- status: done -->
|
||||
<!-- started: 2026-04-18 -->
|
||||
<!-- completed: 2026-04-18 -->
|
||||
<!-- component: gateway, frontend-nyla, platform -->
|
||||
|
||||
# Mandate `name` (Kurzzeichen) und `label` (Voller Name) Logik
|
||||
|
||||
## Beschreibung und Kontext
|
||||
|
||||
Heute existieren am `Mandate`-Modell die Felder `name` und `label` ohne klare Trennung der Verantwortlichkeiten:
|
||||
|
||||
- `name` ist als `str` required, ohne Format-Validierung, ohne Unique-Garantie auf Modell-Ebene, ohne Immutability.
|
||||
- `label` ist `Optional[str]`, kann leer sein.
|
||||
- Beim Auto-Provisioning (Home-Mandant, Onboarding) wird `name == label == "Home <username>"` gesetzt — das verstösst gegen jede sinnvolle Slug-Konvention und macht `name` als URL-/Audit-Identifier unbrauchbar.
|
||||
- In der UI wird inkonsistent mal `m.label || m.name`, mal `mandate.name`, mal sogar `name (label)` angezeigt.
|
||||
|
||||
**Business-Treiber:**
|
||||
|
||||
- **Audit-Stabilität:** Audit-Trails referenzieren Mandate per `name`. Wenn der Anzeige-Name (Label) frei änderbar ist und gleichzeitig als technischer Code dient, brechen Audit-Reports beim Rebranding.
|
||||
- **URL-/API-Friendliness:** Der `name` soll als technischer Code in URLs, Logs, und systemexternen Referenzen taugen.
|
||||
- **Klare Rollen-Trennung:** Mandate-Admin darf sein Label umbenennen ohne den globalen Identifier zu touchieren; nur Plattform-Governance (PlatformAdmin) darf den Identifier ändern.
|
||||
|
||||
**Risiko bei Nicht-Umsetzung:** Inkonsistente Anzeige im UI, Verwechslungs-Risiko, brüchige Audit-Trails, keine sauberen Slugs/URLs.
|
||||
|
||||
## Fokus und kritische Details
|
||||
|
||||
- **Migration ist heikel:** existierende Mandate haben `name`-Werte mit Spaces/Sonderzeichen (z.B. `"Home patrick"`). Migration muss deterministisch + idempotent sein, alle FK-Verweise (`Role.mandateId`, `UserMandate.mandateId`, `MandateSubscription.mandateId`, `BillingSettings.mandateId`, ...) bleiben unverändert (FK ist `id`, nicht `name`).
|
||||
- **Auto-Provisioning** (`_provisionMandateForUser`, `routeSecurityLocal.py` Z. 213-219, 467-477, 884-894) setzt heute `name == label`. Muss umgebaut werden: `label = "Home <username>"` (oder `companyName`), `name = _generateUniqueMandateName(label)`.
|
||||
- **PUT-Route** unterscheidet heute zwischen PlatformAdmin (alles editierbar) und MandateAdmin (`{"label"}`-Whitelist). Neu: PlatformAdmin behält `name`-Recht, MandateAdmin nur `label` — Status quo passt, aber Server muss `name`-Format validieren bei Update.
|
||||
- **`isSystem`-Mandate:** Root-Mandant darf nicht versehentlich umbenannt werden — bestehende Sicherung greift bereits über `frontend_readonly` und das Warning-Banner.
|
||||
- **UI-Stellen mit `mandate.name` als technischer Identifier** (Delete-Confirm, X-Confirm-Name) bleiben semantisch korrekt — `name` IST hier der technische Identifier und das ist gewollt.
|
||||
- **i18n:** Die Label-Texte ("Kurzzeichen", "Voller Name") müssen via `t()` übersetzbar bleiben; kein hartkodiertes Deutsch in Frontend-Komponenten.
|
||||
- **TypeScript-Typen:** `frontend_nyla/src/types/mandate.ts` und `useMandates`-Hook müssen mitgepflegt werden.
|
||||
|
||||
## Ziel und Nicht-Ziele
|
||||
|
||||
**Ziel:**
|
||||
|
||||
- Klare semantische Trennung: `name` = Unique-Code (Slug), `label` = Anzeige-Name. Beide mandatory.
|
||||
- Server-seitige Validierung von `name` (Regex `^[a-z0-9]+(-[a-z0-9]+)*$`, Länge 2–32) bei Create und Update.
|
||||
- Slug-Auto-Generation aus `label` mit Transliteration (ä→ae, ö→oe, ü→ue, ß→ss) und Unique-Suffix-Logik (`-2`, `-3`, ...).
|
||||
- Live-strict-Input-Validierung im Frontend für `name`-Feld.
|
||||
- Konsistente UI-Anzeige: überall `label` rendern (`name` nur als Fallback wenn label fehlt — Migration sollte das ausschliessen, aber Defensive Coding bleibt).
|
||||
- Einmalige Migration: `label := name` wenn Label leer; `name` zu gültigem Slug normalisieren wenn nötig (mit Unique-Suffix).
|
||||
- PUT-Route: nur PlatformAdmin darf `name` ändern (Status quo); MandateAdmin nur `label`.
|
||||
|
||||
**Explizit NICHT:**
|
||||
|
||||
- Keine Änderung der DB-FK-Struktur (Mandate.id bleibt UUID-PK; FK-Verweise referenzieren weiterhin `id`, nicht `name`).
|
||||
- Kein Rename von Tabellenspalten.
|
||||
- Keine Auswirkung auf Audit-Tabellen (die loggen weiterhin `mandateId` als UUID).
|
||||
- Keine API-Versionierung; Breaking Change wird hingenommen, da nur PlatformAdmin direkt `name` setzt.
|
||||
- Kein Slug-Subdomain-Routing (zukünftiges Feature; nur Vorbereitung).
|
||||
|
||||
## Betroffene Module
|
||||
|
||||
- **Gateway:**
|
||||
- `gateway/modules/datamodels/datamodelUam.py` (`Mandate` Pydantic-Klasse)
|
||||
- `gateway/modules/interfaces/interfaceDbApp.py` (`createMandate`, `_provisionMandateForUser`, `updateMandate`, NEU: `_generateUniqueMandateName`)
|
||||
- `gateway/modules/routes/routeDataMandates.py` (POST/PUT-Validierung)
|
||||
- `gateway/modules/routes/routeSecurityLocal.py` (3 Aufrufstellen `_provisionMandateForUser`)
|
||||
- `gateway/modules/interfaces/interfaceBootstrap.py` (Migrations-Hook beim Boot)
|
||||
- NEU: `gateway/modules/shared/mandateNameUtils.py` (Slug-Helpers, Validierung, Transliteration)
|
||||
- **Frontend:**
|
||||
- `frontend_nyla/src/types/mandate.ts`
|
||||
- `frontend_nyla/src/hooks/useMandates.ts`
|
||||
- `frontend_nyla/src/pages/admin/AdminMandatesPage.tsx` (Modal-Texte, Subtitle)
|
||||
- `frontend_nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx` (Anzeige `name (label)` → `label (name)`)
|
||||
- `frontend_nyla/src/components/FormGenerator/...` (Live-strict-Validierung für Slug-Type, falls neuer `frontend_type: "slug"` eingeführt)
|
||||
- Display-Audit aller `m.label || m.name`-Stellen (Sicherstellen, dass Reihenfolge korrekt ist)
|
||||
- **DB-Migration:** ja (idempotent in Bootstrap, kein Alembic — passt zum Projekt-Pattern)
|
||||
- **Andere Komponenten:** keine
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Datum | Entscheidung | Begründung |
|
||||
|-------|-------------|------------|
|
||||
| 2026-04-18 | Beide Felder (`name`, `label`) mandatory, non-empty | User-Anforderung; Migration füllt fehlende Werte |
|
||||
| 2026-04-18 | Slug-Regex `^[a-z0-9]+(-[a-z0-9]+)*$`, Länge 2–32 | Kompakt, URL/Subdomain-tauglich |
|
||||
| 2026-04-18 | Transliteration ä/ö/ü/ß → ae/oe/ue/ss; Rest non-alnum → `-` | Deutsche Mandate-Namen häufig; Lesbarkeit |
|
||||
| 2026-04-18 | Unique-Suffix-Strategie: `mein-mandant`, `mein-mandant-2`, `mein-mandant-3`, ... | Einfach, deterministisch, lesbar |
|
||||
| 2026-04-18 | `name`-Editing: weiterhin nur PlatformAdmin (Status quo); MandateAdmin nur `label` | Bestehende `_MANDATE_ADMIN_EDITABLE_FIELDS = {"label"}` passt |
|
||||
| 2026-04-18 | UI-Form-Validierung: live-strict (Eingabe blockiert ungültige Zeichen) | UX-klarer als nachträglicher Fehler |
|
||||
| 2026-04-18 | Migration: idempotent in `interfaceBootstrap`, kein Alembic | Passt zu bestehendem Bootstrap-Pattern |
|
||||
| 2026-04-18 | Migration `label := name` wenn label leer; `name`-Slug-Normalisierung mit Unique-Suffix | User-Anforderung |
|
||||
| 2026-04-18 | Pydantic-`field_validator` für `name`-Format; Unique-Check ausserhalb (in `createMandate`/`updateMandate`) | Pydantic kann nicht DB-uniqueness prüfen |
|
||||
| 2026-04-18 | json_schema_extra labels: `name` → "Kurzzeichen", `label` → "Voller Name" | User-Anforderung |
|
||||
| 2026-04-18 | Neuer `frontend_type: "slug"` mit Live-Maskierung im FormGenerator | Wiederverwendbar für andere Slug-Felder zukünftig |
|
||||
|
||||
## Umsetzungs-Checkliste
|
||||
|
||||
### Phase 1 — Shared Utilities
|
||||
- [x] `gateway/modules/shared/mandateNameUtils.py` mit Transliteration, Slugify, Validierung
|
||||
- [x] Tests: `gateway/tests/unit/shared/test_mandateNameUtils.py`
|
||||
|
||||
### Phase 2 — Pydantic-Modell
|
||||
- [x] `Mandate.name`: json_schema_extra `Kurzzeichen`, `frontend_type: "slug"`, `pattern`, `min_length`/`max_length`
|
||||
- [x] `Mandate.label`: required, json_schema_extra `Voller Name`, `frontend_required`
|
||||
- [x] `field_validator('name')` und `field_validator('label')`
|
||||
|
||||
### Phase 3 — Interface (Backend-Logik)
|
||||
- [x] `interfaceDbApp._generateUniqueMandateName(label, excludeId)` mit Suffix-Schleife
|
||||
- [x] `createMandate(name=None, label, enabled)`: Auto-Generation, Format/Uniqueness, label-mandatory
|
||||
- [x] `updateMandate(mandateId, data)`: Format + Uniqueness (excluding self), label-nonempty
|
||||
- [x] `_provisionMandateForUser(userId, mandateLabel, planKey)`: Parameter umbenannt, Slug abgeleitet
|
||||
|
||||
### Phase 4 — Routes
|
||||
- [x] `POST /api/mandates/`: name optional, server-generiert
|
||||
- [x] `PUT /api/mandates/{id}`: Format-Check fuer name (PlatformAdmin), label-nonempty
|
||||
- [x] `routeSecurityLocal.py`: 3 Aufrufstellen auf `mandateLabel` umgestellt
|
||||
|
||||
### Phase 5 — Migration in Bootstrap
|
||||
- [x] `_migrateMandateNameLabelSlugRules` idempotent in `initBootstrap`
|
||||
- [x] Tests: `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`
|
||||
|
||||
### Phase 6 — Frontend Modell + Hook
|
||||
- [x] `types/mandate.ts`: `label: string` (mandatory) + Doc
|
||||
- [x] `api/mandateApi.ts`: `MandateCreateData` + Semantik-Doc
|
||||
- [x] `hooks/useMandates.ts`: label-zuerst Sortierung, Validierung in Create/Update
|
||||
|
||||
### Phase 7 — FormGenerator: `slug`-Type
|
||||
- [x] `slug` in `attributeTypeMapper.ts` (mapped auf `text`)
|
||||
- [x] `FormGeneratorForm` Slug-Render: Live-Masking, Hint, Validierung, Auto-Vorschlag aus konfigurierbarer `slugSource` (default `label`)
|
||||
- [x] **Korrektur**: generische Logik in `frontend_nyla/src/utils/slugUtils.ts` ausgelagert; `mandateNameUtils.ts` ist dünner Wrapper. FormGenerator bleibt domain-agnostisch.
|
||||
|
||||
### Phase 8 — UI Display Konsistenz
|
||||
- [x] `mandateDisplayUtils.ts` (`mandateDisplayLabel`, `mandateDisplayLineLabelThenSlug`)
|
||||
- [x] `AdminUserAccessOverviewPage.tsx`: `label (name)` Format
|
||||
- [x] `AdminMandatesPage.tsx`: Subtitle-Hinweis + isSystem-Warnung + Delete-Confirm fragt nach Kurzzeichen
|
||||
- [x] Audit aller `m.label || m.name`-Stellen: 9 Frontend-Dateien auf `mandateDisplayLabel` umgestellt
|
||||
|
||||
### Phase 9 — Tests
|
||||
- [x] Unit: `mandateNameUtils` (7 Tests)
|
||||
- [x] Unit: Migration-Idempotenz (9 Tests)
|
||||
- [x] Integration: `createMandate` (12 Tests) — auto-name, validation, collision, RBAC, label-mandatory
|
||||
- [x] Integration: `updateMandate` (12 Tests) — RBAC name (PlatformAdmin/SysAdmin/MandateAdmin), validation, collision, protected fields
|
||||
- [x] Integration: `_provisionMandateForUser` Slug-Contract (9 Tests) — Umlaute, Kollisionen, label-mandatory, plan-guard
|
||||
- **Total: 49 Tests, alle gruen.**
|
||||
|
||||
### Phase 10 — Dokumentation
|
||||
- [x] Neue Datei `b-reference/platform/mandate.md` (kanonische Referenz)
|
||||
- [x] `TOPICS.md` Eintrag fuer Mandate-Identifier
|
||||
- [x] Diese Plan-Doku abgeschlossen, Status `done`
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
| # | Kriterium (Given-When-Then) | Prio |
|
||||
|---|---------------------------|------|
|
||||
| 1 | Given ein PlatformAdmin im Mandate-Create-Form, When er ein `Voller Name` mit Umlauten ("Müller AG") eingibt und kein `Kurzzeichen` setzt, Then generiert der Server `mueller-ag` und persistiert beide Felder | must |
|
||||
| 2 | Given ein bereits existierender Mandant mit `name == "mueller-ag"`, When ein zweiter mit Label "Müller AG" angelegt wird ohne explizites Kurzzeichen, Then erhält dieser `name == "mueller-ag-2"` | must |
|
||||
| 3 | Given ein Mandate-Admin (kein PlatformAdmin), When er versucht das `name`-Feld via PUT zu ändern, Then ignoriert der Server das Feld (Whitelist) und gibt 200 mit nur `label`-Update zurück | must |
|
||||
| 4 | Given ein PlatformAdmin, When er einen Mandate-Namen mit ungültigen Zeichen ("ABC Müller!") via PUT setzt, Then antwortet die API mit 400 und einer i18n-Fehlermeldung | must |
|
||||
| 5 | Given das System bootet, When der Migrations-Hook läuft auf einem Mandanten ohne `label` und mit `name == "Home patrick"`, Then setzt er `label := "Home patrick"` und `name := "home-patrick"` (mit Suffix wenn Kollision) | must |
|
||||
| 6 | Given die Migration läuft zweimal hintereinander, When der zweite Lauf prüft, Then ändert er nichts (Idempotenz) | must |
|
||||
| 7 | Given ein User im Mandate-Create-Form, When er versucht ins `Kurzzeichen`-Feld Grossbuchstaben oder Spaces einzutippen, Then werden diese Zeichen vom Input live verworfen | must |
|
||||
| 8 | Given ein PlatformAdmin im Edit-Modal eines `isSystem`-Mandanten, When er die Felder sieht, Then ist `Kurzzeichen` disabled und ein Warn-Banner erklärt warum | must |
|
||||
| 9 | Given die Mandate-Liste in `AdminUserAccessOverviewPage`, When ein Mandant gerendert wird, Then steht das Label gross/primary und das `name` (Kurzzeichen) sekundär in Klammern | should |
|
||||
| 10 | Given das Auto-Provisioning bei Registrierung eines Users `Patrick.Möller`, When es läuft, Then ist `label == "Home Patrick.Möller"` und `name == "home-patrick-moeller"` (oder Suffix) | must |
|
||||
|
||||
## Testplan
|
||||
|
||||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||||
|----|----|-----|---------------|-----------|--------|
|
||||
| T1 | -- | unit | ja | `gateway/tests/unit/shared/test_mandateNameUtils.py` | done (7) |
|
||||
| T2 | 1, 2 | integration | ja | `gateway/tests/integration/mandates/test_createMandate.py` | done (12) |
|
||||
| T3 | 3, 4 | integration | ja | `gateway/tests/integration/mandates/test_updateMandate.py` | done (12) |
|
||||
| T4 | 5, 6 | integration | ja | `gateway/tests/unit/bootstrap/test_mandateNameMigration.py` | done (9) |
|
||||
| T5 | 7, 8 | manual | nein | Frontend Smoke-Test AdminMandatesPage | done |
|
||||
| T6 | 9 | manual | nein | Frontend Smoke-Test AdminUserAccessOverviewPage | done |
|
||||
| T7 | 10 | integration | ja | `gateway/tests/integration/mandates/test_provisionMandate.py` | done (9) |
|
||||
| T8 | -- | covered by T2/T3 | -- | -- | n/a (Pydantic-Validators implizit getestet) |
|
||||
|
||||
## Links
|
||||
|
||||
- PR: --
|
||||
- Issue: --
|
||||
- Wiki RBAC-Kontext: `wiki/b-reference/platform/rbac.md`
|
||||
- Bezogene UI-Konsolidierung: `wiki/c-work/1-plan/2026-04-pm-consolidated-customer-requirements.md`
|
||||
|
||||
## Abschluss
|
||||
|
||||
- [x] `b-reference/platform/mandate.md` neu angelegt (kanonische Referenz Mandate-Identifier)
|
||||
- [x] `TOPICS.md` aktualisiert (Eintrag "Mandate-Identifier")
|
||||
- [x] Dieses Dokument → `wiki/c-work/4-done/` verschoben (Repo-Konvention statt `z-archive/`)
|
||||
434
c-work/4-done/2026-04-sysadmin-authority-split.md
Normal file
434
c-work/4-done/2026-04-sysadmin-authority-split.md
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
<!-- status: done -->
|
||||
<!-- started: 2026-04-17 -->
|
||||
<!-- finished: 2026-04-17 -->
|
||||
<!-- component: gateway | frontend-nyla | platform -->
|
||||
|
||||
# SysAdmin-Authority-Modell konsolidieren: Flag + Rolle → zwei klar getrennte Flags
|
||||
|
||||
## Beschreibung und Kontext
|
||||
|
||||
Das PORTA-Berechtigungsmodell kennt heute **zwei parallel gepflegte
|
||||
SysAdmin-Marker**, die sich überschneiden:
|
||||
|
||||
1. **`User.isSysAdmin` FLAG** (Spalte in `User`-Tabelle)
|
||||
- Wirkt als **RBAC-Engine-Bypass** in `rbac.py:getUserPermissions` (Zeile 88)
|
||||
→ umgeht alle AccessRules und gibt vollen Zugriff.
|
||||
- Geprüft via `ctx.isSysAdmin`, `requireSysAdmin`, direktes
|
||||
`getattr(user, 'isSysAdmin', False)`.
|
||||
- Verwendet in Routen für System-Operationen (Tokens, Logs, DB-Health,
|
||||
i18n-Master, Teams-SystemBot, global-scoped Files/Sources).
|
||||
|
||||
2. **`sysadmin` ROLE** (UserMandateRole im Root-Mandant)
|
||||
- Gedacht als normale Rolle im Root-Mandant.
|
||||
- Gibt implizit **mandanten-übergreifende** Admin-Rechte, weil die zugehörigen
|
||||
AccessRules auf User-/Mandate-/RBAC-Verwaltung eingestellt sind.
|
||||
- Geprüft via `ctx.hasSysAdminRole`, `_hasSysAdminRole(userId)`,
|
||||
`requireSysAdminRole`.
|
||||
- Verwendet in Routen für cross-mandate Admin-Operationen
|
||||
(User-Management, Mandate-Management, RBAC-Rules, i18n, Subscription).
|
||||
|
||||
### Warum der aktuelle Zustand problematisch ist
|
||||
|
||||
1. **Architektonisch inkonsistent:** Alle anderen Rollen sind **mandanten-scoped**.
|
||||
Nur `sysadmin` ist eine Rolle im Root-Mandant, die **Autorität über andere
|
||||
Mandanten** verleiht. Das durchbricht die Mandanten-Isolation konzeptionell.
|
||||
2. **Doppelte Quelle der Wahrheit:** Flag und Rolle können auseinander driften.
|
||||
Siehe konkreter Security-Bug (Issue 4): Admin entfernt Flag über UI, Rolle
|
||||
bleibt → User behält alle Cross-Mandate-Rechte.
|
||||
3. **Verwirrende Semantik:** „SysAdmin" kann zwei unterschiedliche Dinge
|
||||
bedeuten — je nachdem, wer fragt.
|
||||
4. **Fehlende Differenzierung:** Es ist heute nicht möglich, jemanden
|
||||
auszustatten mit
|
||||
- „darf die Plattform betreiben" (Logs, Tokens, DB-Health)
|
||||
aber nicht mit
|
||||
- „darf über alle Mandanten User und Rollen verwalten".
|
||||
Oder umgekehrt. Beides hängt am selben Flag bzw. an derselben Rolle.
|
||||
|
||||
**Risiko bei Nicht-Umsetzung:**
|
||||
- **SECURITY:** Ex-SysAdmin mit entferntem Flag behält Cross-Mandate-Admin-Rechte
|
||||
(aktiver Bug).
|
||||
- Aufbau technischer Schuld: jede neue Admin-Route muss zwischen Flag und
|
||||
Rolle wählen — Entwickler wählen falsch.
|
||||
- Audit/Compliance: schwer nachzuweisen, wer welche Autorität warum hat.
|
||||
|
||||
## Fokus und kritische Details
|
||||
|
||||
- Drei Kategorien von Autorität sind heute konzeptionell vermischt:
|
||||
- **Infrastruktur-Autorität** (Kategorie A): Logs, Tokens, DB-Health,
|
||||
Registry-Sync, i18n-Master, Billing-Webhooks, Teams-SystemBot.
|
||||
- **Plattform-Governance-Autorität** (Kategorie B–E): Cross-Mandate
|
||||
User-Mgmt, Mandate-Mgmt, RBAC-Catalog, Feature-Registry,
|
||||
User-Access-Overview, Mandanten-Deblockierung.
|
||||
- **Mandanten-Autorität**: alles innerhalb eines spezifischen Mandanten
|
||||
(inkl. Root-Mandant) — hier sind Rollen das richtige Konzept.
|
||||
- Die `sysadmin`-Rolle existiert heute nur im Root-Mandant
|
||||
(`interfaceBootstrap.py:_initSysAdminRole`, `isSystemRole=False`).
|
||||
Ihre AccessRules regeln aber Autorität über **alle** Mandanten.
|
||||
- Die RBAC-Engine hat für `isSysAdmin=True` einen **harten Bypass**
|
||||
(`rbac.py:88`). Das ist kein Missbrauch, sondern eine **bewusste Design-
|
||||
Entscheidung** — so funktionieren „system-ops" unabhängig vom RBAC-Schema.
|
||||
- Bestehende DB-Stände können inkonsistent sein → Einmalige Migration nötig.
|
||||
|
||||
## Ziel und Nicht-Ziele
|
||||
|
||||
**Ziel (Empfehlung, siehe Entscheidungen unten)**
|
||||
|
||||
- Zwei klar getrennte, **einzeln zuteilbare Berechtigungs-Dimensionen**:
|
||||
- `User.isSysAdmin` (umbenannt von Bedeutung her → **„Infrastructure/System
|
||||
Operator"**): RBAC-Bypass für Infrastruktur-Operationen, **keine** implizite
|
||||
Autorität über Mandanten-Daten oder -Verwaltung.
|
||||
- `User.isPlatformAdmin` (**neu**): Cross-Mandate-Governance ohne RBAC-
|
||||
Bypass. Prüfung in den jeweiligen Admin-Routen explizit.
|
||||
- **Eliminierung der `sysadmin`-Rolle** aus dem Root-Mandant.
|
||||
Die Autorität, die bisher an der Rolle hing, wird an `isPlatformAdmin`
|
||||
gebunden.
|
||||
- Beide Flags einzeln vergebbar, einzeln sichtbar im UI, einzeln auditierbar.
|
||||
- Beispiele für eine klare Trennung:
|
||||
- „Operations-Engineer" → `isSysAdmin=true`, `isPlatformAdmin=false`
|
||||
(kann Logs und DB prüfen, aber keine User/Mandate verwalten).
|
||||
- „Customer-Success-Admin" → `isSysAdmin=false`, `isPlatformAdmin=true`
|
||||
(kann Mandanten deblockieren, User-Access-Overview sehen, aber nicht in
|
||||
Server-Logs schauen).
|
||||
- „Plattform-Super-Admin" → beide auf `true`.
|
||||
|
||||
**Explizit NICHT**
|
||||
|
||||
- Einführung einer neuen Rollen-Tabelle (`PlatformRole`) — Overkill für zwei
|
||||
heute bekannte Autoritäts-Achsen; bei Bedarf später zusätzliche Flags.
|
||||
- Änderung der Mandanten-scoped Rollen (admin/user/viewer etc.)
|
||||
- Änderung der RBAC-Engine-Semantik — `isSysAdmin`-Bypass bleibt; er
|
||||
beschränkt sich jetzt aber **eindeutig auf Infrastruktur-Zwecke**.
|
||||
- Wiederverwendung des Begriffs „sysadmin" in der Namensgebung für Cross-
|
||||
Mandate-Governance (explizit vermeiden — eindeutige Namen, keine
|
||||
Ambiguität).
|
||||
|
||||
## Architektur-Analyse: verworfene Alternativen
|
||||
|
||||
### Alternative A (verworfen): Rolle behalten, Flag eliminieren
|
||||
|
||||
- Nur `sysadmin`-Rolle, Flag weg.
|
||||
- **Verworfen**, weil:
|
||||
- Rolle bleibt konzeptionell im Mandanten-Kontext gefangen.
|
||||
- RBAC-Bypass für Infrastruktur-Operationen wäre schwerer umzusetzen
|
||||
(müsste an Rolle hängen → Root-Mandant-Query bei jedem Request).
|
||||
- Infrastruktur-Routen benötigen oft **keinen** Mandanten-Kontext.
|
||||
|
||||
### Alternative B (verworfen): Flag behalten, Rolle eliminieren, eine Flag-Dimension
|
||||
|
||||
- Nur `isSysAdmin` flag, Rolle weg. **Eine einzige** Dimension.
|
||||
- **Verworfen**, weil:
|
||||
- Keine Trennung „pure Infrastruktur" vs. „Cross-Mandate-Governance".
|
||||
- Wer Logs lesen darf, kann automatisch auch Mandanten deblockieren —
|
||||
das ist zu grob und macht Auditierbarkeit schwierig.
|
||||
- Vom User explizit gewünschte Differenzierung geht verloren („sysadmin
|
||||
flag sauber = keine Daten-Berechtigung, nur Admin-Zwecke").
|
||||
|
||||
### Alternative C (verworfen): Separate `PlatformRole`-Tabelle
|
||||
|
||||
- Neues Schema `UserPlatformRole` mit Enum `SYSTEM_OPERATOR` +
|
||||
`PLATFORM_ADMIN` (+ Zukunft).
|
||||
- **Verworfen für Phase 1**, weil:
|
||||
- Zwei Autoritäts-Achsen heute bekannt → zwei Flags sind die einfachste,
|
||||
ausreichende Lösung.
|
||||
- Einführung einer neuen Tabelle = Migration + zusätzliche Query-Overhead.
|
||||
- Kann **später** eingeführt werden, wenn eine dritte Achse entsteht —
|
||||
die beiden Flags migrieren dann trivial in die neue Struktur.
|
||||
|
||||
### Alternative D (EMPFOHLEN): Zwei orthogonale Flags, Rolle eliminiert
|
||||
|
||||
- `isSysAdmin` behält Bedeutung + Bypass, wird aber semantisch auf
|
||||
Infrastruktur eingeschränkt.
|
||||
- `isPlatformAdmin` neu für Cross-Mandate-Governance.
|
||||
- `sysadmin`-Rolle wird aus Root-Mandant + AccessRules entfernt.
|
||||
- **Vorteile:**
|
||||
- Eine Autorität = genau ein Marker (keine Sync-Pflicht).
|
||||
- Einzeln vergebbar → feinere Governance.
|
||||
- Keine Rollen mehr, die Mandanten-Grenzen durchbrechen.
|
||||
- RBAC-Engine-Logik wird einfacher (kein root-mandate-role-lookup mehr).
|
||||
- **Nachteile:**
|
||||
- Migration nötig.
|
||||
- Alle `ctx.hasSysAdminRole` / `requireSysAdminRole` Callsites müssen
|
||||
umgeschrieben werden.
|
||||
|
||||
→ **Entscheidung: Alternative D.**
|
||||
|
||||
## Zielarchitektur im Detail
|
||||
|
||||
### Autoritäts-Matrix (nach Umsetzung)
|
||||
|
||||
| Autorität | Scope | Marker | Prüfung in Route |
|
||||
| -------------------------------------- | ------------------------------ | ------------------------------------------- | --------------------------------------------- |
|
||||
| Logs, Tokens, DB-Health, i18n-Master | global (ohne Mandant) | `isSysAdmin=true` | `requireSysAdmin` |
|
||||
| RBAC-Engine-Bypass | global (Datenzugriff) | `isSysAdmin=true` | `rbac.py:getUserPermissions` (unverändert) |
|
||||
| User-Mgmt cross-mandate | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` (neu) |
|
||||
| Mandate-Mgmt (create/edit/block) | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
|
||||
| RBAC-Rules-Catalog | cross-mandate (Templates) | `isPlatformAdmin=true` | `requirePlatformAdmin` |
|
||||
| Feature-Registry (Admin-Features-UI) | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
|
||||
| User-Access-Overview | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
|
||||
| Billing-Admin-Overview (alle Mandate) | cross-mandate | `isPlatformAdmin=true` | `requirePlatformAdmin` |
|
||||
| Billing eines Mandanten | 1 Mandant | Mandanten-Rolle `admin` | `requireMandateAdmin(mandateId)` (bestehend) |
|
||||
| User-Verwaltung innerhalb Mandant | 1 Mandant | Mandanten-Rolle `admin` | `requireMandateAdmin(mandateId)` |
|
||||
| Feature-Instance-Verwaltung | 1 Mandant | Mandanten-Rolle `admin` | `requireMandateAdmin(mandateId)` |
|
||||
|
||||
### Neue Auth-Helpers (in `modules/auth/authentication.py`)
|
||||
|
||||
```python
|
||||
@property
|
||||
def isPlatformAdmin(self) -> bool:
|
||||
return getattr(self.user, 'isPlatformAdmin', False)
|
||||
|
||||
def requirePlatformAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
|
||||
if not getattr(currentUser, 'isPlatformAdmin', False):
|
||||
raise HTTPException(403, "Platform admin privileges required")
|
||||
audit_logger.logSecurityEvent(userId=str(currentUser.id),
|
||||
mandateId="system", action="platform_admin_action",
|
||||
details="Cross-mandate governance operation")
|
||||
return currentUser
|
||||
```
|
||||
|
||||
`requireSysAdmin` (bestehend) wird **enger definiert** — nur noch für
|
||||
Infrastruktur-Routen.
|
||||
|
||||
### Migration (einmalig, beim Boot, idempotent)
|
||||
|
||||
```
|
||||
for user in alle User:
|
||||
hadSysadminRole = _hasSysAdminRole_legacy(user.id)
|
||||
# Alte Rolle impliziert sowohl Cross-Mandate-Admin als auch (historisch)
|
||||
# impliziten Infrastruktur-Zugriff. Setze neuen Flag konservativ.
|
||||
if hadSysadminRole and not user.isPlatformAdmin:
|
||||
user.isPlatformAdmin = True
|
||||
log.warning(f"Migration: granted isPlatformAdmin to {user.id}")
|
||||
# Flag bleibt wie es ist (unabhängig).
|
||||
after:
|
||||
drop AccessRules WHERE roleId = sysadmin_root_role
|
||||
drop UserMandateRole WHERE roleId = sysadmin_root_role
|
||||
drop Role WHERE id = sysadmin_root_role
|
||||
```
|
||||
|
||||
## Referenzen (anzupassende Stellen)
|
||||
|
||||
### Backend — Datamodel
|
||||
|
||||
| Datei | Was | Änderung |
|
||||
| --------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `gateway/modules/datamodels/datamodelUam.py` | `User` Pydantic + `UserInDB` | Neues Feld `isPlatformAdmin: bool = False` + Validator |
|
||||
| `gateway/modules/datamodels/datamodelUam.py` | Docstring `isSysAdmin` | Neue Semantik („Infrastructure/System Operator") |
|
||||
|
||||
### Backend — Auth
|
||||
|
||||
| Datei | Was | Änderung |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `gateway/modules/auth/authentication.py` | `_hasSysAdminRole`, `requireSysAdminRole`, `_getRootMandateRoleIds` | **Löschen** (bzw. als deprecated Stub behalten für transitional Phase) |
|
||||
| `gateway/modules/auth/authentication.py` | `RequestContext.hasSysAdminRole` | **Löschen** |
|
||||
| `gateway/modules/auth/authentication.py` | `RequestContext.isPlatformAdmin` (neu) | Property |
|
||||
| `gateway/modules/auth/authentication.py` | `requirePlatformAdmin` (neu) | Dependency |
|
||||
| `gateway/modules/auth/__init__.py` | Exporte | `requirePlatformAdmin` exportieren |
|
||||
|
||||
### Backend — Bootstrap
|
||||
|
||||
| Datei | Was | Änderung |
|
||||
| ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `gateway/modules/interfaces/interfaceBootstrap.py` | `_initSysAdminRole` + `_createSysAdminAccessRules` + `_ensureSysAdminAccessRules` | **Löschen** (inkl. aller damit verbundener AccessRules) |
|
||||
| `gateway/modules/interfaces/interfaceBootstrap.py` | `_ensureAdminUser`, `_ensureEventUser` | Setze `isPlatformAdmin=True` zusätzlich zu `isSysAdmin=True` |
|
||||
| `gateway/modules/interfaces/interfaceBootstrap.py` | Neu: `_migrateSysAdminRoleToPlatformAdminFlag()` | Einmalige Migration beim Boot; idempotent |
|
||||
|
||||
### Backend — Routen (alle Callsites von `hasSysAdminRole` / `requireSysAdminRole`)
|
||||
|
||||
| Datei | Callsites | Änderung |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| `gateway/modules/routes/routeDataUsers.py` | Z.106, 159, 236, 325, 445, 517, 571, 586, 919 | `hasSysAdminRole` → `isPlatformAdmin` |
|
||||
| `gateway/modules/routes/routeDataUsers.py` | `_syncSysAdminRole` (aus Sync-Phase 1) | **Löschen** (nicht mehr nötig) |
|
||||
| `gateway/modules/routes/routeDataUsers.py` | `update_user`: Flag-Sync-Logik | Vereinfachen (kein Rolle-Sync mehr) |
|
||||
| `gateway/modules/routes/routeDataMandates.py` | Z.104, 226, 258, 380, 437, 510, 1054 | `hasSysAdminRole`/`requireSysAdminRole` → `isPlatformAdmin`/`requirePlatformAdmin` |
|
||||
| `gateway/modules/routes/routeDataMandates.py` | Reverse-Sync (Z.916–944 aus Sync-Phase 1) | **Löschen** |
|
||||
| `gateway/modules/routes/routeAdminRbacRules.py` | 11 Stellen (Z.245, 365, 490, 537, 588, 668, 756, 839, 1020, 1079, 1140, 1204, 1360) | `hasSysAdminRole`/`requireSysAdminRole` → `isPlatformAdmin`/`requirePlatformAdmin` |
|
||||
| `gateway/modules/routes/routeAdminFeatures.py` | 22 Stellen | analog |
|
||||
| `gateway/modules/routes/routeAdminUserAccessOverview.py` | Z.78, 126, 220 | analog |
|
||||
| `gateway/modules/routes/routeAdminDatabaseHealth.py` | Z.44, 56, 68, 93 | bleibt `requireSysAdmin` (Infrastruktur) |
|
||||
| `gateway/modules/routes/routeAdminLogs.py` | Z.66, 107 | bleibt `requireSysAdmin` (Infrastruktur) |
|
||||
| `gateway/modules/routes/routeAdminDemoConfig.py` | Z.28, 44, 69 | `isPlatformAdmin` (Data-Change-Operation) |
|
||||
| `gateway/modules/routes/routeBilling.py` | Z.89, 145, 737, 1464, 1487 | Cross-Mandate-Views → `isPlatformAdmin`; Mandate-eigene → unverändert |
|
||||
| `gateway/modules/routes/routeI18n.py` | Z.829, 847, 860, 876, 914, 942 | i18n-Master/System → `requireSysAdmin`; Übersetzungs-Management → `isPlatformAdmin`|
|
||||
| `gateway/modules/routes/routeSubscription.py` | Z.49, 306, 488 | `isPlatformAdmin` |
|
||||
| `gateway/modules/routes/routeInvitations.py` | Z.189, 894 | `isPlatformAdmin` |
|
||||
| `gateway/modules/routes/routeSystem.py` | Z.481, 299, 306, 360, 370, 375, 385, 398, 408 | `isPlatformAdmin` für Navigations-Sichtbarkeit |
|
||||
| `gateway/modules/routes/routeAudit.py` | Z.134 | `isPlatformAdmin` (Cross-Mandate-Audit) |
|
||||
| `gateway/modules/routes/routeNotifications.py` | 518 | unverändert (`addRoleToUserMandate` betrifft keine sysadmin mehr) |
|
||||
| `gateway/modules/routes/routeDataFiles.py` | Z.11, 548, 850, 1044 | Global-Scope → `isSysAdmin` bleibt (Infra-Daten) |
|
||||
| `gateway/modules/routes/routeDataSources.py` | Z.10, 56 | analog |
|
||||
| `gateway/modules/routes/routeWorkflowDashboard.py` | 9 Stellen | Cross-Mandate-Übersicht → `isPlatformAdmin` |
|
||||
| `gateway/modules/features/trustee/routeFeatureTrustee.py` | Z.107, 141, 159, 161, 1814 | prüfen pro Callsite (meist `isPlatformAdmin`) |
|
||||
| `gateway/modules/features/teamsbot/routeFeatureTeamsbot.py` | 8 Stellen | System-Bot-Registrierung → `isSysAdmin` bleibt; User-Mgmt → `isPlatformAdmin` |
|
||||
| `gateway/modules/features/chatbot/routeFeatureChatbot.py` | Z.105 | `isPlatformAdmin` |
|
||||
| `gateway/modules/features/realEstate/routeFeatureRealEstate.py` | Z.119 | `isPlatformAdmin` |
|
||||
| `gateway/modules/routes/routeRealEstate.py` | Z.124 | `isPlatformAdmin` |
|
||||
| `gateway/modules/features/workspace/...` | (suchen) | pro Callsite prüfen |
|
||||
| `gateway/modules/interfaces/interfaceDbManagement.py` | Z.637–643 `_isSysAdmin` | Bleibt an Flag gebunden (RBAC-Bypass in Data-Management) |
|
||||
|
||||
### Backend — Services / Demo / Tests
|
||||
|
||||
| Datei | Was | Änderung |
|
||||
| -------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------- |
|
||||
| `gateway/modules/demoConfigs/investorDemo2026.py` | Z.188, 222 | `isPlatformAdmin=True` statt Rolle; `isSysAdmin` wie gehabt |
|
||||
| `gateway/modules/serviceCenter/services/serviceAgent/...` | Z.568 | Liest Flag — kein Change |
|
||||
| `gateway/tests/**` | alle `hasSysAdminRole`/`requireSysAdminRole` Mentions | Tests anpassen |
|
||||
| `gateway/tests/integration/rbac/test_sysadmin_sync.py` (geplant Phase 1) | **entfällt** bzw. wird zu `test_platform_admin_flag.py` | |
|
||||
|
||||
### Frontend
|
||||
|
||||
| Datei | Was | Änderung |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| `frontend_nyla/src/types/mandate.ts` | User-Typ Z.143 | `isPlatformAdmin: boolean` ergänzen |
|
||||
| `frontend_nyla/src/api/userApi.ts` / `authApi.ts` | User-Schema | `isPlatformAdmin?: boolean` ergänzen |
|
||||
| `frontend_nyla/src/utils/userCache.ts` | User-Cache | `isPlatformAdmin` ergänzen |
|
||||
| `frontend_nyla/src/pages/admin/AdminUsersPage.tsx` | Edit-Formular | Zwei separate Toggles: "Systemadmin" und "Plattformadmin"; Tooltip mit Erklärung |
|
||||
| `frontend_nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx` | User-Grid, Detail-Ansicht | Beide Flags anzeigen; Spalten + Filter |
|
||||
| `frontend_nyla/src/pages/billing/BillingAdmin.tsx` | Z.449, 505, 508, 669 | `isSysAdmin` → `isPlatformAdmin` für Cross-Mandate-View |
|
||||
| `frontend_nyla/src/pages/views/teamsbot/*.tsx` | Z.23, 24, 446, 329 | prüfen; meist `isPlatformAdmin` für Admin-UI, `isSysAdmin` für SystemBot-Config |
|
||||
| `frontend_nyla/src/hooks/useUsers.ts` | Z.35, 39, 74, 88, 92, 218, 222 | Beide Flags mitgeben |
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Datum | Entscheidung | Begründung |
|
||||
| ---------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| 2026-04-17 | `sysadmin`-Rolle im Root-Mandant wird eliminiert | Eine Rolle im Mandanten-Kontext, die Autorität über andere Mandanten gibt, ist architektonisch falsch |
|
||||
| 2026-04-17 | Zwei orthogonale Flags: `isSysAdmin` (Infrastruktur) + `isPlatformAdmin` (Cross-Mandate-Governance) | Deckt die heute bekannten zwei Autoritäts-Achsen sauber ab; einzeln vergebbar, einzeln auditierbar |
|
||||
| 2026-04-17 | Keine neue `PlatformRole`-Tabelle | Zwei Achsen → zwei Flags reichen; Tabelle wäre Over-Engineering; spätere Migration trivial möglich |
|
||||
| 2026-04-17 | `isSysAdmin`-RBAC-Bypass bleibt erhalten | Bewusste Engine-Design-Entscheidung; Infrastruktur-Ops brauchen Unabhängigkeit vom RBAC-Schema |
|
||||
| 2026-04-17 | `requirePlatformAdmin` prüft Flag **ohne** RBAC-Bypass | Plattform-Governance ist Admin-UI-Recht, nicht Daten-Bypass-Recht |
|
||||
| 2026-04-17 | Migration idempotent beim Boot, nicht als separates Migrations-Skript | Konsistent mit bisherigem Bootstrap-Stil; kein Down-Time-Risiko |
|
||||
| 2026-04-17 | `isPlatformAdmin` gibt **keinen** impliziten Daten-Zugriff auf Mandante | Separation of concerns: Governance ≠ Daten |
|
||||
| 2026-04-17 | Phase-1-Sync-Code (Flag↔Rolle) wird zurückgerollt | Wird durch finale Lösung obsolet; aktuell committete Helper `_syncSysAdminRole` etc. entfernen |
|
||||
|
||||
## Umsetzungs-Checkliste
|
||||
|
||||
### Phase A — Datamodel + Auth
|
||||
|
||||
- [ ] `User.isPlatformAdmin` Feld in Pydantic + DB-Schema
|
||||
- [ ] `_normalizePlatformAdmin` Validator
|
||||
- [ ] `RequestContext.isPlatformAdmin` Property
|
||||
- [ ] `requirePlatformAdmin` Dependency + Audit
|
||||
- [ ] Export in `modules/auth/__init__.py`
|
||||
- [ ] Docstring-Update `isSysAdmin` Field: neue enge Semantik
|
||||
|
||||
### Phase B — Migration + Bootstrap
|
||||
|
||||
- [ ] `_migrateSysAdminRoleToPlatformAdminFlag()` in `interfaceBootstrap.py`
|
||||
- [ ] Aufruf nach `_initSysAdminRole` (vor dessen Entfernung)
|
||||
- [ ] Test: vorhandene sysadmin-Rolle + User mit UserMandateRole → Flag=True
|
||||
- [ ] `_ensureAdminUser`, `_ensureEventUser`: `isPlatformAdmin=True` setzen
|
||||
- [ ] `investorDemo2026.py`: analog
|
||||
|
||||
### Phase C — Routen umstellen
|
||||
|
||||
- [ ] Alle Callsites laut Referenz-Tabelle bearbeiten
|
||||
- [ ] Nach Umstellung: Code-Such nach `hasSysAdminRole` / `requireSysAdminRole`
|
||||
→ muss 0 Treffer ergeben (ausser deprecated-Stub)
|
||||
- [ ] Navigation (`routeSystem.py`): Menü-Sichtbarkeit korrekt anpassen
|
||||
|
||||
### Phase D — Phase-1-Sync-Code zurückrollen
|
||||
|
||||
- [ ] `routeDataUsers.py:_syncSysAdminRole` entfernen
|
||||
- [ ] `routeDataUsers.py:update_user` Sync-Aufruf entfernen
|
||||
- [ ] `routeDataUsers.py:create_user` Sync-Aufruf entfernen
|
||||
- [ ] `routeDataMandates.py:update_user_roles_in_mandate` Reverse-Sync entfernen
|
||||
|
||||
### Phase E — sysadmin-Rolle entfernen
|
||||
|
||||
- [ ] `_initSysAdminRole` entfernen
|
||||
- [ ] `_createSysAdminAccessRules`, `_ensureSysAdminAccessRules` entfernen
|
||||
- [ ] Migration: sysadmin-Role + AccessRules + UserMandateRole-Einträge löschen
|
||||
- [ ] `_hasSysAdminRole`, `requireSysAdminRole` entfernen (oder als deprecated
|
||||
für 1 Release behalten)
|
||||
|
||||
### Phase F — Frontend
|
||||
|
||||
- [ ] Typen + userCache erweitern
|
||||
- [ ] `AdminUsersPage`: zwei separate Toggles + Tooltip
|
||||
- [ ] `AdminUserAccessOverviewPage`: Flag-Spalten + Filter
|
||||
- [ ] Callsites Frontend (BillingAdmin, Teamsbot-Pages etc.)
|
||||
|
||||
### Querschnitt
|
||||
|
||||
- [ ] **RBAC / Permissions:** Keine neuen AccessRules. Alte sysadmin-AccessRules gelöscht.
|
||||
- [ ] **Neutralisierung:** User-Neutralisierung setzt beide Flags auf False.
|
||||
- [ ] **Navigation / Routing:** Admin-Menü-Sichtbarkeit via `isPlatformAdmin`.
|
||||
- [ ] **Billing-Impact:** keiner.
|
||||
- [ ] **Tests:** Unit-Tests für `requirePlatformAdmin`, Integrations-Tests für
|
||||
Migration.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
| # | Kriterium (Given-When-Then) | Prio |
|
||||
| -- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| 1 | Given User X mit `isSysAdmin=true`, `isPlatformAdmin=false`, When X ruft `GET /api/admin/mandates` auf, Then 403 | must |
|
||||
| 2 | Given User X mit `isSysAdmin=false`, `isPlatformAdmin=true`, When X ruft `GET /api/admin/database-health` auf, Then 403 | must |
|
||||
| 3 | Given User X mit `isPlatformAdmin=true`, When X ruft `GET /api/admin/mandates` auf, Then 200 mit allen Mandanten | must |
|
||||
| 4 | Given DB-Migration: User Y hatte `sysadmin`-Rolle in Root-Mandant, When Gateway bootet, Then Y hat `isPlatformAdmin=true`; sysadmin-Rolle/AccessRules sind weg | must |
|
||||
| 5 | Given Admin entzieht User X den `isPlatformAdmin`-Flag, When X im nächsten Request `GET /api/admin/rbac-rules` aufruft, Then 403 (ohne Token-Refresh, nächster Request prüft live DB) | must |
|
||||
| 6 | Given Admin entzieht User X den `isSysAdmin`-Flag, When X im nächsten Request `GET /api/admin/logs` aufruft, Then 403 | must |
|
||||
| 7 | Given `Admin > USER`-Formular, When Admin bearbeitet User, Then zwei separate Toggles „Systemadmin" und „Plattformadmin" sind sichtbar + dokumentiert | must |
|
||||
| 8 | Given User X versucht sich selbst `isSysAdmin` oder `isPlatformAdmin` zu verändern, Then 403 (Self-Protection für beide Flags) | must |
|
||||
| 9 | Given `_hasSysAdminRole` / `requireSysAdminRole` werden importiert, When Codebase gescannt wird, Then 0 Treffer (oder nur deprecated-Stub mit Logger-Warning) | should |
|
||||
| 10 | Given `sysadmin`-Rolle existiert noch in DB, When Migration läuft, Then Rolle + zugehörige UserMandateRole + AccessRules werden gelöscht; Flag der betroffenen User ist konsistent gesetzt | must |
|
||||
| 11 | Given „Operations-Engineer"-User mit `isSysAdmin=true`, `isPlatformAdmin=false`, When er die Admin-UI öffnet, Then sieht er System-Menüpunkte (Logs, DB-Health), aber keine Mandate-/User-Verwaltung | should |
|
||||
| 12 | Given „Customer-Success-Admin" mit `isSysAdmin=false`, `isPlatformAdmin=true`, When er die Admin-UI öffnet, Then sieht er User-/Mandate-Verwaltung, aber keine Logs | should |
|
||||
|
||||
## Testplan
|
||||
|
||||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||||
| --- | ---- | ------------------------ | ------------- | ------------------------------------------------------------------ | ------- |
|
||||
| T1 | 1 | api-integration | ja | `gateway/tests/integration/rbac/test_platform_admin_flag.py` | done |
|
||||
| T2 | 2 | api-integration | ja | dito | done |
|
||||
| T3 | 3 | api-integration | ja | dito | done |
|
||||
| T4 | 4,10 | migration-unit | ja | `gateway/tests/unit/rbac/test_sysadmin_migration.py` | done |
|
||||
| T5 | 5,6 | api-integration | ja | `gateway/tests/integration/rbac/test_platform_admin_flag.py` | done |
|
||||
| T6 | 7 | frontend (Playwright) | optional | `frontend_nyla/e2e/admin-users-flags.spec.ts` (neu) | open |
|
||||
| T7 | 8 | api-unit | ja | `gateway/tests/integration/rbac/test_platform_admin_flag.py` | done |
|
||||
| T8 | 9 | codebase-scan | ja (CI-Check) | `scripts/check_no_sysadmin_role.py` (CI-Gate) | done |
|
||||
| T9 | 11,12| manuelle UI-Regression | manuell | Siehe Security-Regression-Manual | open |
|
||||
|
||||
**Security-Regression-Manual (T9)**
|
||||
|
||||
1. User A: `isSysAdmin=true`, `isPlatformAdmin=false`. Login.
|
||||
- `Admin > Logs` → ok.
|
||||
- `Admin > DB-Health` → ok.
|
||||
- `Admin > Mandate` → 403/nicht sichtbar im Menü.
|
||||
- `Admin > User` → 403/nicht sichtbar.
|
||||
2. User B: `isSysAdmin=false`, `isPlatformAdmin=true`. Login.
|
||||
- `Admin > Mandate` → ok.
|
||||
- `Admin > User` → ok.
|
||||
- `Admin > User Access Overview` → ok.
|
||||
- `Admin > Logs` → 403.
|
||||
- `Admin > DB-Health` → 403.
|
||||
3. User C: beide `false`. Login.
|
||||
- Keine Admin-Menüpunkte sichtbar.
|
||||
4. Flag-Entzug im laufenden Betrieb: Admin entzieht B das `isPlatformAdmin`-Flag.
|
||||
B's nächster Request auf `Admin > Mandate` → 403 (ohne Re-Login).
|
||||
5. sysadmin-Rolle-Check: kein User hat mehr eine sysadmin-Rolle in DB
|
||||
(`SELECT COUNT(*) FROM "Role" WHERE "roleLabel" = 'sysadmin'` = 0).
|
||||
|
||||
## Links
|
||||
|
||||
- RBAC-Referenz: `wiki/b-reference/platform/rbac.md`
|
||||
- Auth-Modul: `gateway/modules/auth/authentication.py`
|
||||
- Ausgangspunkt Testing PORTA 2026-04-17 (Issue 4: Security-Bug)
|
||||
|
||||
## Abschluss
|
||||
|
||||
- [x] `wiki/b-reference/platform/rbac.md`: Abschnitt „Platform-Governance-Autoritaet" ergaenzt
|
||||
- [ ] `wiki/TOPICS.md`: Topic „Authority-Modell: System/Platform/Mandate" anlegen
|
||||
- [x] Dieses Dokument nach Abschluss → `wiki/c-work/4-done/` verschoben
|
||||
- [x] CI-Gate `gateway/scripts/check_no_sysadmin_role.py` (Acceptance T8 / AC#9)
|
||||
|
||||
### Umsetzungs-Status
|
||||
|
||||
- Phase A (Datamodel + Auth) : done
|
||||
- Phase B (Migration + Bootstrap) : done
|
||||
- Phase C (Routen umstellen) : done — 0 Treffer fuer hasSysAdminRole/requireSysAdminRole/_hasSysAdminRole
|
||||
- Phase D (Phase-1-Sync zurueckgerollt) : done
|
||||
- Phase E (sysadmin-Rolle entfernt) : done — Stubs entfernt, Migration entfernt Rolle/AccessRules/Memberships
|
||||
- Phase F (Frontend) : done — Typen + AdminUserAccessOverview + AdminMandates + BillingAdmin + useUsers
|
||||
- Tests + CI-Gate : done — T1-T5, T7, T8 automatisiert (13 grün); T6 (Playwright) + T9 (manuell) optional/offen
|
||||
|
||||
Loading…
Reference in a new issue