4fdd6eed34
Der Plan benannte vier handgepflegte Stellen als die, die im dkv-Durchlauf uebersehen wurden, gatete aber nur zwei davon. Ein Ausfuehrender haette Aufgabe 3 abschliessen, jede Pruefung bestehen und Uebersichtszeile, Summenzeile und Klassen-Verteilung stehen lassen koennen — derselbe Fehler, gegen den der Plan schuetzen sollte, im Gate des Plans selbst. Die beiden fehlenden Pruefungen leiten ihre Erwartung ab, statt sie zu raten: die Uebersichtszeile gegen die am Quelltext neu ermittelten Rohtrefferzahlen (mit den beiden Befehlen, die das Dokument selbst nennt), die Summenzeile gegen die Addition der zwoelf Bereichszeilen, die Klassen-Verteilung gegen die ueber die Bestandsaufnahme nachgezaehlten Klassen samt Summe und Ueberschrift. Falsifiziert statt behauptet: gruen gegen das heutige, in sich stimmige Dokument; rot gegen den heutigen unumgestellten Baum; und rot in vier getrennten Mutationen, je eine stehen gelassene Handstelle — Uebersichtszeile, Summenzeile, Klassenzahl, Ueberschrift. Gruen erst, wenn Code umgestellt UND alle vier nachgezogen sind. Zusatzauflage, die daraus folgt: der gebundene Klient heisst in jeder Methode tenantPrisma (Konvention aus ldap/groups/dkv/auth) — ein anderer Name liesse die Gebunden-Zaehlung untertreiben und die Zeile ihrer Aussage berauben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
1050 lines
88 KiB
Markdown
1050 lines
88 KiB
Markdown
---
|
|
phase: quick-260910-das
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
autonomous: true
|
|
requirements: [WINDOWS-18, ETAPPE-2-USER]
|
|
|
|
files_modified:
|
|
- apps/api/scripts/rls-scratch-check.mjs
|
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
|
- apps/api/src/user/user.service.ts
|
|
- apps/api/src/user/user.service.spec.ts
|
|
- apps/api/src/user/admin-seed.service.ts
|
|
- apps/api/src/user/admin-seed.service.spec.ts
|
|
- apps/api/src/user/user.controller.ts
|
|
- apps/api/src/user/user.controller.spec.ts
|
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
|
- .planning/WINDOWS.md
|
|
|
|
estimate:
|
|
tokens: 190000
|
|
raw_tokens: 190000
|
|
tasks: 3
|
|
confidence: low
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Die Linie zwischen 'muss binden' und 'darf nicht binden' ist in diesem Bereich je Methode gezogen, im Code an der jeweiligen Stelle begruendet, und keine der beiden Richtungen ist stillschweigend entschieden worden. Dieser Bereich enthaelt als einziger BEIDE Formen gleichzeitig: Benutzerverwaltung je Mandant, die binden MUSS, und Nachschlagewege auf plattformweit eindeutigen Schluesseln, die binden NICHT DUERFEN."
|
|
- "Die Kette, die dieser Bereich als einziger vollstaendig enthaelt, ist an der echten, ausgelieferten Policy GEMESSEN und nicht behauptet: eine gebundene Suche auf `username` sieht einen fremden Halter nicht, meldet damit 'frei', und das darauf folgende Einfuegen scheitert trotzdem hart an der plattformweiten Eindeutigkeit — die Ablehnung ist nachweislich eine Eindeutigkeitsverletzung und keine Zeilenschutz-Ablehnung, denn nur die erste ist die beschriebene Kette."
|
|
- "Die schwerste Auspraegung der umgekehrten Fehlerrichtung im ganzen Vorhaben ist gefunden, gemessen und entschaerft: die Erstanlage-Pruefung beim Start deutet Leere als 'der Administrator existiert nicht', legt daraufhin an, und laeuft in die plattformweite Eindeutigkeit — und weil der Erstanlage-Schritt bewusst NICHT gekapselt ist, startet die Anwendung nach dem Scharfschalten nicht mehr. Die Entschaerfung liegt im Anwendungscode, nicht im Schema."
|
|
- "Die Klassifikationszeile fuer die Erstanlage des Administrators ist als FALSCH belegt und korrigiert: ihre Begruendung behauptet, es gebe strukturell keinen Mandanten zum Binden — der Mandant wird eine Anweisung vorher angelegt und ist bekannt. Ungebunden waere dieses Einfuegen nach dem Scharfschalten abgewiesen worden, eine frische Installation haette ihren ersten Administrator gar nicht anlegen koennen."
|
|
- "Der Anmeldeweg aus Etappe 1 ist unberuehrt: die drei schmalen SECURITY-DEFINER-Funktionen und ihre Aufrufer bleiben, wie sie sind. Dass die Nachschlagemethode nach Benutzername in diesem Bereich heute KEINEN Aufrufer mehr hat und ihr Kommentar das Gegenteil behauptet, ist gemessen und im Kommentar richtiggestellt statt weiter mitgeschleppt."
|
|
- "Die uebergreifende Sicht des Plattform-Administrators bleibt erhalten, ohne ungebunden zu sein: sie laeuft als Schleife ueber alle Mandanten mit je EINEM gebundenen Lesezugriff im Rumpf — dieselbe Form, die die Standardgruppen-Reparatur beim Start bereits benutzt. Dass der Schleifentreiber selbst ungebunden lesen darf, haengt daran, dass die Mandantentabelle keinen Zeilenschutz traegt, und das ist gemessen."
|
|
- "Eine bereits bestehende, heute wirksame Luecke ist geschlossen: der Riegel gegen das Loeschen des eigenen Kontos vergleicht gegen ein Feld, das der Sitzungsnachweis gar nicht traegt, und greift deshalb nie. Ein Administrator kann sich heute selbst loeschen und einen Mandanten ohne Verwaltung zuruecklassen."
|
|
- "Die Testlage dieses Bereichs ist repariert und die Form der Luecke ist benannt: zwei vorhandene Testdateien haben KEINE Attrappe fuer das Bindungshilfsmittel und waeren nach der Umstellung aus dem falschen Grund rot geworden, die Steuerungsschicht hat gar keine Testdatei. Nach diesem Durchlauf existiert je Datei ein Zwei-Klienten-Nachweis, dessen Rotwerden durch probeweisen Rueckbau belegt ist."
|
|
- "Klassifikationsdokument und `rls-access-inventory.spec.ts` zeigen fuer alle Paare dieses Bereichs denselben, maschinell gemessenen Stand — einschliesslich des NEUEN Paares, das die Schleife ueber alle Mandanten in `user.service.ts` erzeugt. Uebersichtszeile, Summenzeile, Klassen-Verteilungstabelle samt ihrer Summe und der Abschnitt zur Hintergrunddienst-Falle sind von Hand nachgezogen — und jede der vier haengt an einer eigenen maschinellen Pruefung, die die Zahlen aus Quelltext beziehungsweise Bestandsaufnahme neu ableitet, statt nur das Vorhandensein einer Zeile zu bestaetigen. Genau diese vier Stellen wurden im `dkv`-Durchlauf uebersehen; eine Pruefung, die eine stehen gebliebene Fassung bestehen liesse, waere schlimmer als keine, weil sie Sicherheit vortaeuscht."
|
|
- "789 Tests und die Typpruefung sind gruen, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; Schema, Migrationen, alle vier Compose-Dateien und beide Beispiel-Umgebungsdateien sind unveraendert; `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`, der Schalter bleibt AUS."
|
|
artifacts:
|
|
- apps/api/scripts/rls-scratch-check.mjs
|
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
|
- apps/api/src/user/user.service.ts
|
|
- apps/api/src/user/user.service.spec.ts
|
|
- apps/api/src/user/admin-seed.service.ts
|
|
- apps/api/src/user/admin-seed.service.spec.ts
|
|
- apps/api/src/user/user.controller.ts
|
|
- apps/api/src/user/user.controller.spec.ts
|
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
|
- .planning/WINDOWS.md
|
|
key_links:
|
|
- "gebundener Klient <-> die Policy `tenant_isolation_policy` auf `User`, wortgleich aus der ausgelieferten Migration `20260618112133_rls_policies` herausgeschnitten statt im Werkzeug nachgetippt — und zusaetzlich gegen die im Werkzeug bereits von Hand getippte Fassung des Anmeldeweg-Abschnitts gehalten"
|
|
- "plattformweit eindeutiger `username`/`email` <-> gebundene Kollisionspruefung — die eine Stelle, an der Binden den Schaden ERZEUGT statt ihn zu verhindern; der Praezedenzfall `resolveEmailForWrite` im Bereich `ldap` ist wortgleich derselbe Fall auf `email`"
|
|
- "Erstanlage-Pruefung beim Start <-> plattformweite Eindeutigkeit von `username` <-> der bewusst ungekapselte Erstanlage-Schritt — die Kette, an deren Ende die Anwendung nach dem Scharfschalten nicht mehr startet"
|
|
- "frisch angelegter Mandant <-> das unmittelbar folgende Einfuegen des ersten Administrators — der Beleg, dass die Klassifikationsbegruendung 'strukturell nichts zum Binden' falsch ist"
|
|
- "Mandantentabelle ohne Zeilenschutz <-> Schleifentreiber der Plattform-Administratorsicht und der Standardgruppen-Reparatur — die einzige Tabelle dieses Bereichs, die ungebunden gelesen werden DARF, und der Grund, warum die Umstellung der uebergreifenden Sicht ueberhaupt moeglich ist"
|
|
- "die drei SECURITY-DEFINER-Funktionen aus Etappe 1 <-> die Nachschlagemethode nach Benutzername in `user.service.ts` — die Grenze, die dieser Durchlauf nicht verschieben darf, und der Kommentar, der sie heute falsch beschreibt"
|
|
- "`rls-access-inventory.spec.ts` <-> Stand-Spalte, Klassen-Verteilung und Summenzeilen des Klassifikationsdokuments fuer alle vier bisherigen plus das eine neue Paar dieses Bereichs"
|
|
---
|
|
|
|
<objective>
|
|
Der Bereich `user` ist der fuenfte Bereich der Etappe 2 und derjenige, in dem das
|
|
plattformweite Eindeutigkeitsproblem tatsaechlich wohnt. `username` und `email`
|
|
sind im Schema plattformweit eindeutig, nicht je Mandant. Der Bereich `ldap` ist
|
|
diesem Problem schon einmal begegnet und hat `resolveEmailForWrite` mit einer im
|
|
Code niedergeschriebenen Begruendung bewusst ungebunden gelassen. Hier stossen
|
|
beide Formen in EINER Datei aufeinander: Wege, die binden muessen (die
|
|
Benutzerverwaltung je Mandant), und Wege, die nicht binden duerfen (Nachschlagen
|
|
auf einem plattformweit eindeutigen Schluessel).
|
|
|
|
Zweck: Dieser Bereich legt Konten an, aendert sie, vergibt Rollen und schaltet
|
|
Konten scharf oder ab. Ein Quer-Schreiben ist keine Offenlegung, sondern eine
|
|
Rechteausweitung ueber die Mandantengrenze hinweg — die schwerste Klasse dieses
|
|
ganzen Vorhabens.
|
|
|
|
Die diesem Bereich eigene Fehlerform ist die gefaehrlichste von allen fuenf: ein
|
|
Verwaltungsweg, der nach dem Scharfschalten nichts mehr findet, sieht exakt so aus
|
|
wie "diesen Benutzer gibt es nicht". Und die natuerliche naechste Handlung eines
|
|
Aufrufers, der glaubt, einen Benutzer gebe es nicht, ist ihn ANZULEGEN — woraufhin
|
|
er auf einem plattformweit eindeutigen Schluessel kollidiert. Diese Kette ist in
|
|
diesem Bereich nicht hypothetisch: sie steht beim Anwendungsstart, im Erstanlage-Weg
|
|
des Administrators, und sie endet dort damit, dass die Anwendung nicht mehr startet.
|
|
|
|
Ergebnis: Die Kritikschrift bekommt einen `user`-Abschnitt mit dieser Kette. Das
|
|
Messwerkzeug bekennt die Kette als Messung statt als Behauptung — bis hinunter zu
|
|
der Unterscheidung, ob die Ablehnung eine Eindeutigkeitsverletzung oder eine
|
|
Zeilenschutz-Ablehnung ist. Die Verwaltungswege binden, die Nachschlagewege bleiben
|
|
mit geschriebener Begruendung ungebunden, die uebergreifende Sicht des
|
|
Plattform-Administrators wird als gebundene Schleife ueber alle Mandanten
|
|
umgestellt statt aufgegeben, eine heute wirksame Luecke im Selbstloesch-Riegel wird
|
|
geschlossen, und der Bereich bekommt zum ersten Mal Tests, die eine vergessene
|
|
Bindung ueberhaupt bemerken koennen.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/STATE.md
|
|
@docs/mandantentrennung-zugriffsklassifikation.md
|
|
@docs/mandantentrennung-etappe2-fehlerrichtung.md
|
|
@apps/api/src/prisma/prisma-tenant.extension.ts
|
|
@apps/api/src/prisma/rls-access-inventory.spec.ts
|
|
@apps/api/scripts/rls-scratch-check.mjs
|
|
@apps/api/src/groups/groups.service.spec.ts
|
|
@apps/api/src/ldap/ldap.service.ts
|
|
@apps/api/src/auth/auth.service.ts
|
|
@apps/api/prisma/migrations/20260618112133_rls_policies/migration.sql
|
|
@apps/api/prisma/migrations/20260909160000_auth_lookup_functions/migration.sql
|
|
@apps/api/src/user/user.service.ts
|
|
@apps/api/src/user/user.controller.ts
|
|
@apps/api/src/user/admin-seed.service.ts
|
|
@apps/api/src/user/user.service.spec.ts
|
|
@apps/api/src/user/admin-seed.service.spec.ts
|
|
@CLAUDE.md
|
|
</context>
|
|
|
|
<planning_time_findings>
|
|
|
|
Alles Folgende wurde am 2026-09-10 zur Planungszeit am lebenden Baum gemessen. Die
|
|
Zahlen und Zeilenangaben aus dem Auftrag waren Hinweise zum Aufschlagen, keine
|
|
Aenderungsvollmacht — jede Fundstelle wurde einzeln aufgeschlagen. Auch die Zahlen
|
|
in diesem Abschnitt sind Planungsstand: bei der Ausfuehrung neu messen, nicht
|
|
abschreiben.
|
|
|
|
**Ausgangsstand (jetzt gemessen, nicht aus einem Bericht zitiert):**
|
|
|
|
- `git rev-parse --short HEAD` -> `ccb5996`, Arbeitsbaum sauber. Deckt sich mit dem
|
|
Auftrag.
|
|
- `npm --prefix apps/api run test` -> **54 Dateien, 789 Tests, gruen**, 5,00 s,
|
|
Rueckgabewert 0.
|
|
- Das Wegwerf-Werkzeug meldet **"Alle 41 Pruefungen bestanden."**, Rueckgabewert 0.
|
|
Die dabei ermittelte Container-Adresse war `172.19.0.2` — eine Container-Adresse
|
|
ist veraenderlich und ist bei der Ausfuehrung NEU zu ermitteln, nicht von hier
|
|
abzuschreiben.
|
|
- `apps/api/package.json` fuehrt `test` (`vitest run`) und `type-check`
|
|
(`tsc --noEmit`) — die beiden Befehle, auf denen jede Pruefung dieses Plans
|
|
aufsetzt. Es gibt keinen `lint`-Befehl in diesem Paket.
|
|
|
|
**Befund A — die 17 sind echt, und sie verteilen sich auf drei Dateien.**
|
|
`grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/user | grep -v spec` liefert 17
|
|
Treffer: 6 in `user.service.ts` (alle auf `user`), 7 in `user.controller.ts` (alle
|
|
auf `user`), 4 in `admin-seed.service.ts` (2 auf `user`, 2 auf `tenant`). Die
|
|
breitere Suche nach `prisma\.|PrismaService|\$transaction|forTenant|withTenantTransaction|tx\.`
|
|
ueber denselben Ordner findet keine weitere Datei, die die Datenbank erreicht:
|
|
`user.module.ts` und `dto/` beruehren sie nicht. Zweiter Bereich in Folge, dessen
|
|
Kopfzahl beim Hineinsehen nicht kleiner wird.
|
|
|
|
**Befund B — dieser Bereich hat KEINE Transaktion.**
|
|
`grep -rn '\$transaction(' apps/api/src/user --include=*.ts | grep -v spec` liefert
|
|
null Treffer. Der Kopfkommentar von `prisma-tenant.extension.ts` verlangt woertlich,
|
|
vor jedem NEUEN mandantengebundenen Fall mit eigener Transaktion erneut zu messen —
|
|
fuer diesen Bereich faellt kein solcher Fall an, `withTenantTransaction()` wird hier
|
|
nicht gebraucht und darf nicht eingefuehrt werden. Die blinde Stelle des
|
|
`groups`-Erkenners (Modellzugriffe ueber den Transaktionsparameter) kann hier
|
|
strukturell nicht auftreten. Bei der Ausfuehrung neu messen; faellt es anders aus,
|
|
gilt die Messung.
|
|
|
|
**Befund C — die Testlage, und sie ist eine MISCHUNG aus zwei der drei bekannten
|
|
Formen.** Der Auftrag verlangt, das VOR jeder Umstellung zu pruefen. Gemessen:
|
|
|
|
- `user.service.spec.ts` existiert, prueft aber ausschliesslich die
|
|
Standardgruppen-Anbindung von `create`. Der Prisma-Ersatz ist ein nacktes Objekt
|
|
mit genau einem Eintrag (`user.create`), es gibt KEINE Attrappe fuer
|
|
`forTenant` — die zweite bekannte Form (`groups`/`tenders`): nach der Umstellung
|
|
waeren diese Tests rot, aber aus dem falschen Grund (das Hilfsmittel bekaeme
|
|
einen unbrauchbaren Klienten), nicht wegen einer vergessenen Bindung.
|
|
- `admin-seed.service.spec.ts` existiert, prueft die Reihenfolge beim Start ueber
|
|
ein gemeinsames Aufruf-Protokoll, und hat ebenfalls KEINE `forTenant`-Attrappe —
|
|
dieselbe zweite Form.
|
|
- `user.controller.spec.ts` existiert NICHT. Die Steuerungsschicht dieses Bereichs
|
|
ist die dritte Form (`dkv`): sie kann heute auf gar keinen Fehler rot werden — und
|
|
in ihr liegen sieben der siebzehn Zugriffe UND die gesamte Rollenlogik, die
|
|
entscheidet, wer wessen Benutzer sehen darf.
|
|
|
|
Das Muster fuer den Zwei-Klienten-Nachweis liegt vor:
|
|
`apps/api/src/groups/groups.service.spec.ts` mockt die Erweiterung und liefert
|
|
`__makeBoundClient(tenantId)` als protokollierenden Wrapper um DIESELBEN Maps.
|
|
|
|
**Befund D — die Nachschlagemethode nach Benutzername hat KEINEN Aufrufer mehr, und
|
|
ihr Kommentar behauptet das Gegenteil.** `findByUsername` in `user.service.ts`
|
|
traegt einen Kommentar, der sagt, sie benutze bewusst den ungebundenen Klienten,
|
|
"because login must work across all tenants". Gemessen:
|
|
`grep -rn "findByUsername" apps/api/src packages` liefert **genau einen Treffer, die
|
|
Definition selbst**. Etappe 1 (260909-eor) hat den Anmeldeweg auf die drei schmalen
|
|
SECURITY-DEFINER-Funktionen umgezogen; `auth.service.ts` sucht seither ueber
|
|
`auth_lookup_user_by_username` und nicht mehr ueber diese Methode. Der Kommentar
|
|
beschreibt damit einen Zustand, den es seit Etappe 1 nicht mehr gibt.
|
|
|
|
Das ist gefaehrlicher, als es aussieht: der Satz liest sich wie eine Freigabe fuer
|
|
uebergreifende Benutzung. Wer ihn glaubt und die Methode neu verwendet, benutzt nach
|
|
dem Scharfschalten eine Abfrage, die fuer JEDEN Benutzer `null` liefert.
|
|
|
|
**Die Entscheidung dazu, ausgeschrieben statt still getroffen.** Drei Formen wurden
|
|
erwogen:
|
|
|
|
- (a) Binden — **nicht moeglich und nicht richtig.** Die Methode bekommt keinen
|
|
Mandanten und hat keinen Aufrufer, aus dem einer kaeme. Und selbst mit einem waere
|
|
Binden falsch: `username` ist plattformweit eindeutig, eine gebundene Suche saehe
|
|
einen fremden Halter nicht und meldete "frei". Exakt der `resolveEmailForWrite`-Fall
|
|
auf dem anderen der beiden eindeutigen Schluessel.
|
|
- (b) Loeschen, weil es toter Code ist — **abgelehnt.** Die Methode zu entfernen
|
|
entfernt zugleich den Ort, an dem die Begruendung stehen kann, warum hier NICHT
|
|
gebunden wird. Der naechste, der eine Suche nach Benutzername braucht, schriebe sie
|
|
neu — ohne die Begruendung. Ausserdem waere das eine Funktionsaenderung an der
|
|
oeffentlichen Flaeche des Dienstes, kein Bindungsumbau.
|
|
- (c) Behalten, ungebunden lassen, den Kommentar richtigstellen — **gewaehlt**, nach
|
|
dem unmittelbaren Praezedenzfall `resolveEmailForWrite` (Bereich `ldap`,
|
|
260909-ipc, T-IPC-04).
|
|
|
|
Bei der Ausfuehrung ist die Aufruferzahl NEU zu messen. Findet sich wider Erwarten
|
|
ein Aufrufer, gilt die Messung und die Entscheidung ist neu zu treffen und im
|
|
SUMMARY auszuschreiben.
|
|
|
|
**Befund E — die Adjazenz zu `auth`, und wo die Grenze verlaeuft.**
|
|
`auth.service.ts` ist aus demselben Grund `gemischt` und ist NICHT Gegenstand dieses
|
|
Auftrags. Gemessen, damit die Grenze nicht aus Erinnerung gezogen wird: die Datei hat
|
|
drei bereits ueber `forTenant()` gebundene Schreibzugriffe (Anmeldezeitstempel,
|
|
Kennwortwechsel nach Zuruecksetzen) und fuenf noch ungebundene Zugriffe auf `user`,
|
|
die zu `getMe`, `changePassword` und `adminResetPassword` gehoeren. Die drei
|
|
Anmelde-/Zuruecksetz-Nachschlagewege laufen ueber `$queryRaw` auf die drei
|
|
SECURITY-DEFINER-Funktionen. Dieser Plan fasst `auth.service.ts` an KEINER Stelle an
|
|
und aendert an den drei Funktionen und ihren Rechten nichts. Die einzige Beruehrung
|
|
mit dem Anmeldeweg ist der richtiggestellte Kommentar aus Befund D — er beschreibt
|
|
die Grenze, er verschiebt sie nicht.
|
|
|
|
**Befund F — die uebergreifende Sicht des Plattform-Administrators ist echt, sie ist
|
|
gewollt, und sie ist nach dem Scharfschalten in JEDER heute denkbaren Fassung
|
|
kaputt.** In `user.controller.ts` verzweigt `findAll` nach Rolle: fuer
|
|
`SUPER_ADMIN` ein `findMany` OHNE Mandantenbedingung ueber alle Benutzer der
|
|
Plattform, fuer `ADMIN` dasselbe `findMany` mit `where: { tenantId }`. Die drei
|
|
Wege ueber die Kennung (`findOne`, `update`, `remove`) haben dieselbe Zweiteilung,
|
|
nur an anderer Stelle: sie laden erst ueber `userService.findById(id)` und werfen
|
|
danach `ForbiddenException`, WENN der Aufrufer kein `SUPER_ADMIN` ist und der
|
|
Zielbenutzer einem anderen Mandanten gehoert. Fuer `SUPER_ADMIN` faellt die Pruefung
|
|
weg — die uebergreifende Verwaltung ist ausdrueckliche Absicht.
|
|
|
|
Nach dem Scharfschalten gilt: bliebe der `SUPER_ADMIN`-Zweig ungebunden, liefert er
|
|
NULL Zeilen (die Policy vergleicht gegen einen nicht gesetzten Wert). Wuerde man ihn
|
|
an den Mandanten des Aufrufers binden, saehe der Plattform-Administrator nur noch
|
|
seinen eigenen Mandanten — eine stille Funktionsminderung. Beides ist falsch.
|
|
|
|
Die dritte Form, die richtig ist und die dieses Projekt bereits benutzt: eine
|
|
Schleife ueber alle Mandanten mit je EINEM gebundenen Lesezugriff im Rumpf. Genau
|
|
diese Form steht in `admin-seed.service.ts` als `ensureDefaultGroupsForAllTenants()`
|
|
— sie liest die Mandanten uebergreifend und ruft je Mandant einen bereits gebundenen
|
|
Dienst. Dass der Schleifentreiber ungebunden lesen darf, haengt an einer Tatsache,
|
|
die nachgesehen und nicht geglaubt wurde: `Tenant` traegt in KEINER Migration
|
|
`ENABLE ROW LEVEL SECURITY` (geprueft ueber alle fuenf Migrationen mit
|
|
Zeilenschutz), und `20260909130000_rls_app_role` erteilt `tessera_app` Leserechte auf
|
|
alle Tabellen des Schemas. Diese Eigenschaft traegt zwei Wege dieses Bereichs und
|
|
gehoert deshalb gemessen (Aufgabe 1), nicht unterstellt.
|
|
|
|
**Befund G — die Selbstbedienungswege der Steuerungsschicht sind bindbar, alle
|
|
fuenf.** Vier Endpunkte (Bild hochladen, Bild loeschen, Akzentfarbe setzen, Bild
|
|
ausliefern) greifen ueber fuenf Zugriffe auf `user` zu, jeweils ueber
|
|
`currentUser.id`. Der Sitzungsnachweis traegt `tenantId` (die Pruefstrategie liefert
|
|
`{ id, username, role, tenantId }`), der Mandant ist also an jeder dieser Stellen
|
|
bekannt. Sie binden vollstaendig, ohne Verhaltensaenderung: der angemeldete Benutzer
|
|
liegt per Definition im Mandanten seiner eigenen Sitzung.
|
|
|
|
**Befund H — eine heute wirksame Luecke, gefunden beim Aufschlagen genau dieser
|
|
Stelle.** `user.controller.ts` prueft im Loeschweg `if (user.id === currentUser.sub)`
|
|
und will damit verhindern, dass ein Administrator sein eigenes Konto loescht. Der
|
|
Sitzungsnachweis traegt aber gar kein Feld dieses Namens: die Pruefstrategie bildet
|
|
`payload.sub` auf `id` ab und gibt genau vier Felder zurueck. Gemessen ueber den
|
|
gesamten Quelltext: es gibt genau zwei Vorkommen dieses Feldnamens, die Zuweisung in
|
|
der Strategie und diesen Vergleich; nirgends wird das Anfrageobjekt nachtraeglich mit
|
|
einem solchen Feld bestueckt. Der Vergleich ist damit immer falsch, der Riegel greift
|
|
nie.
|
|
|
|
Die Folge ist keine Mandantenfrage, sondern eine Verfuegbarkeitsfrage mit
|
|
Rechtebezug: der einzige Administrator eines Mandanten kann sich selbst loeschen und
|
|
den Mandanten ohne Verwaltung zuruecklassen. Die Reparatur ist ein Feldname, sie
|
|
liegt in genau der Datei, die dieser Auftrag ohnehin umbaut, und sie ist dieselbe
|
|
Klasse von Fund wie der `ldap`-Fund (Aufloesung ueber die Kennung allein) und der
|
|
`dkv`-Fund (uebergebener Mandant wird verworfen): eine Pruefung, die es gibt und die
|
|
nicht wirkt. Sie gehoert hierher.
|
|
|
|
**Befund I — die Erstanlage beim Start, und warum sie die schwerste Auspraegung der
|
|
umgekehrten Fehlerrichtung im ganzen Vorhaben ist.** `admin-seed.service.ts` laeuft
|
|
beim Start in zwei Schritten. Der erste, `seedAdmin()`, ist bewusst NICHT gekapselt:
|
|
der Kopfkommentar der Datei sagt ausdruecklich, ein Fehlschlag solle den Start
|
|
weiterhin laut scheitern lassen. Seine Reihenfolge:
|
|
|
|
1. Es liest die drei Umgebungswerte; fehlt einer, kehrt es zurueck.
|
|
2. Es sucht den Administrator ueber `user.findUnique({ where: { username } })` —
|
|
eine Suche auf dem plattformweit eindeutigen Schluessel, an einer Stelle, an der
|
|
noch KEIN Mandant existiert.
|
|
3. Findet es ihn, kehrt es zurueck.
|
|
4. Sonst legt es den Standard-Mandanten an bzw. holt ihn, stellt die Standardgruppe
|
|
sicher und legt den Administrator an.
|
|
|
|
Nach dem Scharfschalten liefert Schritt 2 `null` — nicht, weil der Administrator
|
|
fehlt, sondern weil ohne gesetzten Mandantenkontext KEINE Zeile der Benutzertabelle
|
|
sichtbar ist. Schritt 3 entfaellt. Schritt 4 laeuft und legt an. Die
|
|
Mandanten-Anlage geht durch, weil `Tenant` keinen Zeilenschutz traegt. Die
|
|
Benutzer-Anlage kollidiert auf `username`. Und weil `seedAdmin()` nicht gekapselt
|
|
ist, **startet die Anwendung nicht mehr**.
|
|
|
|
Das ist die vollstaendige, im Auftrag beschriebene Kette in Reinform: Leere wird als
|
|
Abwesenheit gelesen, die natuerliche Folgehandlung ist Anlegen, und das Anlegen
|
|
scheitert an einem plattformweit eindeutigen Schluessel. Sie ist laut — aber sie
|
|
blockiert den Start, und sie trifft jede bestehende Installation, die die
|
|
Administrator-Umgebungswerte gesetzt hat.
|
|
|
|
**Befund J — die Klassifikationszeile fuer die Erstanlage ist falsch, und die
|
|
Korrektur ist gleichzeitig eine Reparatur.** Das Dokument fuehrt
|
|
`(admin-seed.service.ts, user)` als `bewusst-uebergreifend` mit der Begruendung, es
|
|
gebe zum Zeitpunkt der Anlage strukturell keinen Mandanten, an den gebunden werden
|
|
koennte. Am Code nachgesehen stimmt das fuer den einen der beiden Zugriffe (die
|
|
Suche in Schritt 2) und ist fuer den anderen FALSCH: der Mandant wird eine Anweisung
|
|
vorher angelegt, seine Kennung liegt in einer lokalen Variablen, und der Benutzer
|
|
wird mit genau dieser Kennung angelegt.
|
|
|
|
Die Korrektur ist nicht kosmetisch. Nach dem Scharfschalten wird ein Einfuegen ohne
|
|
gesetzten Mandantenkontext von der Policy ABGEWIESEN (die Policy traegt keine eigene
|
|
`WITH CHECK`-Klausel; was PostgreSQL daraus fuer ein Einfuegen ableitet, ist eine
|
|
Eigenschaft der Datenbank und wird in Aufgabe 1 gemessen). Bliebe dieser eine
|
|
Zugriff ungebunden, koennte eine FRISCHE Installation ihren allerersten
|
|
Administrator ueberhaupt nicht anlegen. Das Paar endet damit auf Klasse `beides` und
|
|
Stand `gemischt`, nicht auf `bewusst-uebergreifend`.
|
|
|
|
**Befund K — die Standardgruppen-Reparatur ist der fuenfte Fall der
|
|
Hintergrunddienst-Falle und der erste, der auf beiden Haelften bereits richtig ist.**
|
|
`ensureDefaultGroupsForAllTenants()` liest alle Mandanten und ruft je Mandant
|
|
`groupsService.ensureDefaultGroup(tenant.id)` — dieser Rumpf ist seit 260909-jts
|
|
gebunden. Die Schleife liest also zu Recht uebergreifend, waehrend ihr Rumpf schon
|
|
korrekt ist. Der bisherige Abschnitt des Klassifikationsdokuments kennt vier Faelle,
|
|
alle drei `beides`-Faelle plus die entartete Form aus `dkv`. Dieser fuenfte ist der
|
|
Gegenfall zu allen vieren und gehoert genau deshalb dazu: er zeigt, wie die Form
|
|
aussieht, wenn sie stimmt. Der Treiber `tenant.findMany` bleibt ungebunden und ist
|
|
korrekt so.
|
|
|
|
Eine Einschraenkung, die dabei NICHT verschwiegen werden darf und in die
|
|
Kritikschrift gehoert: der aeussere `try/catch` um diese Reparatur verschluckt
|
|
jeden Fehler des Treibers in eine Protokollzeile. Laeuft die Mandantenliste nach dem
|
|
Scharfschalten aus irgendeinem Grund leer, entsteht keine Fehlermeldung, sondern gar
|
|
keine Ausgabe — die Reparatur meldet nur, wenn sie etwas GETAN hat.
|
|
|
|
**Befund L — welcher Code Leere als Abwesenheit deutet (Vorarbeit fuer Aufgabe 1,
|
|
dort auszuformulieren und zu ergaenzen, nicht abzuschreiben).**
|
|
|
|
1. Die Erstanlage-Pruefung beim Start (Befund I) — die schwerste. `null` heisst
|
|
"der Administrator existiert nicht", die Folgehandlung ist Anlegen, das Anlegen
|
|
kollidiert plattformweit, und der Start bricht ab.
|
|
2. Die Benutzerliste im ADMIN-Zweig — eine leere Liste heisst "dieser Mandant hat
|
|
keine Benutzer". Ein Administrator, der seine Kollegen nicht mehr sieht, legt sie
|
|
an. Jede dieser Anlagen kollidiert auf `username`. Die Oberflaeche zeigt dabei
|
|
nichts Auffaelliges: eine leere Benutzerliste ist auf einer frischen Installation
|
|
der Normalzustand.
|
|
3. Die Benutzerliste im SUPER_ADMIN-Zweig (Befund F) — dieselbe Leere, eine Ebene
|
|
hoeher: die Plattformverwaltung sieht eine Installation ohne jeden Benutzer.
|
|
4. Eine gebundene Suche nach Benutzername oder Adresse (Befund D, und der
|
|
Praezedenzfall `resolveEmailForWrite`) — meldet "frei" fuer einen Namen, den es
|
|
gibt. Die Kollisionspruefung wird zur Kollisionserzeugung.
|
|
5. `ldap.service.ts`, `upsertMappedUser` — bereits gebunden, seit 260909-ipc, in
|
|
einem ANDEREN Bereich, und deshalb hier nur zu benennen und nicht anzufassen: die
|
|
Identitaetssuche entscheidet ueber Anlegen-oder-Aktualisieren und laeuft in
|
|
dieselbe Kette. Sie ist der Beleg, dass die Kette nicht erst nach dem
|
|
Scharfschalten existiert — die Suchbedingung traegt bereits heute den Mandanten,
|
|
ein fremder Halter ist also bereits heute unsichtbar. Die Uebersetzung der
|
|
Eindeutigkeitsverletzung gehoert deshalb an den Anlegepunkt in `user.service.ts`,
|
|
der in diesem Plan ohnehin angefasst wird — dort und nicht in `ldap`.
|
|
6. Die Selbstbedienungswege fuer Bild und Akzentfarbe (Befund G) — ein leerer
|
|
Lesezugriff heisst "kein Bild hinterlegt". Harmlos in der Wirkung, aber es gehoert
|
|
in die Tabelle, damit sie vollstaendig ist.
|
|
|
|
Gegenrichtung, ebenfalls nachgesehen und in die Kritikschrift gehoerend: ein
|
|
gebundenes Aendern oder Loeschen ueber die Kennung allein trifft eine fremde Zeile
|
|
NICHT still, sondern wirft (Prisma meldet einen nicht gefundenen Datensatz). Ein
|
|
gebundenes Einfuegen mit fremder Mandantenkennung wird abgewiesen. Die drei
|
|
Wege ueber die Kennung in der Steuerungsschicht werfen bei Leere ebenfalls laut. Das
|
|
sind die lauten Stellen, und der Abschnitt darf nicht nur aus Alarm bestehen.
|
|
|
|
**Befund M — was in diesem Bereich NICHT umzustellen ist.** Die beiden Zugriffe auf
|
|
`tenant` (Anlage des Standard-Mandanten, Treiber der Reparaturschleife) bleiben
|
|
ungebunden: `Tenant` traegt keine Mandantenspalte und keinen Zeilenschutz. Die Suche
|
|
nach Benutzername in `user.service.ts` (Befund D) und die Erstanlage-Pruefung beim
|
|
Start (Befund I) bleiben ungebunden, jede mit eigener Begruendung am Ort. Alles
|
|
Uebrige bindet.
|
|
|
|
**Befund N — ein NEUES Fundstellenpaar entsteht, und die Inventarpruefung wuerde
|
|
sonst darueber stolpern.** Die Umstellung der Plattform-Administratorsicht (Befund F)
|
|
bringt einen Lesezugriff auf `tenant` in `user.service.ts` — ein Paar
|
|
`(user.service.ts, tenant)`, das es heute nicht gibt. `rls-access-inventory.spec.ts`
|
|
prueft Vollstaendigkeit in BEIDE Richtungen und wird rot, solange das Paar keine
|
|
Zeile im Dokument hat. Die Klasse ist `keine-mandantengebundene-tabelle`, der Stand
|
|
`ungebunden`. Damit steigt die Gesamtzahl der Paare, und die Klassen-Verteilungstabelle
|
|
samt ihrer Summe muss mitziehen — dieselbe handgepflegte Stelle, die im
|
|
`dkv`-Durchlauf uebersehen wurde. Die genauen Zahlen sind bei der Ausfuehrung aus der
|
|
Ausgabe der Pruefung zu nehmen, nicht aus diesem Absatz.
|
|
|
|
**Befund O — der Zustand des Messwerkzeugs, und warum dieser Bereich es anders
|
|
benutzt als die vier davor.** `rls-scratch-check.mjs` legt seine Wegwerf-Datenbank
|
|
EINMAL an und laesst danach acht Abschnitte darauf laufen. Der zweite Abschnitt
|
|
(Anmeldeweg) legt bereits eine Tabelle `"User"` an — mit `username` eindeutig,
|
|
`email` eindeutig, Zeilenschutz eingeschaltet und erzwungen, und einer Policy, die
|
|
dort von Hand getippt ist statt aus der Migration geschnitten. Ein neuer Abschnitt
|
|
darf diese Tabelle also NICHT ein zweites Mal anlegen; er baut auf ihr auf. Die
|
|
beiden Eindeutigkeitsbedingungen, die dieser Bereich braucht, sind darin bereits
|
|
genau richtig abgebildet.
|
|
|
|
Daraus folgen zwei Dinge fuer Aufgabe 1: der neue Abschnitt muss die von Hand
|
|
getippte Policy gegen die aus `20260618112133_rls_policies` geschnittene halten,
|
|
statt eine dritte Fassung zu erzeugen — die Werkzeuge `readRlsPoliciesMigrationSql()`
|
|
und `extractPolicySql()` liegen vor und werden bereits vom `ldap`-Abschnitt benutzt.
|
|
Und eine Tabelle `Tenant` gibt es in der Wegwerf-Datenbank noch nicht; sie ist neu
|
|
anzulegen, bewusst OHNE Zeilenschutz, weil genau das die zu messende Eigenschaft ist.
|
|
Zu beachten: der Anmeldeweg-Abschnitt haengt die Spalte `role` nachtraeglich an einen
|
|
Aufzaehlungstyp um, neue Zeilen muessen also einen gueltigen Wert dieses Typs tragen.
|
|
|
|
</planning_time_findings>
|
|
|
|
<tasks>
|
|
|
|
<task type="tracer">
|
|
<name>Aufgabe 1: Die Kette messen — von der unsichtbaren Zeile ueber das falsche "frei" bis zum harten Eindeutigkeitsfehler — und die Kritikschrift fuer user schreiben</name>
|
|
<precondition>Der Container `tessera-ctl-db-1` laeuft; seine Adresse per `docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}'` NEU ermitteln (eine Container-Adresse ist veraenderlich und darf nicht aus diesem Plan abgeschrieben werden).</precondition>
|
|
<files>apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
|
<action>
|
|
Zuerst messen, dann die Kritik aus der Messung schreiben — nicht umgekehrt. Kein
|
|
Dienst- und kein Steuerungscode in dieser Aufgabe.
|
|
|
|
TEIL 1, `apps/api/scripts/rls-scratch-check.mjs`: einen siebten Abschnitt
|
|
`runUserAreaChecks(adminUrl, scratchRoleUrl, results)` nach dem Vorbild des
|
|
vorhandenen `runDkvAreaChecks` ergaenzen und in `main()` NACH diesem, aber VOR
|
|
`runTransactionShapeMeasurement` aufrufen — die Transaktionsmessung und die
|
|
Lastprobe setzen auf den vom `groups`-Abschnitt angelegten Tabellen auf und duerfen
|
|
ihre Voraussetzung nicht verlieren; das ist beim Einhaengen zu pruefen, nicht
|
|
anzunehmen.
|
|
|
|
Dieser Abschnitt legt die Benutzertabelle NICHT neu an. Sie existiert bereits, vom
|
|
Abschnitt des Anmeldewegs, samt eingeschaltetem und erzwungenem Zeilenschutz, samt
|
|
beider Eindeutigkeitsbedingungen und samt zweier Testzeilen in zwei Mandanten
|
|
(Befund O). Der neue Abschnitt setzt darauf auf und fuegt hinzu, was ihm fehlt.
|
|
|
|
Zuerst die Policy-Herkunft klaeren, bevor irgendetwas gemessen wird. Die dort von
|
|
Hand getippte Policy wird gegen die aus der Migration `20260618112133_rls_policies`
|
|
geschnittene gehalten; `readRlsPoliciesMigrationSql()` und `extractPolicySql()` sind
|
|
vorhanden und werden bereits vom `ldap`-Abschnitt benutzt. Verglichen wird nach
|
|
Normalisierung von Leerraum und abschliessendem Semikolon, damit der Vergleich an
|
|
Formatierung nicht scheitert. Findet die Extraktion die Policy nicht oder weicht sie
|
|
inhaltlich ab, meldet der Abschnitt eine FEHLGESCHLAGENE Pruefung
|
|
`user-policy-aus-migration-wortgleich` und bricht ab — das Werkzeug darf nicht still
|
|
mit einer geratenen oder abweichenden Policy weitermessen. Faellt der Vergleich
|
|
negativ aus, gilt die Messung: dann wird die im Werkzeug getippte Fassung durch die
|
|
geschnittene ERSETZT und die Abweichung in der Kritikschrift ausgeschrieben, bevor
|
|
Aufgabe 2 beginnt.
|
|
|
|
Danach ergaenzt der Abschnitt eine Tabelle `Tenant` mit Kennung und Kuerzel,
|
|
ausdruecklich OHNE Zeilenschutz — dass diese Tabelle ungeschuetzt ist, ist die zu
|
|
messende Eigenschaft und nicht Beiwerk. Rechtevergabe an die Wegwerf-Rolle nicht
|
|
vergessen. Zwei Zeilen fuer die beiden Mandanten, deren Kennungen exakt den in der
|
|
Benutzertabelle verwendeten Mandantenkennungen entsprechen. Zusaetzliche
|
|
Benutzerzeilen nach Bedarf, mindestens je Mandant eine weitere; die Spalte fuer die
|
|
Rolle traegt einen Aufzaehlungstyp, ein gueltiger Wert ist also Pflicht (Befund O).
|
|
|
|
Gemessen wird unter der Rolle ohne BYPASSRLS ueber das vorhandene
|
|
`forTenantQuery`-Hilfsmittel, mit diesen Kennungen — jede Kennung genau so
|
|
geschrieben, weil die Pruefung dieser Aufgabe sie einzeln in der Ausgabe sucht:
|
|
|
|
- `user-policy-aus-migration-wortgleich` — siehe oben.
|
|
- `user-gebunden-nur-eigener-mandant` — der gebundene SELECT unter dem ersten
|
|
Mandanten liefert dessen Zeilen und keine des zweiten.
|
|
- `user-ungebunden-null-zeilen` — DERSELBE SELECT ohne vorher gesetzten Kontext
|
|
liefert null Zeilen. Die Belegzeile, die den ganzen Abschnitt der Kritikschrift
|
|
traegt; sie muss an der echten, ausgelieferten Policy haengen.
|
|
- `user-ungebundene-suche-nach-benutzername-liefert-keine-zeile` — die Form, die die
|
|
Erstanlage-Pruefung beim Start heute benutzt: ein ungebundenes SELECT mit
|
|
Gleichheitsbedingung auf einen Benutzernamen, den es GIBT. Bestanden, wenn keine
|
|
Zeile zurueckkommt. Der Meldetext sagt ausdruecklich, was das bedeutet: der
|
|
aufrufende Code liest daraus "diesen Benutzer gibt es nicht" und legt an.
|
|
- `user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile` — dieselbe
|
|
Suche, aber gebunden an den ersten Mandanten, nach einem Benutzernamen des
|
|
zweiten. Bestanden, wenn keine Zeile zurueckkommt. Das ist der Moment, in dem eine
|
|
Kollisionspruefung faelschlich "frei" meldet.
|
|
- `user-eindeutigkeit-greift-trotz-unsichtbarkeit` — **die wichtigste Messung dieser
|
|
Aufgabe und der Schluss der Kette.** Unmittelbar nach der vorherigen Pruefung: ein
|
|
gebundenes Einfuegen unter dem ersten Mandanten mit genau diesem, angeblich freien
|
|
Benutzernamen. Bestanden, wenn es ABGEWIESEN wird UND die Ablehnung nachweislich
|
|
eine Verletzung der Eindeutigkeit ist (Fehlerklasse 23505) und NICHT eine Ablehnung
|
|
durch den Zeilenschutz (Fehlerklasse 42501). Diese Unterscheidung ist der Kern:
|
|
nur die erste ist die im Auftrag beschriebene Kette, die zweite waere eine ganz
|
|
andere Geschichte. Die Fehlerklasse ist aus dem Fehlerobjekt zu lesen und im
|
|
Meldetext auszugeben, damit die Zeile fuer sich selbst spricht. Faellt sie anders
|
|
aus als erwartet, gilt die Messung und die Abweichung wird ausgeschrieben, bevor
|
|
Aufgabe 2 beginnt.
|
|
- `user-gebundenes-einfuegen-fremder-mandant-abgelehnt` — ein gebundenes Einfuegen
|
|
unter dem ersten Mandanten, das die Mandantenkennung des zweiten traegt, mit einem
|
|
sonst freien Benutzernamen. Die Abweisung ist das bestandene Ergebnis. Diese
|
|
Pruefung existiert, weil die ausgelieferte Policy KEINE eigene WITH-CHECK-Klausel
|
|
traegt und was PostgreSQL daraus fuer ein Einfuegen ableitet eine Eigenschaft der
|
|
Datenbank ist, keine des Policy-Textes.
|
|
- `user-ungebundenes-einfuegen-abgelehnt` — ein Einfuegen ganz ohne gesetzten
|
|
Kontext, mit einem freien Benutzernamen und einer gueltigen Mandantenkennung. Die
|
|
Abweisung ist das bestandene Ergebnis. Diese Zeile traegt die Entscheidung aus
|
|
Befund J: bliebe die Erstanlage des Administrators ungebunden, koennte eine frische
|
|
Installation ihren ersten Administrator nach dem Scharfschalten nicht anlegen.
|
|
- `user-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen` — ein gebundenes
|
|
UPDATE unter dem ersten Mandanten, das eine Zeile des zweiten allein ueber deren
|
|
Kennung anspricht. Bestanden, wenn null Zeilen betroffen sind. Der Meldetext nennt
|
|
die Folge fuer Aufgabe 2 und 3: die vorgeschalteten Besitz- und Rollenpruefungen
|
|
bleiben erhalten und werden nicht durch die Datenbank ersetzt.
|
|
- `user-gebundenes-loeschen-ueber-kennung-allein-trifft-null-zeilen` — dasselbe fuer
|
|
ein DELETE.
|
|
- `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar` — ohne jeden gesetzten Kontext
|
|
liefert ein SELECT auf die Mandantentabelle beide Zeilen. Das ist die Eigenschaft,
|
|
auf der sowohl die umgestellte Plattform-Administratorsicht als auch die
|
|
Standardgruppen-Reparatur beim Start stehen (Befund F, Befund K). Zusaetzlich ist zu
|
|
pruefen und im Meldetext festzuhalten, dass fuer diese Tabelle im Systemkatalog
|
|
tatsaechlich kein Zeilenschutz eingeschaltet ist — die Aussage haengt sonst allein
|
|
daran, dass der Abschnitt selbst keinen eingeschaltet hat.
|
|
- `user-fan-out-je-mandant-gebunden-liefert-alle-zeilen` — die Nachbildung der
|
|
umgestellten Plattform-Administratorsicht: erst die Mandanten ungebunden lesen,
|
|
dann je Mandant EIN gebundener SELECT, dann die Ergebnisse vereinigen. Bestanden,
|
|
wenn die Vereinigung genau der Gesamtmenge der Benutzerzeilen entspricht. Das ist
|
|
der Beleg, dass die in Aufgabe 3 gewaehlte Form die heutige Sicht erhaelt statt sie
|
|
zu mindern.
|
|
|
|
TEIL 2, Beleg statt Behauptung fuer Befund B:
|
|
`grep -rn '\$transaction(' apps/api/src/user --include=*.ts | grep -v spec`
|
|
ausfuehren und das Ergebnis (Trefferzahl) in der Kritikschrift festhalten, samt der
|
|
Feststellung, dass der im Kopf von `prisma-tenant.extension.ts` verlangte erneute
|
|
Test fuer diesen Bereich damit beantwortet ist: kein neuer Fall,
|
|
`withTenantTransaction()` wird nicht gebraucht und nicht eingefuehrt. Faellt das
|
|
Ergebnis anders aus als in Befund B beschrieben, gilt die MESSUNG, und die Abweichung
|
|
wird ausgeschrieben, bevor Aufgabe 2 beginnt.
|
|
|
|
TEIL 3, Beleg statt Behauptung fuer Befund D:
|
|
`grep -rn "findByUsername" apps/api/src packages` ausfuehren und die Trefferzahl in
|
|
der Kritikschrift festhalten. Ergibt sich mehr als die Definition selbst, gilt die
|
|
Messung und die Entscheidung aus Befund D ist neu zu treffen und auszuschreiben,
|
|
bevor Aufgabe 2 beginnt.
|
|
|
|
Das Werkzeug raeumt weiterhin ausschliesslich seine fest verdrahtete
|
|
Wegwerf-Datenbank ab und bekommt keine steuerbaren Namen (T-EOR-07 bleibt gueltig).
|
|
Kein bestehender Abschnitt wird inhaltlich veraendert; die 41 bisherigen Pruefungen
|
|
muessen unveraendert weiterlaufen. Einzige zulaessige Beruehrung des
|
|
Anmeldeweg-Abschnitts ist der Austausch der von Hand getippten Policy gegen die
|
|
geschnittene, und auch der nur, falls der Vergleich eine Abweichung ergibt.
|
|
|
|
TEIL 4, `docs/mandantentrennung-etappe2-fehlerrichtung.md` um einen Abschnitt
|
|
`## Bereich user` ERWEITERN, nicht ein zweites Dokument anlegen. Die Leitfrage aus
|
|
Abschnitt (a) gilt unveraendert weiter und wird nicht wiederholt; der neue Abschnitt
|
|
verweist darauf und haelt im Kopf fest, dass er den Bereich `user` zum Zeitpunkt
|
|
seiner Umstellung beschreibt (Quick-Task 260910-das). In ganzen Saetzen auf Deutsch,
|
|
mit derselben Gliederung wie der `dkv`-Abschnitt:
|
|
|
|
(u1) Die Messung — die TATSAECHLICH beobachtete Ausgabe des Laufs, hineinkopiert,
|
|
nicht nacherzaehlt, mit Datum und der bei der Ausfuehrung ermittelten Adresse. Die
|
|
drei tragenden Zeilen ausdruecklich benennen und auseinanderhalten: die Belegzeile
|
|
zur Unsichtbarkeit, die Zeile zum falschen "frei", und die Zeile zum harten
|
|
Eindeutigkeitsfehler samt der gemessenen Fehlerklasse. Die Ergebnisse aus Teil 2 und
|
|
Teil 3 gehoeren ebenfalls hierher.
|
|
|
|
(u2) Signaltabelle je umgestelltem Pfad: Pfad, Verhalten bei zu wenig Ergebnis,
|
|
konkretes Signal mit Ort. Es muessen alle in Aufgabe 2 und 3 umgestellten Pfade
|
|
vorkommen, ausserdem die drei bewusst ungebunden bleibenden (Suche nach
|
|
Benutzername, Erstanlage-Pruefung beim Start, beide Zugriffe auf die
|
|
Mandantentabelle) und die umgestellte Plattform-Administratorsicht.
|
|
|
|
(u3) Welcher Code Leere als Abwesenheit deutet — der Kern dieses Abschnitts. Die
|
|
sechs Stellen aus Befund L namentlich benennen, getrennt nach startverhindernd /
|
|
kollisionserzeugend / lautlos / harmlos. Die Kette bekommt eigenen Raum und wird
|
|
ausgeschrieben: unsichtbare Zeile, Leere gelesen als Abwesenheit, Anlegen als
|
|
natuerliche Folgehandlung, harter Eindeutigkeitsfehler auf einem plattformweit
|
|
eindeutigen Schluessel. Die Erstanlage beim Start ist als die startverhindernde
|
|
Auspraegung zu benennen, mit dem Hinweis, dass der Erstanlage-Schritt bewusst nicht
|
|
gekapselt ist und das hier von einer Absicht zu einer Startsperre wird. Die
|
|
Identitaetssuche des AD-Abgleichs (Befund L, Punkt 5) ist ausdruecklich als Beleg
|
|
dafuer zu benennen, dass die Kette bereits HEUTE existiert und nicht erst nach dem
|
|
Scharfschalten entsteht — sie liegt in einem anderen Bereich und wird hier nicht
|
|
angefasst, aber die Uebersetzung der Eindeutigkeitsverletzung am gemeinsamen
|
|
Anlegepunkt entschaerft sie mit. Ausserdem ist auszufuehren, was ein GEBUNDENER
|
|
Nachschlageweg auf `username` mit dem Anmeldeweg machen wuerde, und warum diese
|
|
Frage hier trotzdem gegenstandslos ist: der Anmeldeweg laeuft seit Etappe 1 ueber die
|
|
drei SECURITY-DEFINER-Funktionen und nicht ueber diesen Dienst — belegt durch die
|
|
Aufrufermessung aus Teil 3, nicht behauptet. Die Gegenrichtung (die laut werfenden
|
|
Stellen) ebenfalls nennen, damit der Abschnitt nicht nur Alarm ist.
|
|
|
|
(u4) Was dieser Durchlauf bewusst nicht loest: die plattformweite Eindeutigkeit von
|
|
`username` und `email` selbst. Ausschreiben, dass die ehrliche Reparatur eine
|
|
Schemaaenderung waere (eine Eindeutigkeit mit Mandantendimension), dass das eine
|
|
Produktentscheidung ist — darf dieselbe Adresse zwei Mandanten gehoeren — und dass
|
|
sie fuer Etappe 3 bereits vorgemerkt ist. Hier wird sie festgehalten, nicht
|
|
entschieden und ausdruecklich nicht durch eine Migration vorweggenommen. Dazu die
|
|
Uebergabe in den noch nicht umgestellten Bereich `auth` (Befund E) mit der genauen
|
|
Grenze, und die offene Architekturfrage `req.tenantPrisma`, die auch dieser Bereich
|
|
nicht entscheidet.
|
|
|
|
(u5) Was dieser Durchlauf bewusst NICHT anfasst: `auth.service.ts` an keiner Stelle,
|
|
die drei SECURITY-DEFINER-Funktionen und ihre Rechte nicht, Schema und Migrationen
|
|
nicht, die Verdraengung im gemeinsamen Ablageverzeichnis der Profilbilder nicht
|
|
(sie ist keine Bindungsfrage). Jeweils mit der Feststellung, dass sie geprueft und
|
|
bewusst gelassen sind — nicht uebersehen.
|
|
</action>
|
|
<verify>
|
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in user-policy-aus-migration-wortgleich user-gebunden-nur-eigener-mandant user-ungebunden-null-zeilen user-ungebundene-suche-nach-benutzername-liefert-keine-zeile user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile user-eindeutigkeit-greift-trotz-unsichtbarkeit user-gebundenes-einfuegen-fremder-mandant-abgelehnt user-ungebundenes-einfuegen-abgelehnt user-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen user-gebundenes-loeschen-ueber-kennung-allein-trifft-null-zeilen tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar user-fan-out-je-mandant-gebunden-liefert-alle-zeilen; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden\.$' && grep -q '^## Bereich user$' docs/mandantentrennung-etappe2-fehlerrichtung.md && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"</automated>
|
|
</verify>
|
|
<done>Das Wegwerf-Werkzeug meldet alle Pruefungen bestanden (die 41 bisherigen plus die zwoelf namentlich geprueften des user-Abschnitts) mit Rueckgabewert 0; jede der zwoelf Kennungen steht einzeln als `bestanden` in der Ausgabe; die Zeile zum Eindeutigkeitsfehler nennt in ihrem Meldetext die gemessene Fehlerklasse und unterscheidet sie ausdruecklich von einer Zeilenschutz-Ablehnung; `npm --prefix apps/api run test` meldet weiterhin 789 Tests gruen und die Typpruefung ist sauber; `docs/mandantentrennung-etappe2-fehlerrichtung.md` traegt einen Abschnitt `## Bereich user` mit der tatsaechlich beobachteten Ausgabe, einer Signaltabelle, dem Unterabschnitt zur Kette mit sechs namentlich benannten Stellen, der Abgrenzung zum Anmeldeweg samt Aufrufermessung, und den beiden Abschnitten zu dem, was bewusst offen bzw. unangetastet bleibt; Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 2: Die Testlage herstellen, den Dienst umstellen, die Linie je Methode ziehen und die Startsperre entschaerfen</name>
|
|
<precondition>Aufgabe 1 ist abgeschlossen und uebersetzt; der Container `tessera-ctl-db-1` laeuft (Adresse erneut ermitteln, nicht abschreiben).</precondition>
|
|
<files>apps/api/src/user/user.service.spec.ts, apps/api/src/user/user.service.ts, apps/api/src/user/admin-seed.service.spec.ts, apps/api/src/user/admin-seed.service.ts, .planning/WINDOWS.md, BEDINGT apps/api/src/user/user.controller.ts (nur die vier Aufrufstellen der geaenderten Dienstsignaturen, falls die Signaturaenderung in dieser Aufgabe stattfindet — siehe die Reihenfolgeentscheidung im Aktionstext; nimmt diese Aufgabe die Signaturen nicht, bleibt die Datei hier unberuehrt und gehoert vollstaendig zu Aufgabe 3)</files>
|
|
<behavior>
|
|
Der Nachweis kommt VOR der Umstellung, sonst beweist er nichts. Beide vorhandenen
|
|
Testdateien haben KEINE Attrappe fuer das Bindungshilfsmittel (Befund C) — sie
|
|
werden zuerst auf den Zwei-Klienten-Nachweis umgebaut, danach wird umgestellt.
|
|
|
|
Muster fuer beide Dateien, nach `apps/api/src/groups/groups.service.spec.ts`:
|
|
|
|
- Die Erweiterung `../prisma/prisma-tenant.extension` wird gemockt, sodass
|
|
`forTenant(prisma, tenantId)` an `prisma.__makeBoundClient(tenantId)` delegiert.
|
|
`withTenantTransaction` wird NICHT gemockt und nicht gebraucht — dieser Bereich hat
|
|
keine Transaktion (Befund B).
|
|
- Ein handgeschriebener Speicher mit Maps fuer `user` und `tenant`.
|
|
`__makeBoundClient(tenantId)` liefert je Modell einen protokollierenden Wrapper um
|
|
DIESELBEN Maps und schreibt jeden Aufruf als `{ tenantId, model, method }` in ein
|
|
gemeinsames Protokoll. Der ungebundene Ersatz protokolliert nicht, der gebundene
|
|
schon — genau daran wird eine vergessene Bindung sichtbar.
|
|
- Der Speicher bildet die plattformweite Eindeutigkeit von `username` nach: ein
|
|
Einfuegen mit einem bereits vergebenen Benutzernamen wirft einen Fehler mit dem
|
|
Prisma-Fehlercode fuer Eindeutigkeitsverletzungen, UNABHAENGIG davon, welchem
|
|
Mandanten der bestehende Halter gehoert. Ohne diese Nachbildung ist die zentrale
|
|
Aussage dieses Bereichs nicht pruefbar.
|
|
|
|
Erwartete Testfaelle in `user.service.spec.ts`:
|
|
|
|
- Test 1: `create` steht mit der uebergebenen Mandantenkennung im Bindungsprotokoll,
|
|
und die bereits vorhandene Standardgruppen-Anbindung bleibt unveraendert wirksam
|
|
(die vier bestehenden Testfaelle der Datei bleiben inhaltlich erhalten).
|
|
- Test 2: `create` mit einem Benutzernamen, den ein Benutzer eines ANDEREN Mandanten
|
|
bereits haelt, wirft eine verstaendliche deutsche Konfliktmeldung statt eines
|
|
durchgereichten Datenbankfehlers — und die Meldung nennt weder den Halter noch
|
|
dessen Mandanten.
|
|
- Test 3: `update` steht gebunden im Protokoll, und ein Namenswechsel auf einen
|
|
fremd gehaltenen Benutzernamen wirft dieselbe Art von Konfliktmeldung.
|
|
- Test 4: `findById` steht gebunden im Protokoll und liefert einen Benutzer eines
|
|
anderen Mandanten NICHT.
|
|
- Test 5: `deactivate` und `delete` stehen gebunden im Protokoll.
|
|
- Test 6: die neue Methode fuer die Plattform-Administratorsicht ueber alle
|
|
Mandanten liest die Mandanten UNgebunden und danach je Mandant GEBUNDEN — im
|
|
Protokoll steht je Mandant genau ein Eintrag, und das Ergebnis enthaelt die
|
|
Benutzer beider Mandanten in der bisherigen Sortierung nach Benutzername.
|
|
- Test 7: die neue Methode zum Aufloesen einer Benutzerkennung fuer den
|
|
Plattform-Administrator findet einen Benutzer eines fremden Mandanten und tut das
|
|
ueber einen GEBUNDENEN Lesezugriff je Mandant — im Protokoll nachweisbar, nicht nur
|
|
am Ergebnis.
|
|
- Test 8: die Suche nach Benutzername steht NICHT im Bindungsprotokoll. Der Testname
|
|
sagt ausdruecklich, dass das Fehlen der Bindung hier die bestandene Erwartung ist,
|
|
damit niemand ihn spaeter als vergessene Bindung "repariert".
|
|
|
|
Erwartete Testfaelle in `admin-seed.service.spec.ts` (die bestehenden
|
|
Reihenfolge-Testfaelle bleiben inhaltlich erhalten):
|
|
|
|
- Test 9: die Erstanlage-Pruefung steht NICHT im Bindungsprotokoll, die Erstanlage
|
|
des Administrators dagegen steht dort mit der Kennung des unmittelbar zuvor
|
|
angelegten Mandanten. Das ist Befund J, und es ist der einzige Testfall dieses
|
|
Plans, der beide Richtungen in EINER Methode prueft.
|
|
- Test 10: liefert die Erstanlage-Pruefung nichts, waehrend das Anlegen an der
|
|
plattformweiten Eindeutigkeit scheitert, so wird daraus KEIN Startabbruch: der
|
|
Dienst behandelt das wie "der Administrator existiert bereits", protokolliert das
|
|
verstaendlich und laeuft weiter. Das ist die Entschaerfung aus Befund I; sie braucht
|
|
einen eigenen Test, nicht nur eine Bindungszaehlung.
|
|
- Test 11: jeder ANDERE Fehler beim Anlegen bricht den Start weiterhin ab — die
|
|
Absicht des Dateikopfs bleibt erhalten und wird nicht mit entschaerft.
|
|
- Test 12: die beiden Zugriffe auf die Mandantentabelle stehen NICHT im
|
|
Bindungsprotokoll, und die Reparaturschleife ruft die Standardgruppen-Sicherung
|
|
weiterhin je Mandant mit dessen Kennung auf.
|
|
|
|
Falsifizierungsnachweis, verlangt und zu belegen: nach der Umstellung eine der
|
|
gebundenen Stellen probeweise zurueckbauen, beobachten, dass GENAU der erwartete Test
|
|
rot wird, den Rueckbau zuruecknehmen, und beides im SUMMARY festhalten. Ein Test, von
|
|
dem nur behauptet wird, dass er rot werden koennte, ist kein Nachweis.
|
|
</behavior>
|
|
<action>
|
|
`apps/api/src/user/user.service.ts` — die Linie wird hier gezogen, Methode fuer
|
|
Methode, und jede Entscheidung steht am Ort:
|
|
|
|
Die Suche nach Benutzername bleibt UNGEBUNDEN. Ihr Kopfkommentar wird
|
|
richtiggestellt: er behauptet heute, sie diene dem mandantenuebergreifenden
|
|
Anmeldeweg, und das stimmt seit Etappe 1 nicht mehr. Der neue Kommentar sagt drei
|
|
Dinge — dass der Anmeldeweg seit 260909-eor ueber die drei SECURITY-DEFINER-Funktionen
|
|
laeuft und diese Methode nicht mehr beruehrt; dass sie zum Zeitpunkt der Umstellung
|
|
gemessen KEINEN Aufrufer hatte (Trefferzahl aus Aufgabe 1, Teil 3 eintragen); und
|
|
dass sie NICHT gebunden werden darf, weil der Benutzername plattformweit eindeutig
|
|
ist und eine gebundene Suche einen fremden Halter nicht saehe, faelschlich "frei"
|
|
meldete und die naechste Handlung des Aufrufers in einen harten Eindeutigkeitsfehler
|
|
liefe. Der Kommentar verweist auf den `user`-Abschnitt der Kritikschrift und auf den
|
|
wortgleichen Praezedenzfall `resolveEmailForWrite` im Bereich `ldap`, statt die
|
|
Begruendung zu wiederholen.
|
|
|
|
`findById`, `update`, `deactivate` und `delete` bekommen einen PFLICHT-Mandanten als
|
|
ersten Parameter und laufen ueber einen gebundenen Klienten aus
|
|
`forTenant(this.prisma, tenantId)`. Je Methode EIN gebundener Klient, nicht einer je
|
|
Modellzugriff. Die Steuerungsschicht wird in dieser Aufgabe NICHT angepasst (Aufgabe
|
|
3) — der Uebersetzer wuerde die Aufrufe dort sonst als fehlerhaft melden. Damit jede
|
|
Aufgabe fuer sich uebersetzbar bleibt, ist die Reihenfolge deshalb umgekehrt zu
|
|
waehlen: die neuen Signaturen und die Steuerungsschicht muessen im SELBEN Commit
|
|
liegen. Verlege deshalb die reine Signaturaenderung dieser vier Methoden samt aller
|
|
ihrer Aufrufstellen mit nach Aufgabe 3, ODER stelle in dieser Aufgabe zusaetzlich die
|
|
vier Aufrufstellen in `user.controller.ts` auf die neue Signatur um und lasse alles
|
|
Uebrige an der Steuerungsschicht unberuehrt. Welche der beiden Formen gewaehlt wird,
|
|
ist beim Aufschlagen zu entscheiden und im SUMMARY zu benennen; entscheidend ist
|
|
allein, dass am Ende JEDER Aufgabe die Typpruefung sauber ist.
|
|
|
|
`create` bindet an die im Datensatz uebergebene Mandantenkennung. Der bereits
|
|
vorhandene Kleinschreib-Schritt fuer den Benutzernamen und die Standardgruppen-
|
|
Anbindung bleiben unveraendert. Neu kommt die Uebersetzung der
|
|
Eindeutigkeitsverletzung hinzu, nach dem im Projekt etablierten Muster (Pruefung auf
|
|
den Prisma-Fehlercode, danach eine Konfliktausnahme mit deutscher Meldung, sonst den
|
|
Fehler weiterwerfen) — wie in `groups.service.ts` und den Diensten des Bereichs
|
|
`tenders`. Die Meldung sagt, dass Benutzername beziehungsweise Adresse plattformweit
|
|
bereits vergeben sind; sie nennt WEDER den Halter NOCH dessen Mandanten, weil das
|
|
sonst eine Aussage ueber einen fremden Mandanten waere. Der Kommentar an dieser
|
|
Stelle haelt fest, warum die Uebersetzung hier und nicht bei den Aufrufern liegt:
|
|
dies ist der einzige Erzeugungspunkt fuer Benutzer im Backend, der AD-Abgleich laeuft
|
|
ebenfalls hierueber, und die dortige Identitaetssuche ist bereits gebunden — die
|
|
Kette endet also bei jedem Aufrufer an dieser einen Stelle.
|
|
|
|
`update` bekommt dieselbe Uebersetzung, weil auch ein Namens- oder Adresswechsel auf
|
|
denselben plattformweiten Schluessel treffen kann.
|
|
|
|
Zwei neue Methoden fuer die uebergreifende Sicht des Plattform-Administrators
|
|
(Befund F). Beide tragen im Namen, dass sie fuer den Plattform-Administrator sind,
|
|
damit niemand sie fuer mandantengebundene Methoden haelt, und beide haben einen
|
|
Kopfkommentar, der sagt: dass die uebergreifende Sicht die bestehende, gewollte
|
|
Funktion der obersten Rolle ist; dass sie deshalb NICHT an den Mandanten des
|
|
Aufrufers gebunden werden darf, weil das eine stille Funktionsminderung waere; dass
|
|
sie aber auch nicht ungebunden bleiben darf, weil sie dann nach dem Scharfschalten
|
|
gar nichts mehr liefert; und dass sie deshalb als Schleife ueber alle Mandanten mit
|
|
je EINEM gebundenen Lesezugriff gebaut ist — dieselbe Form, die die
|
|
Standardgruppen-Reparatur beim Start bereits benutzt. Der Kommentar haelt ausserdem
|
|
fest, dass der Schleifentreiber ungebunden lesen DARF, weil die Mandantentabelle
|
|
keinen Zeilenschutz traegt, und verweist auf die Messung aus Aufgabe 1 statt das zu
|
|
behaupten.
|
|
|
|
Die erste Methode liefert alle Benutzer aller Mandanten mit derselben Feldauswahl und
|
|
derselben Sortierung nach Benutzername wie heute. Die Sortierung wird nach dem
|
|
Zusammenfuehren hergestellt, weil je Mandant sortierte Teilmengen zusammengehaengt
|
|
nicht sortiert sind — das ist die eine Stelle, an der die Umstellung das Ergebnis
|
|
verfaelschen koennte, und sie ist durch Test 6 abgedeckt. Die zweite Methode loest
|
|
eine Benutzerkennung auf, indem sie je Mandant gebunden sucht und beim ersten Treffer
|
|
zurueckkehrt.
|
|
|
|
`apps/api/src/user/admin-seed.service.ts`:
|
|
|
|
Die Erstanlage-Pruefung bleibt UNGEBUNDEN und bekommt einen eigenen Kommentar, der
|
|
beide Zustaende benennt: dass sie heute richtig arbeitet, weil es zu diesem Zeitpunkt
|
|
noch keinen Mandanten gibt und der Benutzername plattformweit eindeutig ist, und dass
|
|
sie nach dem Scharfschalten fuer JEDEN Administrator `null` liefert, weil ohne
|
|
gesetzten Kontext keine Zeile der Benutzertabelle sichtbar ist. Er benennt
|
|
ausdruecklich die Folge — Leere gelesen als Abwesenheit, danach Anlegen, danach ein
|
|
harter Eindeutigkeitsfehler — und verweist auf den `user`-Abschnitt der
|
|
Kritikschrift.
|
|
|
|
Die Erstanlage des Administrators wird GEBUNDEN, an die Kennung des unmittelbar
|
|
zuvor angelegten oder geholten Mandanten. Ihr Kommentar haelt die Korrektur aus
|
|
Befund J fest: die bisherige Klassifikationsbegruendung war falsch, der Mandant ist
|
|
an dieser Stelle bekannt, und ungebunden waere dieses Einfuegen nach dem
|
|
Scharfschalten abgewiesen worden — eine frische Installation haette ihren ersten
|
|
Administrator gar nicht anlegen koennen.
|
|
|
|
Die Entschaerfung der Startsperre: das Anlegen bekommt eine Behandlung fuer den
|
|
Prisma-Fehlercode der Eindeutigkeitsverletzung, die den Fall wie den bereits
|
|
vorhandenen Zweig "Administrator existiert bereits" behandelt — eine verstaendliche
|
|
Protokollzeile, kein Abbruch. Der Kommentar sagt, warum das keine Aufweichung der im
|
|
Dateikopf festgehaltenen Absicht ist: JEDER andere Fehler bricht den Start weiterhin
|
|
ab, und gerade dieser eine Fehler bedeutet an dieser Stelle exakt dasselbe wie ein
|
|
Treffer der vorgeschalteten Pruefung. Der Dateikopf wird entsprechend fortgeschrieben
|
|
statt ersetzt.
|
|
|
|
Die beiden Zugriffe auf die Mandantentabelle bleiben unveraendert ungebunden. Der
|
|
Kommentar der Reparaturschleife wird um die Feststellung ergaenzt, dass sie der
|
|
fuenfte und bislang einzige bereits vollstaendig richtige Fall der
|
|
Hintergrunddienst-Falle ist: uebergreifender Treiber, gebundener Rumpf. Und um die
|
|
Einschraenkung aus Befund K, dass ihr aeusserer Auffangblock jeden Fehler des
|
|
Treibers in eine Protokollzeile verschluckt.
|
|
|
|
Broken-Windows-Register: die plattformweite Eindeutigkeit von Benutzername und
|
|
Adresse als offenen Eintrag anlegen, mit
|
|
`node ~/.claude/gsd-core/bin/gsd-tools.cjs windows append` (die Aufrufform ohne
|
|
Argumente ausgeben lassen, wenn die erwarteten Felder unklar sind — nicht raten). Der
|
|
Text nennt die gemessene Kette, die betroffenen Pfade, die Tatsache, dass die ehrliche
|
|
Reparatur eine Schemaaenderung waere und als Produktentscheidung fuer Etappe 3
|
|
vorgemerkt ist, und die in diesem Durchlauf gewaehlten Entschaerfungen. Die
|
|
Verwaltungsfelder des Registers werden dem Werkzeug ueberlassen, nicht von Hand
|
|
geschrieben.
|
|
|
|
Nichts anderes wird in dieser Aufgabe angefasst: `auth.service.ts` nicht,
|
|
`ldap.service.ts` nicht, kein Schema, keine Migration, keine Compose- oder
|
|
Umgebungsdatei. Die Selbstbedienungswege und die Rollenlogik der Steuerungsschicht
|
|
gehoeren zu Aufgabe 3.
|
|
</action>
|
|
<verify>
|
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && grep -q '__makeBoundClient' apps/api/src/user/user.service.spec.ts && grep -q '__makeBoundClient' apps/api/src/user/admin-seed.service.spec.ts && grep -c 'forTenant(' apps/api/src/user/user.service.ts && grep -q 'forTenant(' apps/api/src/user/admin-seed.service.ts && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/auth apps/api/src/ldap docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"</automated>
|
|
</verify>
|
|
<done>Beide Testdateien nutzen den Zwei-Klienten-Nachweis ueber `__makeBoundClient` und bilden die plattformweite Eindeutigkeit von `username` nach; sie decken die zwoelf in `<behavior>` genannten Faelle ab, die bestehenden Testfaelle beider Dateien sind inhaltlich erhalten, die Testzahl liegt ueber 789 und der Lauf ist gruen; die Typpruefung ist sauber; das Wegwerf-Werkzeug meldet weiterhin alle Pruefungen bestanden; in `user.service.ts` laufen `findById`, `create`, `update`, `deactivate`, `delete` sowie die beiden neuen Methoden fuer die Plattform-Administratorsicht ueber `forTenant()`, und ausschliesslich die Suche nach Benutzername bleibt ungebunden — mit einem Kopfkommentar, der die Aufrufermessung, den Verweis auf den Anmeldeweg und das Bindungsverbot samt Begruendung nennt; `create` und `update` uebersetzen die Eindeutigkeitsverletzung in eine deutsche Konfliktmeldung, die weder Halter noch fremden Mandanten nennt; in `admin-seed.service.ts` ist die Erstanlage des Administrators an den zuvor angelegten Mandanten gebunden, die Erstanlage-Pruefung bleibt mit geschriebener Begruendung ungebunden, und die Startsperre ist entschaerft, ohne dass ein anderer Fehler seine abbrechende Wirkung verliert; die plattformweite Eindeutigkeit steht als offener Eintrag im Broken-Windows-Register; der Falsifizierungsnachweis ist im SUMMARY festgehalten; `auth.service.ts`, `ldap.service.ts`, Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Aufgabe 3: Die Steuerungsschicht binden, den wirkungslosen Selbstloesch-Riegel schliessen und beide Dokumente samt Handzaehlungen nachziehen</name>
|
|
<precondition>Aufgabe 2 ist abgeschlossen und uebersetzt; der Container `tessera-ctl-db-1` laeuft (Adresse erneut ermitteln, nicht abschreiben).</precondition>
|
|
<files>apps/api/src/user/user.controller.spec.ts, apps/api/src/user/user.controller.ts, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md</files>
|
|
<behavior>
|
|
Wieder Nachweis vor Umbau. Diese Schicht hatte bisher gar keine Testdatei (Befund C,
|
|
dritte Form) — `apps/api/src/user/user.controller.spec.ts` wird neu angelegt, mit
|
|
demselben Zwei-Klienten-Nachweis und demselben Bindungsprotokoll wie in Aufgabe 2.
|
|
Der Dienst wird dabei als Attrappe gestellt, sein Bindungsverhalten ist bereits in
|
|
Aufgabe 2 geprueft.
|
|
|
|
- Test 1: die Benutzerliste eines Mandanten-Administrators steht gebunden im
|
|
Protokoll, traegt die Mandantenkennung aus dem Sitzungsnachweis, und liefert keine
|
|
Benutzer eines zweiten Mandanten.
|
|
- Test 2: die Benutzerliste des Plattform-Administrators geht ueber die neue,
|
|
uebergreifende Methode des Dienstes und liefert weiterhin die Benutzer aller
|
|
Mandanten in der bisherigen Sortierung. Dass die Rollenverzweigung erhalten bleibt,
|
|
ist der Kern dieses Tests.
|
|
- Test 3: die drei Wege ueber die Kennung loesen den Zielbenutzer rollenabhaengig auf
|
|
— fuer einen Mandanten-Administrator gebunden an dessen eigenen Mandanten, fuer den
|
|
Plattform-Administrator ueber die uebergreifende Methode.
|
|
- Test 4: ein Mandanten-Administrator, der einen Benutzer eines fremden Mandanten
|
|
ueber dessen Kennung anspricht, bekommt weiterhin eine Ablehnung — und die
|
|
bestehende ausdrueckliche Mandantenpruefung im Code bleibt erhalten, sie wird nicht
|
|
durch die Datenbank ersetzt. Der Test haelt fest, welche der beiden Ablehnungsarten
|
|
entsteht, damit eine spaetere Aenderung sichtbar wird.
|
|
- Test 5: ein Mandanten-Administrator kann weiterhin keine oberste Rolle vergeben,
|
|
und die Anlage eines Benutzers landet weiterhin im Mandanten des Aufrufers, wenn
|
|
dieser nicht die oberste Rolle traegt.
|
|
- Test 6: der Riegel gegen das Loeschen des eigenen Kontos greift. Dieser Test muss
|
|
gegen die HEUTIGE Fassung rot sein — das ist der Beleg fuer Befund H und der
|
|
wichtigste Test dieser Aufgabe. Er wird zuerst geschrieben, sein Rotwerden wird
|
|
beobachtet und im SUMMARY festgehalten, und erst danach wird die Zeile repariert.
|
|
- Test 7: alle fuenf Zugriffe der vier Selbstbedienungswege (Bild hochladen, Bild
|
|
loeschen, Akzentfarbe setzen, Bild ausliefern) stehen gebunden im Protokoll, mit
|
|
der Mandantenkennung aus dem Sitzungsnachweis.
|
|
- Test 8: der Weg, der ein Bild ausliefert, liefert fuer einen Benutzer ohne
|
|
hinterlegtes Bild weiterhin die vorhandene Nicht-gefunden-Ausnahme und aendert sein
|
|
Verhalten nicht.
|
|
|
|
Falsifizierungsnachweis wie in Aufgabe 2: eine gebundene Stelle probeweise
|
|
zurueckbauen, beobachten, dass genau der erwartete Test rot wird, zuruecknehmen, im
|
|
SUMMARY festhalten. Fuer Test 6 kommt der Nachweis aus der Reihenfolge selbst: rot
|
|
vor der Reparatur, gruen danach.
|
|
</behavior>
|
|
<action>
|
|
`apps/api/src/user/user.controller.ts`, alle sieben Zugriffe auf `user`:
|
|
|
|
Der Zweig fuer den Mandanten-Administrator in der Benutzerliste laeuft ueber einen
|
|
gebundenen Klienten aus `forTenant(this.prisma, currentUser.tenantId)`. Die
|
|
vorhandene Mandantenbedingung im `where` BLEIBT erhalten — nicht mit dem Argument
|
|
entfernen, das mache jetzt die Datenbank; dieselbe Regel, die der `tenders`- und der
|
|
`dkv`-Durchlauf aufgestellt haben. Die Feldauswahl und die Sortierung bleiben
|
|
unveraendert.
|
|
|
|
Der Zweig fuer den Plattform-Administrator wird auf die in Aufgabe 2 angelegte
|
|
uebergreifende Methode des Dienstes umgestellt. Die Rollenverzweigung selbst bleibt
|
|
unveraendert bestehen; sie ist die Stelle, an der dieser Bereich die gewollte
|
|
uebergreifende Sicht von der mandantengebundenen unterscheidet, und sie darf nicht
|
|
eingeebnet werden.
|
|
|
|
Die drei Wege ueber die Kennung loesen den Zielbenutzer rollenabhaengig auf: fuer
|
|
einen Mandanten-Administrator ueber die an dessen Mandanten gebundene Methode des
|
|
Dienstes, fuer den Plattform-Administrator ueber die uebergreifende Aufloesemethode.
|
|
Die anschliessenden Pruefungen bleiben Wort fuer Wort erhalten: die
|
|
Mandantenzugehoerigkeit, das Verbot der Rollenerhoehung, der Riegel gegen das
|
|
Loeschen des eigenen Kontos. Die Schreibaufrufe an den Dienst bekommen die
|
|
Mandantenkennung des ZIELBENUTZERS mit, so wie sie aus der vorangegangenen Aufloesung
|
|
hervorgeht — nicht die des Aufrufers; nur so bleibt die uebergreifende Verwaltung
|
|
durch die oberste Rolle erhalten und ist der Schreibzugriff trotzdem gebunden. Ein
|
|
Kommentar an dieser Stelle haelt genau diese Unterscheidung fest, weil sie beim
|
|
Lesen nicht offensichtlich ist.
|
|
|
|
Der Riegel gegen das Loeschen des eigenen Kontos (Befund H) wird repariert: der
|
|
Vergleich zieht das Feld heran, das der Sitzungsnachweis tatsaechlich traegt. Ein
|
|
Kommentar haelt fest, dass der Riegel vorher nie gegriffen hat, weil das verglichene
|
|
Feld im Sitzungsnachweis nicht existiert, und dass die Wirkung damit eine
|
|
Verhaltensaenderung ist: ein Administrator kann sein eigenes Konto nun nicht mehr
|
|
loeschen. Das ist die urspruengliche, im Code bereits formulierte Absicht.
|
|
|
|
Die fuenf Zugriffe der vier Selbstbedienungswege binden vollstaendig an die
|
|
Mandantenkennung aus dem Sitzungsnachweis. Je Weg EIN gebundener Klient, nicht einer
|
|
je Modellzugriff. Es aendert sich nichts an den Dateipfaden, an der Pruefung der
|
|
Dateitypen, an der Groessenbegrenzung und am Aufraeumen alter Bilddateien.
|
|
|
|
`docs/mandantentrennung-zugriffsklassifikation.md` nachziehen — hier liegen die vier
|
|
Stellen, die im `dkv`-Durchlauf uebersehen wurden, und sie sind alle handgepflegt:
|
|
|
|
- Die Bereichszeile `user` der Uebersichtstabelle mit den bei der Ausfuehrung NEU
|
|
gemessenen Zahlen fortschreiben, im Stil der bereits fortgeschriebenen Zeilen
|
|
(`war 17/0` plus eine Begruendung, welche Zugriffe umgestellt wurden und welche
|
|
bewusst nicht). Die Zahlen werden mit den beiden Befehlen ermittelt, die das
|
|
Dokument selbst ueber dieser Tabelle nennt — der eine zaehlt die ungebundenen
|
|
Rohtreffer, der andere die ueber den Namen `tenantPrisma` gebundenen. Die
|
|
Pruefung dieser Aufgabe ermittelt sie erneut und vergleicht sie gegen die Zeile:
|
|
das Dokument wird also an den Code angeglichen, nicht umgekehrt. Zur
|
|
Planungszeit an vier bereits umgestellten Bereichen gegengeprueft, dass diese
|
|
beiden Befehle deren Tabellenzeilen exakt reproduzieren. Daraus folgt eine
|
|
Auflage fuer Aufgabe 2 und 3, die sonst still unterlaufen wuerde: der gebundene
|
|
Klient heisst in jeder Methode `tenantPrisma`, wie in `ldap`, `groups`, `dkv` und
|
|
`auth` — ein anderer Name wuerde die zweite Zaehlung untertreiben lassen und die
|
|
Zeile ihrer Aussage berauben.
|
|
- Die Summenzeile mitziehen. Die Pruefung dieser Aufgabe addiert die zwoelf
|
|
Bereichszeilen und haelt das Ergebnis gegen die Summenzeile — eine
|
|
fortgeschriebene Bereichszeile ueber einer stehen gebliebenen Summe faellt damit
|
|
durch, genau der Fehler, der im `dkv`-Durchlauf durchrutschte.
|
|
- Die vier vorhandenen Bestandsaufnahme-Zeilen des Bereichs auf den maschinell
|
|
gemessenen Stand setzen und dabei ZWEI Klassenkorrekturen vornehmen, jede mit
|
|
Begruendung: `user.service.ts`/`user` wechselt wegen der einen bewusst
|
|
ungebundenen Suche von `muss-mandantengebunden` auf `beides` — wortgleich derselbe
|
|
Praezedenzfall wie `ldap.service.ts`/`user` in 260909-ipc; und
|
|
`admin-seed.service.ts`/`user` wechselt von `bewusst-uebergreifend` auf `beides`,
|
|
weil die bisherige Begruendung nachweislich falsch war (Befund J).
|
|
`user.controller.ts`/`user` endet auf `gebunden`, `admin-seed.service.ts`/`tenant`
|
|
bleibt unveraendert.
|
|
- Die NEUE Zeile fuer das Paar `(user.service.ts, tenant)` ergaenzen, Klasse
|
|
`keine-mandantengebundene-tabelle`, Stand `ungebunden`, mit der Begruendung, dass
|
|
es der Schleifentreiber der Plattform-Administratorsicht ist und die
|
|
Mandantentabelle keinen Zeilenschutz traegt (Aufgabe 1 gemessen). Ohne diese Zeile
|
|
wird `rls-access-inventory.spec.ts` rot (Befund N).
|
|
- Die Klassen-Verteilungstabelle samt ihrer Summe und der Ueberschrift, die die
|
|
Paarzahl nennt, auf die neuen Zahlen ziehen. Die Zahlen kommen aus der Ausgabe der
|
|
Inventarpruefung, nicht aus einer Rechnung im Kopf. Die Pruefung dieser Aufgabe
|
|
zaehlt die Klassen ueber die Bestandsaufnahme-Zeilen selbst nach und haelt vier
|
|
Dinge gegeneinander: jede einzelne Klassenzahl, die Summe der Klassenzeilen, die
|
|
ausgewiesene Summenzeile und die Paarzahl in der Ueberschrift. Weil dieser
|
|
Durchlauf mit dem Schleifentreiber ein zusaetzliches Paar erzeugt, wandern alle
|
|
vier gemeinsam — eine stehen gebliebene faellt durch. Zur Planungszeit gegen das
|
|
heutige, in sich stimmige Dokument getestet (es besteht) und gegen eine
|
|
mutierte Fassung, in der genau dieser Fehler nachgestellt wurde (sie faellt
|
|
durch, mit der Meldung, welche Zahl klemmt).
|
|
- Den Abschnitt zum Hintergrunddienst als Falle um den fuenften Fall erweitern und
|
|
seine Ueberschrift, die heute vier Faelle nennt, mitziehen. Der fuenfte Fall ist die
|
|
Standardgruppen-Reparatur beim Start (Befund K) und ist ausdruecklich als der
|
|
bislang EINZIGE Fall zu beschreiben, der auf beiden Haelften bereits richtig ist —
|
|
uebergreifender Treiber, gebundener Rumpf — mit der Einschraenkung, dass sein
|
|
aeusserer Auffangblock jeden Fehler des Treibers in eine Protokollzeile
|
|
verschluckt.
|
|
|
|
Sollte `rls-access-inventory.spec.ts` nach der Umstellung einen anderen Stand messen
|
|
als hier beschrieben, gilt die MESSUNG: dann wird das Dokument auf den gemessenen
|
|
Stand gesetzt und die Abweichung im SUMMARY ausgeschrieben, statt die Pruefung passend
|
|
zu machen.
|
|
|
|
`docs/mandantentrennung-etappe2-fehlerrichtung.md` abschliessen: den in Aufgabe 1
|
|
angelegten `user`-Abschnitt um einen Nachtrag ergaenzen, der die tatsaechlich
|
|
umgesetzten Pfade gegen die dort angekuendigten haelt, die geschlossene Luecke im
|
|
Selbstloesch-Riegel festhaelt und die gewaehlte Form der Plattform-Administratorsicht
|
|
samt ihres Belegs aus der Messung nennt. Wie in den vorherigen Durchlaeufen wird der
|
|
urspruengliche Text NICHT umgeschrieben — er beschreibt korrekt den Zustand zum
|
|
Zeitpunkt der Umstellung; der Nachtrag steht daneben.
|
|
</action>
|
|
<verify>
|
|
<automated>DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs && npm --prefix apps/api run test && npm --prefix apps/api run type-check && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && test -f apps/api/src/user/user.controller.spec.ts && grep -q '__makeBoundClient' apps/api/src/user/user.controller.spec.ts && grep -qE '^\| apps/api/src/user/user\.controller\.ts \| user \| muss-mandantengebunden \| gebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/user/user\.service\.ts \| user \| beides \| gemischt \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/user/user\.service\.ts \| tenant \| keine-mandantengebundene-tabelle \| ungebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/user/admin-seed\.service\.ts \| user \| beides \| gemischt \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^\| apps/api/src/user/admin-seed\.service\.ts \| tenant \| keine-mandantengebundene-tabelle \| ungebunden \|' docs/mandantentrennung-zugriffsklassifikation.md && grep -qE '^## Der Hintergrunddienst als Falle — fünf Fälle$' docs/mandantentrennung-zugriffsklassifikation.md && U=$(grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/user | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." apps/api/src/user | grep -v spec | wc -l | tr -d ' ') && { test "$U" -lt 17 || { echo "UEBERSICHTSZEILE: ungebundene Rohtreffer in apps/api/src/user sind $U, also nicht gesunken — es wurde nichts umgestellt"; exit 1; }; } && { test "$B" -gt 0 || { echo "UEBERSICHTSZEILE: gebundene Rohtreffer in apps/api/src/user sind $B"; exit 1; }; } && { grep -qE "^\| user \| ${U} \| ${B} \| \*\*war 17/0\*\*" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE user nennt nicht die neu gemessenen Zahlen ${U}/${B} im etablierten Stil"; exit 1; }; } && awk -F'|' '$2 ~ /^ *[a-z][a-z-]* *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && awk -F'|' '$2 ~ /^ *apps\/api\/src\// { k=$4; gsub(/^ +| +$/,"",k); cls[k]++; pairs++ } $2 ~ /^ *(muss-mandantengebunden|keine-mandantengebundene-tabelle|beides|bewusst-uebergreifend) *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *$/ { k=$2; gsub(/^ +| +$/,"",k); v=$3; gsub(/[^0-9]/,"",v); tab[k]=v+0; tn++ } $2 ~ /^ *\*\*Summe\*\* *$/ && $4 ~ /^ *$/ { v=$3; gsub(/[^0-9]/,"",v); tsum=v+0; tseen=1 } /^## Klassen-Verteilung/ { h=$0; gsub(/[^0-9]/,"",h); hp=h+0; hseen=1 } END { if (tn+0 != 4 || !tseen || !hseen) { print "KLASSEN-VERTEILUNG nicht erkannt: Klassenzeilen " tn ", Summenzeile " tseen ", Ueberschrift " hseen; exit 1 } if (tsum != pairs+0) { print "KLASSEN-SUMME stimmt nicht: Bestandsaufnahme hat " pairs " Paare, Tabellensumme nennt " tsum; exit 1 } if (hp != pairs+0) { print "UEBERSCHRIFT der Klassen-Verteilung nennt " hp " Paare, Bestandsaufnahme hat " pairs; exit 1 } s=0; for (k in tab) { if (tab[k] != cls[k]+0) { print "KLASSE " k ": Tabelle nennt " tab[k] ", Bestandsaufnahme zaehlt " cls[k]+0; exit 1 } s+=tab[k] } if (s != pairs+0) { print "KLASSENZEILEN ergeben " s ", Bestandsaufnahme hat " pairs; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/auth apps/api/src/ldap docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)"</automated>
|
|
</verify>
|
|
<done>Alle sieben Zugriffe der Steuerungsschicht laufen ueber `forTenant()` beziehungsweise ueber die uebergreifenden Methoden des Dienstes, deren Rumpf je Mandant gebunden ist; die Rollenverzweigung zwischen mandantengebundener und uebergreifender Sicht ist erhalten und durch Tests belegt; der Riegel gegen das Loeschen des eigenen Kontos greift, und sein Rotwerden vor der Reparatur ist im SUMMARY festgehalten; `apps/api/src/user/user.controller.spec.ts` existiert, nutzt den Zwei-Klienten-Nachweis und deckt die acht in `<behavior>` genannten Faelle ab; der Testlauf ist gruen mit mehr als 789 Tests und die Typpruefung sauber; `rls-access-inventory.spec.ts` laeuft gruen und stimmt mit den fuenf Bestandsaufnahme-Zeilen des Bereichs ueberein, einschliesslich der neu ergaenzten Zeile fuer den Schleifentreiber; die beiden Klassenkorrekturen sind mit Begruendung vollzogen; alle VIER handgepflegten Stellen sind nachgezogen UND einzeln maschinell gegatet — die Uebersichtszeile gegen die am Code neu ermittelten Rohtrefferzahlen, die Summenzeile gegen die Addition der zwoelf Bereichszeilen, die Klassen-Verteilung gegen die ueber die Bestandsaufnahme nachgezaehlten Klassen samt Summe und Ueberschrift, und der Abschnitt zur Hintergrunddienst-Falle gegen seine byte-genaue Ueberschrift; keine dieser vier Pruefungen kann von einer stehen gebliebenen Fassung bestanden werden; die Kritikschrift traegt den Nachtrag mit den tatsaechlich umgesetzten Pfaden; das Wegwerf-Werkzeug meldet alle Pruefungen bestanden; `auth.service.ts`, `ldap.service.ts`, Schema, Migrationen, Compose- und Beispiel-Umgebungsdateien sind unveraendert und `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Browser/Admin → Benutzer-API | Der Aufrufer kommt ausschliesslich aus dem Sitzungsnachweis (`request.user`, gesetzt von der JWT-Pruefstrategie aus dem httpOnly-Cookie); er traegt genau vier Felder: Kennung, Benutzername, Rolle, Mandant. Eine Mandantenkennung aus dem Anfragekoerper wird nur akzeptiert, wenn der Aufrufer die oberste Rolle traegt. Alles jenseits dieser Grenze ist nicht vertrauenswuerdig. |
|
|
| Mandanten-Administrator → fremder Mandant | Die Grenze, um die es in diesem Bereich geht. Sie wird heute ALLEIN vom Anwendungscode gezogen (Rollenvergleich plus Mandantenvergleich nach einem ungebundenen Lesezugriff). |
|
|
| Oberste Rolle → alle Mandanten | Eine bewusst durchlaessige Grenze: die Plattformverwaltung sieht und verwaltet alle Mandanten. Sie wird in diesem Plan nicht enger gezogen, aber so gebaut, dass sie den Zeilenschutz nicht umgeht, sondern ihn je Mandant benutzt. |
|
|
| API → PostgreSQL | Die RLS-Grenze. Heute wirkungslos, weil `DATABASE_URL` auf die Rolle `tessera` mit BYPASSRLS zeigt (WINDOWS #18) — dieser Plan bereitet die Grenze vor, schaltet sie aber NICHT scharf. |
|
|
| API → plattformweiter Eindeutigkeitsraum von `username`/`email` | Die einzige Grenze dieses Bereichs, die der Zeilenschutz NICHT ziehen kann: der eindeutige Index kennt keinen Mandanten und wirkt quer ueber alle. Sie ist der Grund fuer jede Ausnahme in diesem Plan. |
|
|
| Anwendungsstart → Datenbank | Der Erstanlage-Weg laeuft ohne Anfrage, ohne Sitzungsnachweis und im ersten Schritt ohne Mandanten. |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-DAS-01 | Elevation of Privilege | `user.controller.ts` — Aendern eines Benutzers ueber `PATCH /users/:id`: Rolle setzen, Konto aktivieren oder abschalten, Kennwort neu setzen. Heute liegt zwischen einem Administrator von Mandant A und einem Benutzer von Mandant B ausschliesslich ein Vergleich im Anwendungscode, dem ein UNGEBUNDENER Lesezugriff vorausgeht | critical | mitigate | Aufgabe 3 bindet die Aufloesung des Zielbenutzers fuer einen Mandanten-Administrator an dessen eigenen Mandanten und den Schreibzugriff an den Mandanten des aufgeloesten Ziels; Aufgabe 2 gibt den vier Dienstmethoden einen Pflicht-Mandanten, sodass ein ungebundener Aufruf gar nicht mehr formulierbar ist. Die vorhandene ausdrueckliche Mandantenpruefung und das Verbot der Rollenerhoehung bleiben Wort fuer Wort als zweite Schicht bestehen. Aufgabe 1 misst an der ausgelieferten Policy, dass ein gebundenes Aendern ueber die Kennung allein eine fremde Zeile nicht trifft (`user-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen`) — es scheitert also still, weshalb die Vorpruefung nicht entfallen darf. Tests 3 und 4 der Aufgabe 3 belegen beides. |
|
|
| T-DAS-02 | Information Disclosure | `user.controller.ts` — Benutzerliste und Einzelabruf: Benutzername, Adresse, Anzeigename, Rolle, Aktivzustand und letzter Anmeldezeitpunkt aller Beschaeftigten eines fremden Unternehmens | high | mitigate | Aufgabe 3 bindet den Zweig des Mandanten-Administrators an dessen Mandanten und laesst die vorhandene Mandantenbedingung als zweite Schicht stehen; Aufgabe 1 misst, dass ein gebundener Lesezugriff nur die eigenen Zeilen liefert (`user-gebunden-nur-eigener-mandant`) und ein ungebundener nach dem Scharfschalten gar keine (`user-ungebunden-null-zeilen`). Test 1 der Aufgabe 3 belegt es am Code. |
|
|
| T-DAS-03 | Elevation of Privilege | Kontouebernahme ueber einen falsch gebundenen Nachschlageweg auf einem plattformweit eindeutigen Schluessel: eine gebundene Suche sieht einen fremden Halter nicht, meldet "frei", und ein darauf vertrauender Schreibweg koennte eine bestehende Identitaet neu belegen oder umlenken | high | mitigate | Die Suche nach Benutzername bleibt ausdruecklich UNGEBUNDEN, mit der Begruendung am Ort (Aufgabe 2) — derselbe Praezedenzfall wie `resolveEmailForWrite` im Bereich `ldap`. Aufgabe 1 misst die vollstaendige Kette an der echten Policy und weist nach, dass das Anlegen trotz "frei" hart an der Eindeutigkeit scheitert und die Ablehnung eine Eindeutigkeitsverletzung und keine Zeilenschutz-Ablehnung ist (`user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile`, `user-eindeutigkeit-greift-trotz-unsichtbarkeit`). Aufgabe 2 uebersetzt die Verletzung am einzigen Erzeugungspunkt fuer Benutzer in eine Konfliktmeldung, sodass aus einer stillen Falschbelegung ein sichtbarer Abbruch wird. Tests 2, 3 und 8 der Aufgabe 2 belegen es. |
|
|
| T-DAS-04 | Denial of Service | Umgekehrte Fehlerrichtung mit Startsperre: die Erstanlage-Pruefung beim Start geht nach dem Scharfschalten blind, liest Leere als "der Administrator existiert nicht", legt an, kollidiert plattformweit — und weil der Erstanlage-Schritt bewusst nicht gekapselt ist, startet die Anwendung nicht mehr. Trifft jede bestehende Installation mit gesetzten Administrator-Umgebungswerten | high | mitigate | Aufgabe 1 misst die beiden tragenden Eigenschaften einzeln (`user-ungebundene-suche-nach-benutzername-liefert-keine-zeile`, `user-eindeutigkeit-greift-trotz-unsichtbarkeit`). Aufgabe 2 entschaerft im Anwendungscode: die Eindeutigkeitsverletzung an genau dieser Stelle bedeutet dasselbe wie ein Treffer der vorgeschalteten Pruefung und wird als "Administrator existiert bereits" protokolliert, ohne Abbruch — waehrend JEDER andere Fehler seine abbrechende Wirkung behaelt. Tests 10 und 11 der Aufgabe 2 pruefen beide Richtungen getrennt. Keine Schemaaenderung, keine Migration. |
|
|
| T-DAS-05 | Denial of Service | Frische Installation kann ihren allerersten Administrator nach dem Scharfschalten nicht anlegen: das Einfuegen laeuft ohne gesetzten Mandantenkontext, und die Policy traegt keine eigene WITH-CHECK-Klausel | high | mitigate | Aufgabe 1 misst, was PostgreSQL daraus fuer ein Einfuegen ableitet, statt es aus dem Policy-Text zu schliessen (`user-ungebundenes-einfuegen-abgelehnt`, `user-gebundenes-einfuegen-fremder-mandant-abgelehnt`). Aufgabe 2 bindet die Erstanlage an den unmittelbar zuvor angelegten Mandanten und korrigiert damit zugleich die nachweislich falsche Klassifikationsbegruendung (Befund J). Test 9 der Aufgabe 2 prueft beide Richtungen derselben Methode in einem Testfall. |
|
|
| T-DAS-06 | Elevation of Privilege | Wirkungsloser Riegel gegen das Loeschen des eigenen Kontos: der Vergleich zieht ein Feld heran, das der Sitzungsnachweis nicht traegt, und ist deshalb immer falsch. Der einzige Administrator eines Mandanten kann sich selbst loeschen und den Mandanten ohne Verwaltung zuruecklassen | medium | mitigate | Aufgabe 3 repariert den Vergleich auf das tatsaechlich vorhandene Feld. Der Nachweis kommt aus der Reihenfolge: Test 6 wird zuerst geschrieben, sein Rotwerden gegen die heutige Fassung wird beobachtet und im SUMMARY festgehalten, erst danach wird repariert. Ein Kommentar haelt fest, dass die Wirkung eine Verhaltensaenderung ist und der urspruenglichen, im Code bereits formulierten Absicht entspricht. |
|
|
| T-DAS-07 | Denial of Service | Die uebergreifende Sicht der obersten Rolle geht nach dem Scharfschalten in JEDER heute denkbaren Fassung verloren: ungebunden liefert sie null Zeilen, an den Mandanten des Aufrufers gebunden mindert sie still die Funktion | medium | mitigate | Aufgabe 2 baut sie als Schleife ueber alle Mandanten mit je EINEM gebundenen Lesezugriff im Rumpf — dieselbe Form, die die Standardgruppen-Reparatur beim Start bereits benutzt. Aufgabe 1 misst die beiden Voraussetzungen: dass die Mandantentabelle keinen Zeilenschutz traegt und ungebunden lesbar bleibt (`tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`) und dass die Vereinigung der je Mandant gebundenen Ergebnisse der Gesamtmenge entspricht (`user-fan-out-je-mandant-gebunden-liefert-alle-zeilen`). Tests 6 und 7 der Aufgabe 2 und Test 2 der Aufgabe 3 belegen, dass Sortierung und Rollenverzweigung erhalten bleiben. |
|
|
| T-DAS-08 | Information Disclosure | Die Konfliktmeldung bei einem vergebenen Benutzernamen oder einer vergebenen Adresse verraet, dass und moeglicherweise wo ein fremder Mandant diesen Schluessel haelt | medium | mitigate | Die in Aufgabe 2 eingefuehrte Meldung nennt weder den Halter noch dessen Mandanten, sondern ausschliesslich, dass der Schluessel plattformweit bereits vergeben ist. Tests 2 und 3 der Aufgabe 2 pruefen die Meldung, nicht nur das Werfen. Die Restaussage — dass der Schluessel irgendwo belegt ist — ist unvermeidbar, solange die Eindeutigkeit plattformweit gilt; genau das ist der fuer Etappe 3 vorgemerkte Produktentscheid und steht als offener Registereintrag. |
|
|
| T-DAS-09 | Information Disclosure | Selbstbedienungswege fuer Profilbild und Akzentfarbe: fuenf Zugriffe auf die Benutzertabelle ueber die Kennung aus dem Sitzungsnachweis, heute ungebunden | low | mitigate | Aufgabe 3 bindet alle fuenf an die Mandantenkennung aus dem Sitzungsnachweis; Test 7 der Aufgabe 3 weist die Bindung im Protokoll nach, Test 8 haelt fest, dass das Verhalten bei fehlendem Bild unveraendert bleibt. Die Ablage der Bilddateien selbst ist keine Bindungsfrage und bleibt unangetastet — in der Kritikschrift festgehalten. |
|
|
| T-DAS-10 | Tampering | Plattformweite Eindeutigkeit von `username` und `email` ohne Mandantendimension: die ehrliche Reparatur waere eine Schemaaenderung, und genau die ist hier verboten | high | transfer | Bewusst NICHT in diesem Durchlauf geloest und ausdruecklich nicht durch eine Migration vorweggenommen: es ist eine Produktentscheidung — darf dieselbe Adresse zwei Mandanten gehoeren — und sie ist fuer Etappe 3 bereits vorgemerkt. Uebertragen wird sie mit vollstaendiger Beweislage statt mit einer Behauptung: gemessene Kette in Aufgabe 1, ausgeschriebener Abschnitt (u4) der Kritikschrift in Aufgabe 1, offener Eintrag im Broken-Windows-Register in Aufgabe 2. Bis dahin wirken die Entschaerfungen aus T-DAS-03 und T-DAS-04, die den Schaden von still auf sichtbar drehen, ohne die Ursache zu beruehren. |
|
|
| T-DAS-SC | Tampering | npm/pip/cargo-Installationen | n/a | accept | Dieser Plan installiert kein Paket — keine Aufgabe fuehrt einen Paketmanager aus, alle benutzten Bausteine (`vitest`, `@prisma/client`, `forTenant`, `argon2`) sind bereits Abhaengigkeiten. Das Legitimitaets-Gate faellt damit nicht an; sollte bei der Ausfuehrung wider Erwarten eine Installation noetig werden, ist das ein Abbruchgrund und keine Nebensache. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- Das Wegwerf-Werkzeug meldet nach jeder Aufgabe alle Pruefungen bestanden, mit
|
|
Rueckgabewert 0, und die 41 vorbestehenden Pruefungen laufen unveraendert mit.
|
|
- `npm --prefix apps/api run test` ist nach jeder Aufgabe gruen und liegt nach
|
|
Aufgabe 2 ueber 789 Tests.
|
|
- `npm --prefix apps/api run type-check` gibt nach jeder Aufgabe 0 zurueck. Das ist
|
|
in diesem Plan mehr als eine Formalie: die vier Dienstmethoden bekommen einen neuen
|
|
Pflichtparameter, und die Typpruefung ist der Nachweis, dass keine Aufrufstelle
|
|
uebersehen wurde.
|
|
- `rls-access-inventory.spec.ts` und die Stand-Spalte des Klassifikationsdokuments
|
|
stimmen fuer alle fuenf Paare des Bereichs ueberein — die vier vorhandenen und das
|
|
eine neue, das die Schleife ueber alle Mandanten erzeugt.
|
|
- `git diff --name-only HEAD -- apps/api/prisma apps/api/src/auth apps/api/src/ldap docker-compose*.yml .env.example .env.prod.example`
|
|
ist nach jeder Aufgabe leer: kein Schema, keine Migration, kein Anmeldeweg, kein
|
|
AD-Abgleich, keine Compose-Datei, keine Umgebungsdatei angefasst. `DATABASE_URL`
|
|
zeigt unveraendert auf die Rolle `tessera` — der Schalter bleibt AUS.
|
|
- Der Falsifizierungsnachweis ist je Aufgabe (2 und 3) im SUMMARY ausgeschrieben:
|
|
welche Bindung probeweise zurueckgebaut wurde, welcher Test daraufhin rot wurde,
|
|
und dass der Rueckbau zurueckgenommen ist. Fuer den Selbstloesch-Riegel gilt
|
|
zusaetzlich der Nachweis aus der Reihenfolge: rot vor der Reparatur, gruen danach.
|
|
- Im Verzeichnisdienst wurde nichts geaendert; dieser Plan beruehrt Active Directory
|
|
an keiner Stelle.
|
|
- Lokal gibt es keinen Mail-Container; ein Versandfehler auf `mailhog` ist
|
|
umgebungsbedingt und kein Befund dieses Plans.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Alle 17 Zugriffe des Bereichs `user` sind entweder ueber `forTenant()` gebunden
|
|
oder gehoeren zu einer der vier benannten, im Code begruendeten Ausnahmen (Suche
|
|
nach Benutzername, Erstanlage-Pruefung beim Start, zwei Zugriffe auf die
|
|
Mandantentabelle). Es bleibt keine Fundstelle ohne Zuordnung.
|
|
- Die Linie ist je Methode gezogen und in beide Richtungen begruendet — kein Pfad
|
|
bindet, der nicht binden darf, und kein Pfad bleibt ungebunden, der binden muss.
|
|
- Die Kette aus unsichtbarer Zeile, falschem "frei" und hartem Eindeutigkeitsfehler
|
|
ist gemessen und nicht behauptet, samt der Unterscheidung zwischen einer
|
|
Eindeutigkeitsverletzung und einer Zeilenschutz-Ablehnung.
|
|
- Die Startsperre nach dem Scharfschalten ist im Anwendungscode entschaerft, ohne
|
|
dass irgendein anderer Startfehler seine abbrechende Wirkung verliert — und ohne
|
|
Schemaaenderung.
|
|
- Die uebergreifende Sicht der obersten Rolle bleibt in vollem Umfang erhalten und
|
|
laeuft dabei ueber gebundene Lesezugriffe je Mandant.
|
|
- Die heute wirksame Luecke im Selbstloesch-Riegel ist geschlossen, belegt durch
|
|
einen Test, der gegen die heutige Fassung rot war.
|
|
- Der Bereich hat in allen drei Dateien Tests, die auf eine vergessene Bindung rot
|
|
werden koennen — nachgewiesen durch probeweisen Rueckbau, nicht behauptet.
|
|
- Beide Dokumente sind fortgeschrieben, nicht umgeschrieben, und die vier
|
|
handgepflegten Stellen (Uebersichtszeile, Summenzeile, Klassen-Verteilung samt
|
|
Ueberschrift, Hintergrunddienst-Abschnitt samt Ueberschrift) sind mitgezogen.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/quick/260910-das-mandantentrennung-etappe-2-bereich-user-/260910-das-SUMMARY.md` when done
|
|
</output>
|
|
</content>
|
|
</invoke>
|