Phase 10: Mandantenfähigkeit (#15)
This commit was merged in pull request #15.
This commit is contained in:
@@ -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