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
88 KiB
phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
| phase | plan | type | wave | depends_on | autonomous | requirements | files_modified | estimate | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260910-das | 01 | execute | 1 | true |
|
|
|
|
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.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_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.jsonfuehrttest(vitest run) undtype-check(tsc --noEmit) — die beiden Befehle, auf denen jede Pruefung dieses Plans aufsetzt. Es gibt keinenlint-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.tsexistiert, prueft aber ausschliesslich die Standardgruppen-Anbindung voncreate. Der Prisma-Ersatz ist ein nacktes Objekt mit genau einem Eintrag (user.create), es gibt KEINE Attrappe fuerforTenant— 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.tsexistiert, prueft die Reihenfolge beim Start ueber ein gemeinsames Aufruf-Protokoll, und hat ebenfalls KEINEforTenant-Attrappe — dieselbe zweite Form.user.controller.spec.tsexistiert 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:
usernameist plattformweit eindeutig, eine gebundene Suche saehe einen fremden Halter nicht und meldete "frei". Exakt derresolveEmailForWrite-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(Bereichldap, 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:
- Es liest die drei Umgebungswerte; fehlt einer, kehrt es zurueck.
- 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. - Findet es ihn, kehrt es zurueck.
- 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).
- Die Erstanlage-Pruefung beim Start (Befund I) — die schwerste.
nullheisst "der Administrator existiert nicht", die Folgehandlung ist Anlegen, das Anlegen kollidiert plattformweit, und der Start bricht ab. - 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. - Die Benutzerliste im SUPER_ADMIN-Zweig (Befund F) — dieselbe Leere, eine Ebene hoeher: die Plattformverwaltung sieht eine Installation ohne jeden Benutzer.
- 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. 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 inuser.service.ts, der in diesem Plan ohnehin angefasst wird — dort und nicht inldap.- 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>
Aufgabe 1: Die Kette messen — von der unsichtbaren Zeile ueber das falsche "frei" bis zum harten Eindeutigkeitsfehler — und die Kritikschrift fuer user schreiben 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). apps/api/scripts/rls-scratch-check.mjs, docs/mandantentrennung-etappe2-fehlerrichtung.md 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.
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)"
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.
Muster fuer beide Dateien, nach apps/api/src/groups/groups.service.spec.ts:
- Die Erweiterung
../prisma/prisma-tenant.extensionwird gemockt, sodassforTenant(prisma, tenantId)anprisma.__makeBoundClient(tenantId)delegiert.withTenantTransactionwird NICHT gemockt und nicht gebraucht — dieser Bereich hat keine Transaktion (Befund B). - Ein handgeschriebener Speicher mit Maps fuer
userundtenant.__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
usernamenach: 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:
createsteht 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:
createmit 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:
updatesteht gebunden im Protokoll, und ein Namenswechsel auf einen fremd gehaltenen Benutzernamen wirft dieselbe Art von Konfliktmeldung. - Test 4:
findByIdsteht gebunden im Protokoll und liefert einen Benutzer eines anderen Mandanten NICHT. - Test 5:
deactivateunddeletestehen 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.
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.
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)"
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.
- 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.
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
userder Uebersichtstabelle mit den bei der Ausfuehrung NEU gemessenen Zahlen fortschreiben, im Stil der bereits fortgeschriebenen Zeilen (war 17/0plus 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 NamentenantPrismagebundenen. 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 MethodetenantPrisma, wie inldap,groups,dkvundauth— 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/userwechselt wegen der einen bewusst ungebundenen Suche vonmuss-mandantengebundenaufbeides— wortgleich derselbe Praezedenzfall wieldap.service.ts/userin 260909-ipc; undadmin-seed.service.ts/userwechselt vonbewusst-uebergreifendaufbeides, weil die bisherige Begruendung nachweislich falsch war (Befund J).user.controller.ts/userendet aufgebunden,admin-seed.service.ts/tenantbleibt unveraendert. - Die NEUE Zeile fuer das Paar
(user.service.ts, tenant)ergaenzen, Klassekeine-mandantengebundene-tabelle, Standungebunden, mit der Begruendung, dass es der Schleifentreiber der Plattform-Administratorsicht ist und die Mandantentabelle keinen Zeilenschutz traegt (Aufgabe 1 gemessen). Ohne diese Zeile wirdrls-access-inventory.spec.tsrot (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.
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)"
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.
<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> |
<success_criteria>
- Alle 17 Zugriffe des Bereichs
usersind entweder ueberforTenant()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>