Phase 10: Mandantenfaehigkeit
Ein Server, mehrere Wehren. Ein Konto, mehrere Zugehoerigkeiten. Eine Kreisebene, die zusieht. Datenmodell und RLS trugen seit Phase 0 mehrere Wehren. Was fehlte, war alles darueber: bis hierher war die Personenakte zugleich das Anmeldekonto, und die gehoert genau einer Wehr. Wer in zweien gefuehrt wurde, brauchte zwei Passwoerter, und die Anmeldung per E-Mail war mehrdeutig. Neu sind `Benutzer` (das Konto) und `Mitgliedschaft` (die Zugehoerigkeit). Das Konto traegt bewusst keine Mandantenkennung und kann keine tragen - es ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehoert. Geschuetzt wird es deshalb ueber Rechte statt ueber RLS, wie `Berechtigung` seit Phase 0. Was es in einer Wehr darf, entscheidet seine Mitgliedschaft. Die Sitzung behaelt ihre Form und entsteht nur anders - aus der aktiven Mitgliedschaft statt aus der Person. Von 414 Fundstellen in 61 Dateien musste dadurch keine angefasst werden. Die eine Ausnahme ist Absicht: `personId` ist nullbar geworden, weil eine Aufsichtssitzung keine Akte hat, und der Typechecker hat daraus genau die fuenf Stellen gemacht, an denen die Kreisaufsicht nichts verloren hat. Ihre Rechte werden gefiltert statt aufgezaehlt: alles, was `.lesen` heisst, minus Kasse, Audit-Log, KI-Protokoll und Personenexport. So bekommt sie neue Leserechte automatisch und neue Schreibrechte nie. Die Migration ist datenerhaltend und bricht ab, statt einen halben Zustand zu hinterlassen. Nebenbei behoben: `db:test-reset` liess die Nachbarwehr stehen, der Fehlversuch-Test sperrte das gemeinsame Testkonto, ein Testaufbau war nicht wiederholbar, und die Wartelogik der Bildschirmfotos konnte ausloesen, bevor eine Seite ueberhaupt geladen war. 581 Unit-Tests und 266 E2E-Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2b53556175
commit
ff40437a1a
@@ -33,6 +33,7 @@ Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
|
||||
| Phase 7 — Benachrichtigungen und Termine | fertig |
|
||||
| Phase 8 — Lokale KI | fertig |
|
||||
| Phase 9 — Kassenverwaltung | fertig |
|
||||
| Phase 10 — Mandantenfähigkeit | fertig |
|
||||
|
||||
Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und
|
||||
[docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md).
|
||||
@@ -1305,6 +1306,147 @@ läuft: Postgres, MinIO für die Belege und Gotenberg für die PDFs.
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — Mandantenfähigkeit
|
||||
|
||||
Ein Server, mehrere Wehren. Ein Konto, mehrere Zugehörigkeiten. Eine
|
||||
Kreisebene, die zusieht.
|
||||
|
||||
Datenmodell und Row Level Security tragen seit Phase 0 mehrere Wehren — jede
|
||||
Tabelle hat `organisationId`, jede Abfrage läuft im Mandantenkontext. Was
|
||||
fehlte, war alles darüber: eine Anmeldung, die zu mehr als einer Wehr gehören
|
||||
kann, ein Weg zum Umschalten, und eine Verwaltung für den, der den Server
|
||||
betreibt.
|
||||
|
||||
### Konto und Akte sind nicht dasselbe
|
||||
|
||||
Bis Phase 9 war die Personenakte zugleich das Anmeldekonto: Passwort-Hash und
|
||||
OIDC-Subject hingen an `person`, und `person` gehört genau einer Wehr. Daraus
|
||||
folgten zwei Dinge, die niemand wollte:
|
||||
|
||||
- Wer in zwei Wehren geführt wurde, brauchte **zwei Passwörter**, die
|
||||
auseinanderlaufen können.
|
||||
- Die Anmeldung per E-Mail war **mehrdeutig**, sobald dieselbe Adresse in zwei
|
||||
Wehren stand — die Eindeutigkeit galt nur je Organisation, und gefunden
|
||||
wurde, was die Datenbank zuerst hergab.
|
||||
|
||||
Seit Phase 10:
|
||||
|
||||
| Tabelle | Was sie ist | Mandant |
|
||||
|---|---|---|
|
||||
| `Benutzer` | das Anmeldekonto — E-Mail, Passwort, OIDC | **keiner** |
|
||||
| `Mitgliedschaft` | Zugehörigkeit eines Kontos zu einer Wehr | `organisationId` |
|
||||
| `Person` | die Akte in einer Wehr — Dienstgrad, Qualifikationen, Einsätze | `organisationId` |
|
||||
|
||||
**`Benutzer` trägt bewusst keine Mandantenkennung** und kann keine tragen: sie
|
||||
ist ja der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehört. RLS greift
|
||||
dort deshalb nicht. Geschützt wird sie stattdessen über Rechte — dieselbe
|
||||
Lösung wie bei `Berechtigung` aus Phase 0 und aus demselben Grund: eine
|
||||
Tabelle, die über allen Mandanten steht, gehört nicht in die Hand der
|
||||
Anwendungsrolle. Was ein Konto in einer Wehr darf, entscheidet nicht es
|
||||
selbst, sondern seine `Mitgliedschaft`.
|
||||
|
||||
Ein angenehmer Nebeneffekt: der Passwort-Hash kann aus einer Personenabfrage
|
||||
gar nicht mehr herausfallen. Die drei Stellen, die ihn bisher vorsichtshalber
|
||||
abgeschnitten haben, sind entfallen — eine Sorgfaltsmaßnahme ist durch eine
|
||||
Struktur ersetzt.
|
||||
|
||||
### Die Wehr wechseln
|
||||
|
||||
Der Wähler steht in der Seitenleiste unter dem Namen der Wehr — **aber nur,
|
||||
wenn es etwas zu wählen gibt.** Für die allermeisten gibt es das nie, und ein
|
||||
Auswahlfeld mit genau einem Eintrag ist keine Hilfe, sondern eine Frage ohne
|
||||
Antwortmöglichkeit.
|
||||
|
||||
Nach der Anmeldung öffnet die **eigene** Wehr, nicht die betreute: wer eine
|
||||
Wehr führt und acht andere beaufsichtigt, will in seiner landen.
|
||||
|
||||
Technisch: die Wehr steht im Sitzungstoken, also wird beim Wechsel ein neues
|
||||
ausgestellt. Was der Browser dabei schickt, ist ein **Wunsch** — geprüft wird
|
||||
serverseitig gegen die Mitgliedschaften des Kontos. Eine fremde Kennung zu
|
||||
nennen bringt nichts, weil es dazu keine Mitgliedschaft gibt.
|
||||
|
||||
### Die Kreisebene
|
||||
|
||||
Kreisbrandrat und Kreisbrandinspektion gehören den betreuten Wehren nicht an.
|
||||
Sie bekommen deshalb dort **keine Personenakte**, tauchen in keiner
|
||||
Mitgliederliste auf und können nichts ändern.
|
||||
|
||||
Ihre Rechte werden nicht aufgezählt, sondern **gefiltert**: alles, was
|
||||
`.lesen` heißt, und sonst nichts. Eine von Hand gepflegte Liste wäre bei der
|
||||
nächsten Phase veraltet, und zwar unbemerkt — ein neues Modul brächte Rechte
|
||||
mit, die niemand nachträgt. So bekommt die Aufsicht neue Leserechte
|
||||
automatisch und neue Schreibrechte nie.
|
||||
|
||||
Vier Rechte sind ausdrücklich ausgenommen, obwohl sie auf `.lesen` enden:
|
||||
|
||||
| Recht | Warum nicht |
|
||||
|---|---|
|
||||
| `kasse.lesen` | Die Kasse gehört dem Verein oder der Kommune, nicht der Kreisaufsicht. |
|
||||
| `person.export` | Das Auskunftsrecht nach DSGVO erfüllt die Wehr, die die Daten führt. |
|
||||
| `system.auditlog` | Wer wann was getan hat, bleibt innerhalb der Wehr. |
|
||||
| `ki.protokoll` | Dasselbe. |
|
||||
|
||||
Der **Atemschutz** steht dagegen bewusst drin: ob ein Träger tauglich ist, ist
|
||||
genau die Frage, für die es eine Kreisebene gibt.
|
||||
|
||||
Wer als Aufsicht unterwegs ist, sieht dauerhaft einen Hinweis darauf — nicht
|
||||
als kurz aufblitzende Meldung, sondern als Zeile über der Ansicht. Man soll
|
||||
nicht erst merken, in welcher Rolle man ist, wenn ein Knopf fehlt.
|
||||
|
||||
### Doppelmitgliedschaft
|
||||
|
||||
Ein Konto, mehrere Zugehörigkeiten. In Bayern der Regelfall bei allem, was
|
||||
über die eigene Wehr hinausgeht: aktiv in A, Ehrenmitglied in B, oder
|
||||
Kommandant der einen und Kreisbrandinspektor für sechs weitere.
|
||||
|
||||
Zwei Zusicherungen setzt die Datenbank durch:
|
||||
|
||||
- **Ein Konto gehört einer Wehr höchstens einmal an.**
|
||||
- **Eine Personenakte hängt an höchstens einem Konto.** Sonst wäre nicht
|
||||
entscheidbar, wer unter ihrem Namen gehandelt hat.
|
||||
|
||||
Und eine dritte, die weniger offensichtlich ist: die Akte muss zu **derselben
|
||||
Wehr** gehören wie die Zuordnung. Ein Fremdschlüssel prüft nur, *dass* es die
|
||||
Akte gibt, nicht *wo* sie liegt — ohne diese Prüfung ließe sich ein Konto über
|
||||
eine Akte aus Wehr A in Wehr B hängen, und die Sitzung stünde dort mit einer
|
||||
Personenkennung da, die niemandem gehört. Ein Trigger lehnt das ab.
|
||||
|
||||
### Wehren und Konten verwalten
|
||||
|
||||
Unter **`/wehren`**, und nur für den, der die Installation betreibt.
|
||||
|
||||
Der Zugang hängt an einem Merkmal des Kontos (`istVerwalter`), **nicht an
|
||||
einem Recht**. Rechte gelten je Wehr; wer Wehren anlegt, steht über allen. Ein
|
||||
Recht, das in Wehr A vergeben wird und in Wehr B wirkt, wäre genau die
|
||||
Hintertür, die diese Phase vermeiden soll.
|
||||
|
||||
Was dort geht:
|
||||
|
||||
- Eine Wehr anlegen — sie bekommt sofort ihre Systemrollen samt Rechten. Ein
|
||||
Konto bekommt sie nicht: eine Wehr anzulegen und jemanden hineinzusetzen
|
||||
sind zwei Entscheidungen, und die zweite trifft meist jemand anders.
|
||||
- Ein Konto anlegen — ohne Wehr. Die kommt über die Zuordnung.
|
||||
- Zuordnen: als Mitglied mit Personenakte oder als Kreisaufsicht ohne.
|
||||
|
||||
**Jede Zuordnung wird in der betroffenen Wehr protokolliert**, nicht beim
|
||||
Verwalter. Sonst hätte der Betreiber einen Weg, sich still in fremde Daten zu
|
||||
setzen.
|
||||
|
||||
**Keine zusätzlichen Umgebungsvariablen.**
|
||||
|
||||
### Was bewusst fehlt
|
||||
|
||||
**Wehrübergreifende Auswertungen.** Eine Zahl über mehrere Wehren hinweg
|
||||
braucht eine abgestimmte Definition — was zählt als Einsatz, was als Übung,
|
||||
wie wird eine Doppelmitgliedschaft gezählt. Ohne die ist eine solche Zahl
|
||||
schlimmer als keine, weil sie benutzt wird. Die Aufsicht sieht jede Wehr
|
||||
einzeln, und das reicht für die Fragen, die tatsächlich gestellt werden.
|
||||
|
||||
**Ein Einladungsverfahren.** Passwörter werden vom Verwalter vergeben und
|
||||
weitergegeben. FLORIAN verschickt keine.
|
||||
|
||||
---
|
||||
|
||||
## Befehle
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user