---
phase: quick-260909-jts
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [WINDOWS-20, ETAPPE-2-GROUPS]
files_modified:
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/prisma/prisma-tenant.extension.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/api/src/groups/groups.service.ts
- apps/api/src/groups/groups.service.spec.ts
- apps/api/src/groups/module-grants.service.ts
- apps/api/src/groups/module-grants.service.spec.ts
- docs/mandantentrennung-zugriffsklassifikation.md
estimate:
tokens: 110000
raw_tokens: 110000
tasks: 3
confidence: low
must_haves:
truths:
- "Jeder mandantengebundene Datenbankzugriff des Bereichs groups laeuft ueber einen gebundenen Client — einschliesslich der fuenf Zugriffe, die heute nur ueber den Rueckgabeparameter einer interaktiven Transaktion erreichbar sind und die von keiner Pruefung dieses Projekts je gesehen wurden."
- "Welche Transaktionsform den Mandantenkontext auf DERSELBEN Verbindung traegt, ist unter einer Rolle ohne BYPASSRLS gemessen, BEVOR die Umstellung der drei Transaktionsstellen darauf aufsetzt."
- "Die Uebergabe der Standardgruppe unmittelbar vor einer Gruppenloeschung (reassignDefaultBeforeDelete, danach ensureDefaultGroup) ist gebunden; Befund D aus der ldap-Kritik ist damit erledigt und als erledigt vermerkt."
- "Es existiert eine schriftliche Kritik fuer den Bereich groups, die je Pfad das konkrete Signal nennt und benennt, welcher Code Leere als Abwesenheit deutet — einschliesslich der einen Stelle, an der ein zu kleines Leseergebnis nicht zu wenig, sondern ZU VIEL bewirkt."
- "Die maschinelle Absicherung sieht Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion; ein dort fehlender Mandantenkontext kann nicht mehr unentdeckt bleiben."
- "Beide Testdateien des Bereichs koennen rot werden, wenn eine Fundstelle ungebunden bleibt — nachgewiesen ueber zwei unterscheidbare Clients, nicht behauptet."
- "Die Mitgliedschaftsanlage in der Standardgruppe prueft den Mandanten des Zielbenutzers; dass die Policy auf GroupMembership das NICHT tut, ist gemessen."
- "Klassifikationsdokument und maschinelle Absicherung zeigen fuer alle Paare des Bereichs denselben, gemessenen Stand `gebunden`."
- "719+ Tests und die Typpruefung sind gruen; Schema, Migrationen und alle vier Compose-Dateien sind unveraendert; der Schalter bleibt AUS."
artifacts:
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/prisma/prisma-tenant.extension.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/api/src/groups/groups.service.ts
- apps/api/src/groups/module-grants.service.ts
- docs/mandantentrennung-zugriffsklassifikation.md
key_links:
- "gebundener Client <-> Policies tenant_isolation_policy auf Group/GroupMembership/ModuleGrant/TenantModuleActivation (wortgleich aus den ausgelieferten Migrationen extrahiert, nicht im Werkzeug nachgetippt)"
- "interaktive Transaktion in ensureDefaultGroup <-> set_config auf derselben Verbindung — die eine Stelle, an der die Umstellung scheitern kann, ohne dass ein Test es merkt"
- "reassignDefaultBeforeDelete <-> Loeschzweig in ldap.service.ts — die Uebergabe, deren stilles false eine Gruppe ohne Standardnachfolger zuruecklaesst (Befund D)"
- "ensureDefaultGroup <-> seine vier Aufrufer (ldap.service.ts, tenant.service.ts, admin-seed.service.ts zweimal) — der Start darf nicht brechen"
- "rls-access-inventory.spec.ts <-> Stand-Spalte des Klassifikationsdokuments, jetzt auch fuer Zugriffe ueber den Transaktionsparameter"
---
Der Bereich `groups` (37 Rohtreffer in zwei Dateien, dazu fuenf bisher fuer jede
Pruefung unsichtbare Zugriffe) wird auf einen gebundenen Prisma-Client umgestellt —
als zweiter Bereich der Etappe 2 und als Reihenfolgebedingung fuer Etappe 4.
Zweck: Dieser Bereich IST die Berechtigungsschicht. Gruppenmitgliedschaft und
Modulfreigaben entscheiden, wer welches Modul sehen darf. Ein zu kleines
Leseergebnis fuehrt hier nicht nur zu einer leeren Liste, sondern an mindestens
vier Stellen zu einer Handlung: eine Gruppe wird ohne Standardnachfolger geloescht,
ein Loeschdialog meldet "keine Mitglieder, keine Freigaben" ueber eine volle Gruppe,
ein neuer Benutzer bekommt still keine Modulfreigabe — und an einer Stelle wird aus
zu wenig Lesen sogar zu viel Schreiben.
Ergebnis: die Kritikschrift bekommt einen `groups`-Abschnitt, das Messwerkzeug
bekommt die Policies dieses Bereichs UND die Antwort auf die einzige offene
Architekturfrage der Umstellung (welche Transaktionsform den Mandantenkontext
traegt), zwei Dienste sind umgestellt, eine latente Mitgliedschaftsluecke ist
geschlossen, die maschinelle Absicherung sieht erstmals Zugriffe ueber den
Transaktionsparameter, und das Klassifikationsdokument weist seinen neuen Stand
maschinell nach.
Aufgabe 1 fuehrt bewusst, obwohl sie keinen Nutzernutzen liefert: sie ist der
Durchstich durch die gesamte Kette (ausgelieferte Policy -> Rolle ohne BYPASSRLS ->
Bindungsmuster -> die drei Transaktionsformen, die dieser Bereich tatsaechlich
verwendet) und beantwortet die Frage, auf der die gesamte Umstellung ruht, mit einer
Messung statt mit einer Annahme. Erst danach wird Dienstcode angefasst.
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
@.planning/STATE.md
@docs/mandantentrennung-zugriffsklassifikation.md
@docs/mandantentrennung-etappe2-fehlerrichtung.md
@.planning/quick/260909-ipc-mandantentrennung-etappe-2-bereich-ldap-/260909-ipc-SUMMARY.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/ldap/ldap-config.service.ts
@apps/api/src/groups/groups.service.ts
@apps/api/src/groups/module-grants.service.ts
@apps/api/src/groups/groups.controller.ts
@apps/api/src/groups/module-grants.controller.ts
@apps/api/src/user/admin-seed.service.ts
@CLAUDE.md
Alles Folgende wurde am 2026-09-09 zur Planungszeit am lebenden Baum gemessen. Die
Zeilennummern aus dem Auftrag waren Hinweise zum Aufschlagen, keine
Aenderungsvollmacht — jede Fundstelle wurde einzeln angesehen.
**Ausgangsstand (gemessen, nicht zitiert):**
- `npm --prefix apps/api run test` -> 53 Dateien, **719 Tests**, gruen, 4,94 s.
- `npm --prefix apps/api run type-check` -> Rueckgabewert 0.
- `pnpm --filter @tessera/api exec vitest run src/groups` -> 84 Tests gruen
(42 in `groups.service.spec.ts`, 28 in `module-grants.service.spec.ts`,
14 in `migration-sql.spec.ts`).
- `npm --prefix apps/api run test -- src/groups/groups.service.spec.ts src/prisma/rls-access-inventory.spec.ts`
-> 51 Tests gruen (die Zielform der Aufgaben-Verifikation laeuft).
- Wegwerf-Werkzeug: `TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs`
-> "Alle 13 Pruefungen bestanden.", Rueckgabewert 0. Das Fundament ist damit
JETZT belegt, nicht laut Bericht. `docker inspect tessera-ctl-db-1` liefert
derzeit 172.19.0.2 — eine Container-Adresse ist veraenderlich und wird bei der
Ausfuehrung neu ermittelt.
**Die 37 Rohtreffer, einzeln aufgeschlagen — und warum 37 nicht die Zahl der
Fundstellen ist.** Die Bereichsuebersicht misst mit
`grep -ro "this\.prisma\.[a-zA-Z]*"`. Dieses Muster trifft auch
`this.prisma.$transaction`, weil `[a-zA-Z]*` auch null Zeichen erlaubt. Von den 37
Rohtreffern sind daher **drei gar keine Modellzugriffe**, sondern die drei
Transaktionsaufrufe in `groups.service.ts`. Tatsaechliche Modellzugriffe: 21 + 13 =
**34**. Dazu kommen **fuenf weitere**, die die Zaehlung ueberhaupt nicht sieht (siehe
Befund B). Die dokumentierte Bereichszahl 37 ist als Rohtrefferzahl korrekt, taugt
aber nicht als Arbeitsvorrat.
`apps/api/src/groups/groups.service.ts` — 21 Modellzugriffe in 12 Methoden, alle mit
am Aufrufort bereits bekanntem Mandanten:
| Methode | Modelle | Anmerkung |
|---|---|---|
| `listForTenant` | group | Filter auf `tenantId` vorhanden |
| `create` | group | schreibt `tenantId` |
| `findOwned` (privat) | group | Filter auf `id` UND `tenantId` — der Ownership-Schutz aller CRUD-Routen |
| `update` | group (3x, davon 2 in einer Array-Transaktion) | zwei der drei schreiben ueber `where: { id }` allein und verlassen sich auf das vorherige `findOwned` |
| `getImpact` | groupMembership, moduleGrant | zaehlt ueber `groupId` allein, ohne Mandantenfilter |
| `remove` | group | loescht ueber `id` allein, nach `findOwned` |
| `listMembers` | groupMembership | ueber `groupId` allein |
| `addMembers` | user, groupMembership | die Benutzerabfrage filtert auf `tenantId` |
| `removeMember` | groupMembership | ueber `groupId`/`userId`/`source` |
| `ensureDefaultGroup` | group (Zaehler) | plus die interaktive Transaktion, siehe Befund B |
| `reassignDefaultBeforeDelete` | group (5x, davon 2 in einer Array-Transaktion) | drei Lesezugriffe filtern auf `tenantId` |
| `addUserToDefaultGroup` | group, groupMembership | siehe Befund E |
`apps/api/src/groups/module-grants.service.ts` — 13 Modellzugriffe in 5 Methoden,
alle mit bekanntem Mandanten: `assertTargetBelongsToTenant` (group, user),
`grant` (tenantModuleActivation, moduleGrant 2x), `revoke` (moduleGrant),
`getMatrix` (tenantModuleActivation, group, moduleGrant),
`getUserAccess` (tenantModuleActivation, moduleGrant 2x, groupMembership).
**Befund A — die drei Transaktionen sind der eigentliche Kern dieser Umstellung, und
die Wirkung ihrer Bindung ist NICHT bekannt.** Der Kopfkommentar von
`apps/api/src/prisma/prisma-tenant.extension.ts` fuehrt genau diesen Fall als
ausdruecklichen Vorbehalt fuer Etappe 2: `$transaction` ist keine Modelloperation,
laeuft nicht durch `$allOperations` und bekommt daher keinen Mandantenkontext; die
darin enthaltenen Einzeloperationen wuerden jede ihre EIGENE Teiltransaktion
bekommen, was die Atomaritaet der aeusseren Transaktion verletzt. Der Kommentar
haelt fest, dass zum Zeitpunkt der ldap-Umstellung KEIN gebundener Aufrufer eine
eigene Transaktion hatte, und verlangt woertlich, das vor jedem neuen Fall in
Etappe 2 erneut zu pruefen. Dieser Bereich ist dieser Fall.
Gemessen (`grep -rn '\$transaction(' apps/api/src --include=*.ts | grep -v spec`):
im gesamten API-Quelltext gibt es vier Transaktionsaufrufe ausserhalb der Erweiterung
selbst. Drei davon liegen in `groups.service.ts` (zwei Array-Form in `update` und
`reassignDefaultBeforeDelete`, eine interaktive Callback-Form in
`ensureDefaultGroup`), der vierte in `tender-fingerprint-backfill.service.ts` auf der
plattformweiten `Tender`-Tabelle und damit ausserhalb jeder Mandantenbindung.
`groups.service.ts` ist ausserdem die EINZIGE Datei im gesamten API-Quelltext mit
einer interaktiven Callback-Transaktion. Die Frage, welche Form den Kontext traegt,
faellt also ausschliesslich hier an — und muss vor der Umstellung beantwortet sein,
nicht danach.
**Befund B — fuenf Modellzugriffe, die keine Pruefung dieses Projekts je gesehen
hat.** Innerhalb der interaktiven Transaktion in `ensureDefaultGroup` laufen fuenf
Zugriffe ueber den Rueckgabeparameter der Transaktion (`group.create`,
`user.findMany`, `groupMembership.createMany`, `tenantModuleActivation.findMany`,
`moduleGrant.createMany`). Weder die alte Erkennung ueber `this.prisma.` noch
die in 260909-ipc ergaenzte Erkennung gebundener Zugriffe sieht sie. Eine davon,
`tenantModuleActivation`, kommt in `groups.service.ts` NUR dort vor — das Paar
(`groups.service.ts`, `tenantModuleActivation`) fehlt deshalb bis heute vollstaendig
im Klassifikationsdokument. Und ausgerechnet `moduleGrant.createMany` an dieser
Stelle verteilt Modulfreigaben. Die Erkennungsluecke sitzt damit genau auf der
Schreibstelle mit der groessten Wirkung.
**Befund C — die Testdateien koennen die Umstellung nicht bemerken, aber anders als
bei ldap.** `grep -n "forTenant\|prisma-tenant\|vi.mock"` liefert in
`groups.service.spec.ts` und `module-grants.service.spec.ts` **null Treffer**. Es gibt
keinen Identitaets-Mock wie bei ldap — es gibt gar keinen. Beide Dateien uebergeben
einen handgeschriebenen In-Memory-Fake als Prisma-Ersatz. Nach der Umstellung liefe
`forTenant(this.prisma, tenantId)` gegen ein Objekt ohne `$extends` und JEDER Test
wuerde abstuerzen — rot aus dem falschen Grund, ohne irgendetwas zu beweisen. Die
Dateien brauchen einen Mock, der den gebundenen Client als ZWEITES, unterscheidbares
Objekt ueber DEMSELBEN Speicher liefert (Muster aus 260909-ipc), sonst ist "umgestellt"
wieder nur eine Behauptung. Der vorhandene Fake beherrscht bereits beide
Transaktionsformen und reicht sich selbst als Transaktionsparameter durch — er ist
wiederverwendbar, nicht wegzuwerfen.
**Befund D — `ensureDefaultGroup` ist NICHT der `beides`-Fall, den der Auftrag
vermutet.** Gemessen: die Methode hat VIER Aufrufstellen, nicht drei —
`ldap.service.ts` (nach dem Loeschzweig), `tenant.service.ts` (Mandantenanlage) und
`admin-seed.service.ts` ZWEIMAL (einmal in `seedAdmin` fuer den frisch angelegten
Vorgabe-Mandanten, einmal in der Startup-Reparatur-Schleife ueber alle Mandanten).
Alle vier uebergeben einen konkreten, bereits bekannten Mandanten. Die uebergreifende
Abfrage ist die Schleifenquelle `tenant.findMany` in `admin-seed.service.ts` — die
liegt ausserhalb dieses Bereichs und ist bereits als
`keine-mandantengebundene-tabelle` klassifiziert, weil `Tenant` per Definition keine
eigene `tenantId` hat. `ensureDefaultGroup` selbst ist damit eindeutig
mandantengebunden und MUSS binden. Es gibt hier keinen Konflikt zwischen Schleife und
Bindung; die eigentliche Gefahr fuer den Start ist eine andere, naemlich Befund A: die
Methode ist die interaktive Transaktion.
**Befund E — eine latente Mandantenluecke, die die Policy nicht auffaengt.**
`addUserToDefaultGroup(tenantId, userId)` sucht die Standardgruppe mandantengebunden,
legt danach aber die Mitgliedschaft an, ohne zu pruefen, dass der Zielbenutzer zu
diesem Mandanten gehoert. Die ausgelieferte Policy auf `GroupMembership`
(`20260804130918_groups_rls_policies`) prueft ausschliesslich die GRUPPENSEITE
(`groupId IN (SELECT id FROM "Group" WHERE tenantId = current_tenant_id())`) — die
Benutzerseite prueft sie nachweislich nicht. Der einzige heutige Aufrufer
(`user.service.ts`, direkt nach `user.create`) uebergibt einen frisch angelegten
Benutzer desselben Mandanten, die Luecke ist also heute nicht erreichbar; die Methode
ist aber aus `GroupsModule` exportiert und nimmt eine rohe Benutzerkennung entgegen.
Das Schwestermuster steht zwei Methoden hoeher: `addMembers` filtert seine
Benutzerliste ausdruecklich auf `tenantId` und ueberspringt fremde Kennungen. Dieselbe
Pruefung fehlt hier.
**Befund F — dieselbe Luecke eine Ebene hoeher: die ModuleGrant-Policy prueft die
referenzierte Gruppe nicht.** `CREATE POLICY tenant_isolation_policy ON "ModuleGrant"
USING ("tenantId" = current_tenant_id())` — eine Freigabezeile mit eigenem, korrektem
`tenantId`, die aber auf die Gruppe eines FREMDEN Mandanten zeigt, verletzt diese
Policy nicht. Der einzige Schutz davor ist die Anwendungspruefung
`assertTargetBelongsToTenant` in `module-grants.service.ts`. Das ist keine
Vermutung aus dem Policy-Text, sondern eine in Aufgabe 1 zu messende Tatsache, und
es ist der Grund, warum diese Anwendungspruefung bei der Umstellung nicht als
"macht jetzt ohnehin die Datenbank" wegfallen darf.
**Befund G — die offene Frage aus dem Auftrag zur plattformweiten Eindeutigkeit
faellt in diesem Bereich nicht an.** Gemessen mit
`grep -n "email\|username" apps/api/src/groups/*.ts`: der einzige Treffer ausserhalb
von Kommentaren ist eine Feldauswahl in `listMembers` (`select: { id, username,
displayName, email }`) — eine Projektion, keine Suche. Beide `user`-Zugriffe des
Bereichs (`addMembers`, `assertTargetBelongsToTenant`) suchen ueber Kennung UND
Mandant. Die Falle aus Befund A des ldap-Durchlaufs (`resolveEmailForWrite`,
plattformweite Eindeutigkeit von `email`/`username`) existiert hier nicht. Beide
`user`-Fundstellen binden.
**Befund H — Fremdzugriff ueber die Kennung allein: in diesem Bereich nicht
vorhanden.** Beide Steuerungen (`groups.controller.ts`, `module-grants.controller.ts`)
holen den Mandanten ausschliesslich aus dem Sitzungsnachweis und weisen ohne ihn mit
403 ab; jede Route reicht ihn an den Dienst weiter. Die Luecke, die der ldap-Durchlauf
gefunden hat (Loeschen ueber die Kennung allein), gibt es hier nicht. Die einzige
Luecke dieser Klasse ist Befund E, und sie sitzt nicht in der Steuerung, sondern in
einer aus dem Modul exportierten Dienstmethode.
**Befund I — welcher Code Leere als Abwesenheit deutet (Vorarbeit fuer Aufgabe 1,
dort auszuformulieren und zu ergaenzen, nicht abzuschreiben):**
`reassignDefaultBeforeDelete` (zweimal: kein Treffer fuer die Gruppe, kein
Ersatzkandidat — beide Male stilles `false`, und der Aufrufer loescht danach
trotzdem), `getImpact` (0/0 vor einer kaskadierenden Loeschung),
`addUserToDefaultGroup` (stilles `return` ohne Standardgruppe),
`ensureDefaultGroup` (der Zaehler ist UMGEKEHRT gepolt: null gelesen heisst hier
nicht "nichts tun", sondern "alles neu anlegen"). Gegenbeispiele in die andere
Richtung, ebenfalls festzuhalten: die Aktivierungspruefung in `grant` und
`assertTargetBelongsToTenant` werfen bei Leere LAUT.
Zur Frage, ob eine leere Freigabe-Matrix zu einem Massen-Entzug fuehren kann:
gemessen in `apps/web/src/app/(portal)/admin/modules/grants/page.tsx` — die Matrix
schaltet je Zelle einzeln (POST bzw. DELETE pro Klick), es gibt keinen
Sammel-Speichern-Knopf, der einen Abgleich gegen den gelesenen Zustand faehrt. Ein zu
kleines Leseergebnis fuehrt dort also zu einer leeren Anzeige, nicht zu einem
Massen-Entzug. Das ist der Unterschied zum `deleteMany`-mit-`notIn` des
ldap-Bereichs und gehoert als Entlastung in die Kritikschrift.
**Gewaehltes Muster (bewusst, nicht stillschweigend):** Der Mandantenkontext wird
weiterhin IM DIENST erzeugt, wie im Bereich `ldap` und in `auth.service.ts`. Der
offene Befund `req.tenantPrisma` (gesetzt in `tenant.middleware.ts` und
`tenant.guard.ts`, nirgends gelesen) wird auch von diesem Durchlauf AUSDRUECKLICH
NICHT entschieden.
**Nicht angefasst:** `prisma/schema.prisma`, `prisma/migrations/`, alle vier
Compose-Dateien, `.env`. `DATABASE_URL` bleibt auf der Rolle `tessera` mit
BYPASSRLS — das Scharfschalten ist Etappe 4. Am Verzeichnis (AD) wird nichts
geaendert; der Dienstzugang ist auslegungsgemaess nur lesend.
Aufgabe 1: Fehlerrichtung fuer groups schreiben und die Transaktionsfrage messen
Der Container `tessera-ctl-db-1` laeuft und ist erreichbar; seine Adresse per `docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}'` NEU ermitteln (zur Planungszeit 172.19.0.2 — eine Container-Adresse ist veraenderlich und darf nicht aus dem 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
Dienstcode in dieser Aufgabe.
TEIL 1, `apps/api/scripts/rls-scratch-check.mjs`: einen vierten Abschnitt
`runGroupsAreaChecks(adminUrl, scratchRoleUrl, results)` nach dem Vorbild des
vorhandenen `runLdapAreaChecks` ergaenzen und in `main()` nach diesem aufrufen.
Die Policies werden NICHT im Werkzeug neu getippt. Sie kommen aus zwei
ausgelieferten Migrationen: `Group`, `GroupMembership` und `ModuleGrant` aus dem
Verzeichnis, das auf `_groups_rls_policies` endet, `TenantModuleActivation` aus dem
Verzeichnis, das auf `_rls_remaining_tenant_tables` endet. Das vorhandene
`extractPolicySql` kann beide bedienen; `readRlsPoliciesMigrationSql` schliesst die
Groups-Migration heute ausdruecklich aus und braucht deshalb ein zweites, eigenes
Lesehilfsmittel statt einer Aenderung am bestehenden. Findet die Extraktion eine der
vier Anweisungen nicht, meldet der Abschnitt eine FEHLGESCHLAGENE Pruefung
`groups-policies-aus-migration-gefunden` und bricht ab — das Werkzeug darf nicht
still weitermessen, wenn es nichts zu messen gefunden hat.
Der Abschnitt legt in der Wegwerf-Datenbank schlanke Tabellen an, die genau die
Spalten tragen, die die vier Policies und die Messungen brauchen: `Group`
(id, tenantId, name, isDefault), `GroupMembership` (id, groupId, userId, source),
`ModuleGrant` (id, tenantId, moduleId, groupId, userId) und
`TenantModuleActivation` (id, tenantId, moduleId, isActive). Danach ENABLE plus
FORCE ROW LEVEL SECURITY, die vier extrahierten Policies, die Rechtevergabe an die
Wegwerf-Rolle und je Mandant (TENANT-A, TENANT-B) eine Gruppe, eine Mitgliedschaft,
eine Freigabe und eine Aktivierung.
Gemessen werden unter der Rolle ohne BYPASSRLS, ueber das vorhandene
`forTenantQuery`-Hilfsmittel, diese Verhaltensweisen mit diesen Kennungen:
- `group-gebunden-nur-eigene-zeile` — der gebundene SELECT unter TENANT-A liefert
genau die Gruppe von A und keine von B.
- `group-ungebunden-null-zeilen` — DERSELBE SELECT ohne vorheriges Setzen des
Kontexts liefert null Zeilen. Das ist die Belegzeile, die die Kritikschrift traegt.
- `groupmembership-folgt-join-auf-group` — gebunden unter TENANT-A ist genau die
Mitgliedschaft sichtbar, die an A's Gruppe haengt.
- `groupmembership-schreiben-fremde-gruppe-abgelehnt` — ein gebundenes INSERT unter
TENANT-A mit der Gruppenkennung von B wird abgewiesen; die Abweisung ist das
bestandene Ergebnis.
- `groupmembership-schreiben-fremder-benutzer-nicht-verhindert` — ein gebundenes
INSERT unter TENANT-A mit A's Gruppe, aber einer Benutzerkennung, die es in A
nicht gibt, GELINGT. Diese Pruefung gilt als bestanden, wenn das INSERT
durchgeht: sie belegt Befund E, naemlich dass die Policy die Benutzerseite nicht
prueft und die Anwendung sie pruefen muss. Der Meldetext dieser Pruefung sagt das
ausdruecklich, damit eine bestandene Pruefung nicht mit "ist abgesichert"
verwechselt wird.
- `modulegrant-gebunden-nur-eigene-zeile` — wie bei `Group`.
- `modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt` — ein
gebundenes INSERT unter TENANT-A mit korrekter eigener Mandantenkennung, aber der
Gruppenkennung von B, GELINGT. Auch hier ist das Durchgehen das bestandene
Ergebnis und der Meldetext benennt die Konsequenz: `assertTargetBelongsToTenant`
ist der einzige Schutz und darf bei der Umstellung nicht entfallen (Befund F).
- `tenantmoduleactivation-gebunden-nur-eigene-zeile` — wie bei `Group`.
TEIL 2, dieselbe Datei, die Transaktionsmessung — der eigentliche Grund, warum diese
Aufgabe vor jedem Dienstcode steht. Gemessen wird an einem Client, der WORTGLEICH die
Erweiterungsform aus `apps/api/src/prisma/prisma-tenant.extension.ts` nachbaut
(`$extends` mit `$allOperations`, darin die Array-Form der Transaktion aus
Kontextsetzung und eigentlicher Abfrage) — nicht ueber das vereinfachte
`forTenantQuery`, denn genau die Erweiterungsschicht ist hier der Gegenstand.
Drei Formen werden beobachtet, jeweils mit `pg_backend_pid()` UND
`current_tenant_id()` in jeder Teilabfrage plus einem echten Lesezugriff auf
`"Group"`, damit sichtbar wird, ob die Policy die Zeile durchlaesst:
(i) die Array-Form auf dem gebundenen Client — das, was `update` und
`reassignDefaultBeforeDelete` nach einer naiven Umstellung waeren;
(ii) die interaktive Callback-Form auf dem gebundenen Client — das, was
`ensureDefaultGroup` nach einer naiven Umstellung waere;
(iii) die interaktive Callback-Form auf dem UNgebundenen Client, bei der die
Kontextsetzung als erste Anweisung auf dem Transaktionsparameter selbst laeuft und
danach jede weitere Anweisung ebenfalls auf ihm — der Kandidat fuer ein Hilfsmittel,
das mehrschrittige Transaktionen traegt.
Jede der drei Formen wird ueber eine eigene Meldefunktion `beobachte(...)`
ausgegeben, die NICHT in die Pruefliste einfliesst und den Rueckgabewert nicht
beeinflusst: sie druckt je Form entweder die beobachteten Werte (Verbindungskennung
je Teilschritt, gelesener Mandantenkontext, Zeilenzahl) oder, falls die Form
ueberhaupt nicht laeuft, den vollstaendigen Fehlertext. Eine Form, die abbricht, ist
ein Messergebnis und kein Werkzeugfehler.
Darauf gesetzt wird GENAU EINE echte Pruefung:
`mindestens-eine-transaktionsform-traegt-den-mandantenkontext`. Sie gilt als
bestanden, wenn mindestens eine der drei Formen alle drei Bedingungen erfuellt —
gleiche Verbindungskennung ueber alle Teilschritte, gelesener Mandantenkontext gleich
TENANT-A, und der Lesezugriff liefert genau die Zeile von A. Ihr Meldetext nennt
NAMENTLICH, welche Formen bestanden haben und welche nicht. Das Ergebnis dieser
Pruefung ist die Entscheidungsgrundlage fuer Aufgabe 2; ohne sie gaebe es dort nur
eine Annahme.
Das Werkzeug raeumt weiterhin ausschliesslich seine fest verdrahtete
Wegwerf-Datenbank ab und bekommt keine steuerbaren Namen (T-EOR-07 bleibt gueltig).
TEIL 3, `docs/mandantentrennung-etappe2-fehlerrichtung.md` um einen Abschnitt
`## Bereich groups` 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 `groups` zum Zeitpunkt
seiner Umstellung beschreibt (Quick-Task 260909-jts).
Inhalt, in ganzen Saetzen auf Deutsch:
(g1) Die Messung aus Teil 1 und Teil 2 mit den TATSAECHLICH beobachteten Zeilen als
Beleg — nicht mit erwarteten. Insbesondere die Belegzeile
`group-ungebunden-null-zeilen` und das namentliche Ergebnis der
Transaktionsmessung.
(g2) Eine Signaltabelle je umzustellendem Pfad mit den Spalten Pfad, Verhalten bei zu
wenig Ergebnis, konkretes Signal. Mindestens diese Zeilen, jeweils mit dem Ort, an
dem man es merkt: die Gruppenliste in der Verwaltung (Mitgliederzahl je Zeile), der
Loeschdialog mit seinen zwei Zahlen, die Mitgliederliste im Gruppen-Detail, die
Freigabe-Matrix (Module- und Gruppenachse), das Benutzer-Detail mit seinen zwei
unabhaengigen Antworten, der Zaehler `defaultMarkerMoved` im Abgleich-Bericht des
Verzeichnis-Syncs, und die Modulkacheln, die ein frisch angelegter Benutzer nach
seiner ersten Anmeldung sieht.
(g3) Ein eigener, hervorgehobener Abschnitt "Welcher Code deutet Leere als
Abwesenheit", mit je Stelle der Richtung der Gefahr. Die Vorarbeit aus Befund I
dieses Plans ist der Ausgangspunkt und ausdruecklich NICHT die vollstaendige Liste —
beide Dateien werden dafuer noch einmal durchgesehen, und was dabei zusaetzlich
auffaellt, kommt dazu. Vier Punkte muessen darin auf jeden Fall vorkommen:
- `reassignDefaultBeforeDelete` — zerstoerend und still. Zwei getrennte Stellen
liefern `false`: die Gruppe selbst ist nicht sichtbar, oder es ist kein
Ersatzkandidat sichtbar. Der Aufrufer im Verzeichnis-Sync loescht die Gruppe
danach in beiden Faellen trotzdem, und die Loeschung nimmt ueber die
Kaskadenregeln Mitgliedschaften und Modulfreigaben mit. Das ist der Befund D aus
der ldap-Kritik, und ihn zu schliessen ist ein Hauptzweck dieses Durchlaufs.
- `ensureDefaultGroup` — die einzige Stelle des Bereichs, an der zu wenig Lesen zu
ZU VIEL Schreiben fuehrt. Der Waechter ist umgekehrt gepolt: null gelesene
Gruppen heisst nicht "nichts zu tun", sondern "alles neu aufbauen". Bliebe der
Zaehler ungebunden waehrend der Schreibteil gebunden liefe, legte die Methode
fuer einen Mandanten, der bereits Gruppen hat, eine zweite Standardgruppe an,
naehme alle seine Benutzer hinein und verteilte Freigaben fuer alle aktiven
Module — eine stille Ausweitung von Berechtigungen, ausgeloest durch ein zu
kleines Leseergebnis. Der partielle Eindeutigkeitsindex faengt einen Teil der
Faelle ab und liefert dann `null`; die Faelle, in denen der Mandant Gruppen, aber
keine markierte Standardgruppe hat, faengt er nicht ab. Genau deshalb muessen
Zaehler und Transaktion gemeinsam gebunden werden, nie einzeln.
- `getImpact` — die Zahlen des Loeschdialogs. Zwei Zaehlungen ohne Mandantenfilter,
die bei Leere 0 und 0 melden. Der Administrator entscheidet auf dieser Grundlage
ueber eine kaskadierende Loeschung und bekommt "keine Mitglieder, keine
Freigaben" fuer eine volle Gruppe angezeigt.
- `addUserToDefaultGroup` — stilles Zurueckkehren ohne sichtbare Standardgruppe.
Jeder neu angelegte Benutzer landet dann in keiner Gruppe und sieht nach seiner
ersten Anmeldung kein einziges Modul. Nicht zerstoerend, aber lautlos und in der
Wirkung ein Berechtigungsverlust.
Zusaetzlich die Gegenrichtung festhalten: die Aktivierungspruefung beim Erteilen
einer Freigabe und die Mandanten-Gegenpruefung vor jedem Erteilen werfen bei Leere
LAUT und sind damit die harmlosen Stellen des Bereichs. Und die Entlastung: die
Freigabe-Matrix schaltet je Zelle einzeln, es gibt keinen Sammel-Abgleich gegen den
gelesenen Zustand — ein zu kleines Leseergebnis fuehrt dort zu einer leeren
Anzeige, nicht zu einem Massen-Entzug. Das ist ausdruecklich am Frontend
nachgesehen und nicht aus dem Backend geschlossen.
(g4) Ein Abschnitt "Was dieser Durchlauf bewusst nicht loest" mit: der offenen Frage
`req.tenantPrisma`, die auch dieser Bereich nicht entscheidet; und der Feststellung,
dass die Policies auf `GroupMembership` und `ModuleGrant` die jeweils zweite
Referenz (Benutzerseite bzw. Gruppenseite) nachweislich nicht pruefen — die
Anwendungspruefungen bleiben deshalb der primaere Schutz und werden nicht durch die
Datenbank ersetzt.
(g5) Ein Satz zur Fortschreibung des ldap-Abschnitts: der dort als offen gefuehrte
Befund D wird durch diesen Durchlauf geschlossen. Der Vermerk selbst wird in
Aufgabe 3 gesetzt, wenn die Schliessung tatsaechlich vorliegt — nicht hier auf
Vorrat.
set -o pipefail && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}'):5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | tee "${TMPDIR:-/tmp}/rls-groups-check.log" && grep -q "group-ungebunden-null-zeilen: bestanden" "${TMPDIR:-/tmp}/rls-groups-check.log" && grep -q "groupmembership-folgt-join-auf-group: bestanden" "${TMPDIR:-/tmp}/rls-groups-check.log" && grep -q "modulegrant-fremde-gruppe-trotz-eigener-mandantenkennung-erlaubt: bestanden" "${TMPDIR:-/tmp}/rls-groups-check.log" && grep -q "mindestens-eine-transaktionsform-traegt-den-mandantenkontext: bestanden" "${TMPDIR:-/tmp}/rls-groups-check.log" && grep -q "^## Bereich groups" docs/mandantentrennung-etappe2-fehlerrichtung.md && npm --prefix apps/api run test && npm --prefix apps/api run type-check
Das Werkzeug meldet alle Pruefungen bestanden und beendet sich mit 0 — die 13 aus den Vorlaeufern plus die neuen dieses Bereichs. Die Ausgabe nennt namentlich, welche Transaktionsform den Mandantenkontext auf derselben Verbindung traegt und welche nicht. Der Abschnitt `## Bereich groups` in der Kritikschrift existiert, traegt die tatsaechlich beobachteten Werte (nicht erwartete), nennt je Pfad ein konkretes Signal und enthaelt die vier Pflichtpunkte einschliesslich der Stelle, an der zu wenig Lesen zu viel Schreiben ausloest. Die 719 Tests und die Typpruefung sind unveraendert gruen. Kein Dienstcode wurde angefasst.
Aufgabe 2: groups.service.ts binden, die Transaktionen tragfaehig machen, die Absicherung sehend machen
apps/api/src/prisma/prisma-tenant.extension.ts, apps/api/src/prisma/rls-access-inventory.spec.ts, apps/api/src/groups/groups.service.spec.ts, apps/api/src/groups/groups.service.ts, docs/mandantentrennung-zugriffsklassifikation.md
- Jede der zwoelf Methoden von `GroupsService` erzeugt ihren Mandantenkontext aus dem uebergebenen Mandanten und fuehrt ihre Abfragen darauf aus; je Methode weist ein Test nach, dass der Kontext mit genau diesem Mandanten erzeugt wurde.
- Die beiden mehrschrittigen Aenderungen (Standardmarkierung umsetzen; Standardmarkierung vor einer Loeschung verschieben) laufen weiterhin als EINE Transaktion und tragen dabei den Mandantenkontext; ein Test weist nach, dass beide Teilschritte am gebundenen Client landen.
- Der Aufbau einer Standardgruppe laeuft weiterhin als EINE Transaktion ueber alle vier Schritte (Gruppe, Mitgliedschaften, Aktivierungen lesen, Freigaben) und traegt dabei den Mandantenkontext; ein Test weist nach, dass auch die Zaehlung davor am gebundenen Client landet — Zaehler und Schreibteil duerfen nie unterschiedlich gebunden sein.
- Der Aufbau einer Standardgruppe bleibt fuer alle vier Aufrufwege unveraendert wirksam: Mandantenanlage, Startanlage des Vorgabe-Mandanten, Startreparatur je Mandant in der Schleife, und der Aufruf nach dem Loeschzweig des Verzeichnis-Abgleichs. Die vorhandenen Tests dieser Wege bleiben gruen.
- Das Verschieben der Standardmarkierung vor einer Loeschung findet den Ersatzkandidaten weiterhin deterministisch (bevorzugt die gleichnamige Standardgruppe, sonst die aelteste andere) und meldet `false` ausschliesslich dann, wenn es tatsaechlich keinen gibt.
- Die Mitgliedschaftsanlage in der Standardgruppe nimmt einen Zielbenutzer nur auf, wenn er zum selben Mandanten gehoert; ein Test mit einem fremden Benutzer weist nach, dass keine Mitgliedschaft entsteht und die Methode nicht wirft.
- Alle 42 Bestandstests der Datei bleiben gruen, insbesondere die Uebersetzung der Eindeutigkeits- und Nichtgefunden-Fehlercodes, die Namenssperre fuer importierte Gruppen und die Beschraenkung des Mitglieder-Entfernens auf manuelle Mitgliedschaften.
- Die Bestandsaufnahme-Pruefung sieht Modellzugriffe ueber den Rueckgabeparameter einer interaktiven Transaktion und ordnet sie gebunden oder ungebunden zu.
Reihenfolge: erst das Fundament aus der Messung, dann die Absicherung, dann die
Tests, dann die Umstellung. Fundstellen werden ueber ihren Inhalt aufgesucht, nicht
ueber Zeilennummern — die Datei verschiebt sich waehrend ihrer eigenen Bearbeitung.
SCHRITT 1, das Fundament, `apps/api/src/prisma/prisma-tenant.extension.ts`.
Die Entscheidung faellt aus dem Ergebnis der Pruefung
`mindestens-eine-transaktionsform-traegt-den-mandantenkontext` aus Aufgabe 1, nicht
aus einer Vermutung:
- Traegt die Array-Form auf dem gebundenen Client den Kontext, bleiben die beiden
mehrschrittigen Aenderungen bei ihrer heutigen Form und laufen einfach auf dem
gebundenen Client. Kein neues Hilfsmittel noetig.
- Traegt die interaktive Form auf dem gebundenen Client den Kontext, gilt dasselbe
fuer den Aufbau der Standardgruppe.
- Traegt eine der beiden Formen ihn NICHT, bekommt diese Datei ein zweites,
ausgeschriebenes Hilfsmittel `withTenantTransaction(prisma, tenantId, fn)`: es
oeffnet eine interaktive Transaktion auf dem UNgebundenen Client, setzt als
erste Anweisung den Mandantenkontext ueber ein getaggtes Roh-Template auf dem
Transaktionsparameter selbst (parametrisiert, nie zusammengebauter Text — die
Injektionsfestigkeit aus T-02-05 bleibt erhalten) und reicht denselben
Transaktionsparameter an `fn` weiter, sodass jede Folgeanweisung auf derselben
Verbindung laeuft. Ueber der Funktion steht ein Absatz, der die in Aufgabe 1
beobachteten Werte nennt und daraus begruendet, warum es sie gibt.
In JEDEM dieser Faelle wird der Vorbehalt im Kopfkommentar der Datei
fortgeschrieben: er sagt heute, zum Zeitpunkt der ldap-Umstellung habe kein
gebundener Aufrufer eine eigene Transaktion gehabt, und verlangt eine erneute
Pruefung vor jedem neuen Fall. Diese Pruefung hat jetzt stattgefunden; ihr
Ergebnis gehoert an genau diese Stelle, damit der naechste Bereich nicht wieder
bei null anfaengt.
SCHRITT 2, die Absicherung sehend machen, `apps/api/src/prisma/rls-access-inventory.spec.ts`
(Befund B). Die Fundstellensuche bekommt eine dritte Erkennung fuer Modellzugriffe
ueber den Rueckgabeparameter einer interaktiven Transaktion. Je Datei werden die
Callback-Parameternamen solcher Transaktionen eingesammelt und danach ihre
`.`-Vorkommen gesucht. Die Zuordnung richtet sich nach dem
Empfaenger der Transaktion: laeuft sie auf einem Namen, der aus einer erkannten
Bindungszuweisung stammt, oder ueber das in Schritt 1 gegebenenfalls ergaenzte
Hilfsmittel, zaehlen die Zugriffe als gebunden; laeuft sie auf dem ungebundenen
Client, zaehlen sie als ungebunden. Die vorhandene Kommentarfilterung gilt
unveraendert auch fuer diese Erkennung.
Die Erkennung bekommt dieselbe offen gehaltene Grenze wie die zweite: jede
interaktive Transaktion im Quelltext muss einer der erkannten Empfaengerformen
entsprechen oder in einer kurzen, begruendeten Ausnahmeliste stehen — sonst schlaegt
eine eigene Pruefung fehl. Gemessen zur Planungszeit gibt es im gesamten
API-Quelltext genau eine interaktive Transaktion, und sie steht in der Datei, die
diese Aufgabe umstellt; die Ausnahmeliste startet deshalb leer. Die
Fehlermeldung nennt je Verstoss Datei und Anzahl, damit sie ohne Ratespiel behebbar
ist.
Diese Erweiterung wird die Paarmenge des Quelltexts vergroessern: mindestens das
Paar aus dieser Datei und dem Modell der Mandanten-Modulaktivierungen wird erstmals
sichtbar und fehlt heute im Klassifikationsdokument. Wie viele Paare es am Ende
sind, wird der Ausgabe der fehlschlagenden Pruefung entnommen, nicht geschaetzt.
SCHRITT 3, die Tests scharf machen, `apps/api/src/groups/groups.service.spec.ts`
(Befund C). Die Datei hat heute keinerlei Ersatz fuer die Kontextbindung; nach der
Umstellung wuerde jeder Test am fehlenden Erweiterungsaufruf abstuerzen — rot aus dem
falschen Grund. Sie bekommt deshalb einen Ersatz nach dem in 260909-ipc etablierten
Muster mit ZWEI unterscheidbaren Clients: der ungebundene Ersatz ist der vorhandene
handgeschriebene Speicher-Fake, der gebundene ist ein davon unterscheidbares Objekt,
das auf DENSELBEN Speicher zugreift und mitschreibt, welche Aufrufe ueber ihn
liefen. Nur so bleiben die 42 Bestandstests aussagefaehig UND ein vergessener
Bindungsaufruf faellt auf. Der Fake beherrscht bereits beide Transaktionsformen und
reicht sich selbst als Transaktionsparameter durch — er wird erweitert, nicht
ersetzt.
Darauf die in `` beschriebenen Erwartungen als Tests schreiben, je Methode
mindestens einen Bindungsnachweis, dazu die drei Transaktionsnachweise und den
Nachweis fuer den fremden Zielbenutzer. Diese Tests laufen zunaechst rot. Ein Test,
der auch bei einer weggelassenen Bindung gruen bliebe, ist kein Nachweis und wird
umgeschrieben, bis er es ist.
SCHRITT 4, `apps/api/src/groups/groups.service.ts` umstellen. Alle 21
Modellzugriffe und die fuenf Zugriffe innerhalb der Transaktion des
Standardgruppen-Aufbaus laufen danach ueber den Mandantenkontext des jeweils
uebergebenen Mandanten. Je Methode wird der Kontext einmal am Methodenkopf erzeugt,
in der Schreibweise der Bestandsstellen aus `ldap.service.ts` und
`ldap-config.service.ts`, damit die Typpruefung gruen bleibt und die
Fundstellenerkennung aus Schritt 2 sie je Datei zuordnen kann. Gebundene Clients
werden NICHT zwischen Methoden weitergereicht.
Drei Stellen brauchen besondere Aufmerksamkeit:
- Der Aufbau der Standardgruppe: die Zaehlung davor und die Transaktion danach
muessen GEMEINSAM gebunden sein. Eine halb umgestellte Fassung ist gefaehrlicher
als die heutige, weil sie aus einem zu kleinen Leseergebnis eine zusaetzliche
Standardgruppe samt Modulfreigaben erzeugen wuerde — die Stelle, an der zu wenig
Lesen zu viel Schreiben ausloest. Das Abfangen des Eindeutigkeitsfehlers und
seine Uebersetzung in "nichts zu tun" bleiben unveraendert.
- Das Verschieben der Standardmarkierung vor einer Loeschung: die drei
Lesezugriffe und die zweischrittige Aenderung gehoeren an denselben gebundenen
Client. Das bewusste Weglassen der werfenden Ownership-Pruefung bleibt
unveraendert — eine fremde oder nicht markierte Gruppe ist weiterhin ein
folgenloses Nichttun mit Rueckgabe `false` und darf einen Abgleichlauf nicht
abbrechen.
- Die Mitgliedschaftsanlage in der Standardgruppe: hier kommt die fehlende
Mandantenpruefung des Zielbenutzers dazu (Befund E, T-JTS-02), nach dem Vorbild
der Methode zum manuellen Hinzufuegen von Mitgliedern zwei Methoden hoeher —
Benutzer laden, auf den Mandanten filtern, bei keinem Treffer folgenlos
zurueckkehren statt zu werfen. Warum das noetig ist, obwohl die Datenbank eine
Regel auf dieser Tabelle hat, steht als ausgeschriebener Absatz an der Stelle:
die ausgelieferte Regel prueft die Gruppenseite und nicht die Benutzerseite, und
das ist in Aufgabe 1 gemessen.
Die vorhandenen Filter auf den Mandanten bleiben ueberall stehen. Sie sind das erste
Netz, die Regel in der Datenbank das zweite — an keiner Stelle wird ein
Anwendungsfilter mit der Begruendung entfernt, die Datenbank erledige das jetzt.
SCHRITT 5, `docs/mandantentrennung-zugriffsklassifikation.md` fuer diese Datei
nachziehen: die vier vorhandenen Zeilen bekommen ihren gemessenen Stand, das durch
Schritt 2 neu sichtbar gewordene Paar bekommt eine eigene Zeile mit Klasse,
gemessenem Stand und einer Begruendung, die sagt, warum es bis heute unsichtbar war.
Die Klassen-Verteilung wird nachgerechnet, nicht fortgeschrieben. Die Quelle fuer
alle eingetragenen Stand-Werte ist die Ausgabe der Pruefung aus Schritt 2, nicht eine
Schaetzung.
npm --prefix apps/api run test -- src/groups/groups.service.spec.ts src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run type-check && npm --prefix apps/api run test
Die 42 Bestandstests der Datei sind weiterhin gruen, dazu die neuen Bindungs- und Transaktionsnachweise und der Nachweis fuer den fremden Zielbenutzer. Die Bestandsaufnahme-Pruefung sieht Zugriffe ueber den Transaktionsparameter, prueft die Stand-Spalte gegen den Quelltext und ist gruen — mit ergaenztem Dokument, nicht mit geloeschten Zeilen. Der gesamte Testlauf zeigt mindestens 719 Tests gruen, die Typpruefung liefert 0.
Reine Dienst-, Test- und Werkzeugaenderung ohne Schema-, Migrations- oder Konfigurationsanteil; ein einzelner Commit laesst sich zuruecknehmen.
Aufgabe 3: module-grants.service.ts binden und beide Dokumente schliessen
apps/api/src/groups/module-grants.service.spec.ts, apps/api/src/groups/module-grants.service.ts, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md
- Alle fuenf Methoden von `ModuleGrantsService` erzeugen ihren Mandantenkontext aus dem uebergebenen Mandanten und fuehren ihre Abfragen darauf aus; je Methode weist ein Test nach, dass der Kontext mit genau diesem Mandanten erzeugt wurde.
- Die Mandanten-Gegenpruefung vor jedem Erteilen bleibt bestehen und bleibt wirksam: eine Gruppe oder ein Benutzer eines fremden Mandanten fuehrt weiterhin zu einer Nichtgefunden-Antwort, und ein Test haelt fest, dass diese Pruefung nicht durch die Datenbankregel ersetzt wurde.
- Die Entweder-oder-Regel, die Aktivierungspruefung, das Abfangen des Eindeutigkeitsfehlers beim Doppelklick und die Protokollzeile je erfolgreicher Aenderung bleiben unveraendert.
- Die beiden Datenlieferungen (Freigabe-Matrix, Benutzer-Detail) behalten Form und Sortierung exakt bei; der Anzeigename faellt weiterhin per Nullish auf den Gruppennamen zurueck.
- Alle 28 Bestandstests der Datei bleiben gruen.
SCHRITT 1, Tests zuerst, `apps/api/src/groups/module-grants.service.spec.ts`. Wie in
Aufgabe 2: die Datei hat heute keinen Ersatz fuer die Kontextbindung (Befund C) und
bekommt denselben Aufbau mit zwei unterscheidbaren Clients ueber demselben
Speicher-Fake. Darauf je Methode ein Bindungsnachweis sowie der Nachweis, dass die
Mandanten-Gegenpruefung vor dem Erteilen erhalten geblieben ist. Diese Tests laufen
zunaechst rot.
SCHRITT 2, `apps/api/src/groups/module-grants.service.ts` umstellen. Alle 13
Modellzugriffe in fuenf Methoden laufen danach ueber den Mandantenkontext des
uebergebenen Mandanten; je Methode wird er einmal am Methodenkopf erzeugt. Bei den
beiden Datenlieferungen bedeutet das, dass alle parallel abgesetzten Teilabfragen
denselben gebundenen Client benutzen — es entsteht kein zweiter.
Der Kommentarblock ueber der Mitgliedschaftsabfrage im Benutzer-Detail, der heute
begruendet, warum an dieser Stelle KEIN Mandantenkontext erzeugt wird, ist nach der
Umstellung falsch und wird durch einen Absatz ersetzt, der den neuen Stand
beschreibt: der Kontext wird gesetzt, der Filter ueber die Beziehung zur Gruppe
bleibt zusaetzlich stehen, und die Regel auf dieser Tabelle bezieht ihre Sichtbarkeit
ueber die Gruppenseite. Ein stehen gebliebener alter Kommentar waere die naechste
stille Falle: er wuerde den naechsten Leser ueber den tatsaechlichen Zustand
taeuschen.
Die Mandanten-Gegenpruefung vor jedem Erteilen bleibt ausdruecklich erhalten und
bekommt einen Absatz mit dem in Aufgabe 1 gemessenen Grund: die Regel auf der
Freigabetabelle prueft ausschliesslich die Mandantenkennung der Zeile selbst und
nicht die referenzierte Gruppe; eine Zeile mit korrekter eigener Mandantenkennung,
die auf die Gruppe eines fremden Mandanten zeigt, verletzt sie nachweislich nicht.
Die Anwendungspruefung ist damit der einzige Schutz gegen diese Form der
Rechteausweitung und darf nicht als "macht jetzt die Datenbank" entfallen
(Befund F, T-JTS-03).
SCHRITT 3, `docs/mandantentrennung-zugriffsklassifikation.md` schliessen:
- Die fuenf Zeilen dieser Datei bekommen ihren gemessenen Stand.
- Die Bereichsuebersicht wird fuer `groups` NEU GEMESSEN, nicht fortgeschrieben:
beide Zaehlungen (ungebunden, gebunden) mit den im Kopf der Uebersicht
dokumentierten Befehlen erneut ausfuehren, beide Werte eintragen und die
Summenzeile nachrechnen. In die Hinweisspalte kommt, was sich geaendert hat.
- Der Abschnitt zum Hintergrunddienst als Falle wird beim Eintrag zum
Verzeichnis-Abgleich fortgeschrieben: die dort als offene Reihenfolgebedingung
fuer Etappe 4 gefuehrte Uebergabe der Standardgruppe ist mit diesem Durchlauf
geschlossen.
- Im Abschnitt "Was diese Etappe NICHT entscheidet" wird festgehalten, dass auch
der Bereich `groups` den dienst-internen Weg gewaehlt hat und die Frage
`req.tenantPrisma` weiterhin fuer die uebrigen Bereiche offen bleibt.
- Die Zaehlung der Paare in der Ueberschrift der Klassen-Verteilung wird an die
tatsaechliche Zahl angepasst, die die Pruefung meldet.
SCHRITT 4, `docs/mandantentrennung-etappe2-fehlerrichtung.md` schliessen: im
ldap-Abschnitt (e) wird der dort als offen gefuehrte Befund zur Uebergabe der
Standardgruppe als durch diesen Durchlauf erledigt vermerkt, mit Verweis auf den
`groups`-Abschnitt. Der bestehende Text wird dabei nicht geloescht — die urspruengliche
Feststellung bleibt lesbar und bekommt einen Nachtrag; eine stillschweigend
umgeschriebene Vorgeschichte waere fuer die spaeteren Etappen wertlos.
npm --prefix apps/api run test -- src/groups/module-grants.service.spec.ts src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run type-check && npm --prefix apps/api run test && test "$(awk -F'|' '$2 ~ /groups\/(groups|module-grants)\.service\.ts/ { gsub(/ /,"",$5); print $5 }' docs/mandantentrennung-zugriffsklassifikation.md | sort -u)" = "gebunden" && test "$(awk -F'|' '$2 ~ /groups\/(groups|module-grants)\.service\.ts/ { print }' docs/mandantentrennung-zugriffsklassifikation.md | wc -l)" -ge 9 && test -z "$(git status --porcelain -- apps/api/prisma/schema.prisma apps/api/prisma/migrations docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml)"
Die 28 Bestandstests der Datei plus die neuen Bindungsnachweise sind gruen. Die Bestandsaufnahme-Pruefung ist mit aktualisiertem Dokument gruen. Jede Zeile des Bereichs `groups` in der Bestandsaufnahme traegt den Stand `gebunden`, und es sind mindestens die neun bisherigen Zeilen. Der gesamte Testlauf zeigt mindestens 719 Tests gruen, die Typpruefung liefert 0. Schema, Migrationen und alle vier Compose-Dateien sind unberuehrt.
Dienst-, Test- und Dokumentaenderung ohne Schema- oder Konfigurationsanteil.
## Vertrauensgrenzen
| Grenze | Beschreibung |
|---|---|
| Browser/Administrator -> API | Der Mandant stammt aus dem Sitzungsnachweis; Gruppen-, Benutzer- und Modulkennungen stammen aus Pfad und Rumpf der Anfrage und sind ungeprueft fremd |
| API -> PostgreSQL | Heute Rolle `tessera` mit BYPASSRLS; die Regeln wirken erst nach Etappe 4. Bis dahin ist die Bindung Vorsorge, keine Durchsetzung |
| Verzeichnis (AD) -> API | Nur lesend; Verzeichnisantworten loesen den Loeschzweig aus, der die Uebergabe der Standardgruppe in diesem Bereich aufruft |
| Startvorgang -> PostgreSQL | Der Startvorgang legt Mandanten und Standardgruppen an, bevor irgendeine Anfrage existiert |
| Werkzeug -> PostgreSQL | Das Wegwerf-Werkzeug spricht dieselbe Instanz an wie die Entwicklungsdatenbank |
## STRIDE-Register
| Kennung | Kategorie | Bauteil | Schwere | Umgang | Massnahme |
|---|---|---|---|---|---|
| T-JTS-01 | Information Disclosure | Lesen von `Group`, `GroupMembership`, `ModuleGrant`, `TenantModuleActivation` in beiden Diensten | high | mitigate | Alle 34 sichtbaren und 5 bisher unsichtbaren Modellzugriffe laufen nach Aufgabe 2/3 ueber den Mandantenkontext; dass die vier ausgelieferten Regeln tragen, wird in Aufgabe 1 unter einer Rolle ohne BYPASSRLS gemessen statt behauptet. |
| T-JTS-02 | Elevation of Privilege | Mitgliedschaftsanlage in der Standardgruppe ohne Mandantenpruefung des Zielbenutzers | medium | mitigate | Die Regel auf der Mitgliedschaftstabelle prueft nachweislich nur die Gruppenseite. Aufgabe 2 zieht die Mandantenpruefung des Zielbenutzers nach dem Vorbild des manuellen Hinzufuegens nach; Aufgabe 1 misst die Luecke in der Regel, damit die Massnahme nicht als ueberfluessig zurueckgebaut wird. Heute ueber keinen Aufrufer erreichbar, deshalb medium und nicht high. |
| T-JTS-03 | Elevation of Privilege | Freigabe mit eigener Mandantenkennung auf die Gruppe eines fremden Mandanten | high | mitigate | Die Regel auf der Freigabetabelle prueft nur die Mandantenkennung der Zeile, nicht die referenzierte Gruppe. Die Anwendungspruefung vor jedem Erteilen bleibt der Schutz; Aufgabe 1 misst die Luecke, Aufgabe 3 haelt sie mit einem Test fest und begruendet sie am Ort. |
| T-JTS-04 | Denial of Service (selbst verursacht, zerstoerend) | Uebergabe der Standardmarkierung vor der Loeschung im Verzeichnis-Abgleich | high | mitigate | Ein zu kleines Leseergebnis meldet still "kein Ersatzkandidat", der Aufrufer loescht die Gruppe trotzdem, und die Kaskade nimmt Mitgliedschaften und Freigaben mit. Aufgabe 2 bindet beide Lesewege und die zweischrittige Aenderung; Aufgabe 1 haelt Richtung und Signal schriftlich fest. Dies ist der Befund D des ldap-Durchlaufs und eine Reihenfolgebedingung fuer Etappe 4. |
| T-JTS-05 | Tampering | Aufbau der Standardgruppe bei halb umgestelltem Zustand | high | mitigate | Der Waechter ist umgekehrt gepolt: ein zu kleines Leseergebnis loest hier ein Schreiben aus statt es zu unterlassen. Eine zweite Standardgruppe samt Mitgliedschaften ALLER Benutzer und Freigaben ALLER aktiven Module waere die Folge. Aufgabe 2 bindet Zaehler und Transaktion zwingend gemeinsam und weist das mit einem eigenen Test nach. |
| T-JTS-06 | Denial of Service | Mitgliedschaftsanlage in der Standardgruppe bei nicht sichtbarer Standardgruppe | medium | mitigate | Neue Benutzer landen still in keiner Gruppe und sehen kein Modul. Nicht zerstoerend, aber lautlos; Aufgabe 2 bindet den Lesezugriff, Aufgabe 1 nennt das Signal (die Modulkacheln nach der ersten Anmeldung). |
| T-JTS-07 | Repudiation | Zahlen des Loeschdialogs | high | mitigate | Zwei Zaehlungen ohne Mandantenfilter melden bei Leere 0 und 0; der Administrator entscheidet auf dieser Grundlage ueber eine kaskadierende Loeschung. Aufgabe 2 bindet beide Zaehlungen; Aufgabe 1 fuehrt den Dialog als Signalort. |
| T-JTS-08 | Denial of Service | Startvorgang: Startanlage und Startreparatur der Standardgruppen | medium | mitigate | Der Aufbau der Standardgruppe hat vier Aufrufwege, zwei davon im Startvorgang. Eine Bindung, die den Kontext nicht auf dieselbe Verbindung bringt, koennte den Start beschaedigen. Aufgabe 1 misst die Transaktionsformen VOR der Umstellung; Aufgabe 2 haelt alle vier Wege mit Tests fest. |
| T-JTS-09 | Spoofing | Regeltext im Messwerkzeug | medium | mitigate | Die gemessenen Regeln werden aus den beiden ausgelieferten Migrationen extrahiert, nicht im Werkzeug nachgetippt; findet die Extraktion eine der vier nicht, meldet das Werkzeug eine fehlgeschlagene Pruefung statt still weiterzumessen. |
| T-JTS-10 | Tampering | Wegwerf-Werkzeug trifft dieselbe Datenbankinstanz wie die Entwicklung | high | mitigate | Der Name der Wegwerf-Datenbank bleibt fest verdrahtet und nicht steuerbar; der neue Abschnitt legt seine Tabellen ausschliesslich dort an und raeumt mit dem vorhandenen Abbau ab (T-EOR-07 unveraendert gueltig). |
| T-JTS-11 | Repudiation | Testsuiten, die eine fehlende Bindung nicht bemerken koennen | high | mitigate | Beide Testdateien haben heute gar keinen Ersatz fuer die Kontextbindung. Aufgabe 2 und 3 fuehren zwei unterscheidbare Clients ueber demselben Speicher ein; ein Test, der auch ohne Bindung gruen bliebe, wird umgeschrieben, bis er rot werden kann. |
**Paketlegitimitaet:** Dieser Durchlauf installiert kein Paket (npm/pip/cargo). Das
Legitimitaetstor greift daher nicht; es wird kein Lieferketten-Eintrag erfunden.
**Schema-Tor:** `apps/api/prisma/schema.prisma` und `apps/api/prisma/migrations/`
werden nicht angefasst, es entsteht keine Migration. Das Schema-Tor greift nicht.
Sollte sich bei der Ausfuehrung zeigen, dass eine Schemaaenderung unvermeidbar ist,
ist das ein Abbruchgrund: melden statt machen.
1. `npm --prefix apps/api run test` -> mindestens 719 Tests gruen (Ausgangsstand am
2026-09-09 gemessen: 53 Dateien, 719 Tests, 4,94 s).
2. `npm --prefix apps/api run type-check` -> Rueckgabewert 0.
3. Das Wegwerf-Werkzeug meldet alle Pruefungen bestanden, einschliesslich der neuen
des Bereichs `groups` und der einen Transaktionspruefung, und beendet sich mit 0.
4. Die Bestandsaufnahme-Pruefung ist gruen, obwohl Fundstellen von ungebunden auf
gebunden gewechselt sind UND mindestens eine bisher unsichtbare Fundstelle
hinzugekommen ist — die Absicherung ist mitgewachsen, nicht ausgehoehlt.
5. Jede Zeile des Bereichs `groups` in der Bestandsaufnahme traegt den Stand
`gebunden`, maschinell gegen den Quelltext geprueft.
6. `git status --porcelain` zeigt keine Aenderung an `apps/api/prisma/schema.prisma`,
an `apps/api/prisma/migrations/` oder an einer der vier Compose-Dateien. Die
Umgebungsdatei wird nicht geoeffnet und nicht geaendert; `DATABASE_URL` bleibt
unveraendert auf der Rolle `tessera`.
- Der Bereich `groups` ist umgestellt: 34 sichtbare und 5 bisher unsichtbare
Modellzugriffe in zwei Dateien laufen mandantengebunden; kein Zugriff dieses
Bereichs bleibt bewusst uebergreifend, und dass dem so ist, wurde gemessen und
nicht angenommen.
- Welche Transaktionsform den Mandantenkontext traegt, ist an der Datenbank gemessen,
BEVOR die drei Transaktionsstellen darauf umgestellt wurden; das Ergebnis steht im
Kopfkommentar der Erweiterung, damit der naechste Bereich es nicht erneut suchen
muss.
- Die Uebergabe der Standardgruppe vor einer Gruppenloeschung ist geschlossen; die
Reihenfolgebedingung fuer Etappe 4 aus dem ldap-Durchlauf ist erfuellt und in
beiden Dokumenten als erfuellt vermerkt.
- Die Frage "Woran wuerde ich merken, dass eine umgestellte Abfrage zu wenig
liefert?" ist fuer diesen Bereich schriftlich beantwortet, je Pfad mit einem
konkreten Signal, und die Antwort stuetzt sich auf eine Messung an den
ausgelieferten Regeln.
- Die maschinelle Absicherung sieht Zugriffe ueber den Transaktionsparameter; die
Erkennungsluecke, die ausgerechnet die Schreibstelle fuer Modulfreigaben verdeckt
hat, ist geschlossen.
- Beide Testdateien koennen rot werden, wenn eine Fundstelle ungebunden bleibt.
- Klassifikationsdokument und maschinelle Absicherung zeigen denselben, gemessenen
Stand.
- Der Schalter ist unveraendert AUS; Schema, Migrationen und Compose-Dateien sind
unberuehrt; am Verzeichnis wurde nichts geaendert.