Drei Aufgaben: Fehlerrichtung schriftlich festhalten und an der ausgelieferten Policy messen, dann ldap-config.service.ts und ldap.service.ts auf forTenant() umstellen. Alle Fundstellen zur Planungszeit einzeln aufgeschlagen statt gezaehlt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
39 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-260909-ipc | 01 | execute | 1 | true |
|
|
|
|
Zweck: Die Mandantentrennung im Bereich ldap so weit fertigstellen, dass das Scharfschalten (Etappe 4) diesen Bereich nicht mehr beschaedigen kann — und die Umkehr der Fehlerrichtung ("sieht zu viel" wird zu "sieht nichts") vorher schriftlich und gemessen festhalten, statt sie zu entdecken, wenn sie eintritt.
Ergebnis: eine Kritikschrift mit Signaltabelle, fuenf neue Messungen im vorhandenen Wegwerf-Datenbank-Werkzeug, zwei umgestellte Dienste, eine geschlossene Fremdzugriffsluecke beim Loeschen von Feldzuordnungen, und ein Klassifikationsdokument, das seinen neuen Stand maschinell nachweist.
Aufgabe 1 fuehrt bewusst, obwohl sie keinen Nutzernutzen liefert: sie ist der duenne Durchstich durch die gesamte Kette (ausgelieferte Policy -> Rolle ohne BYPASSRLS -> forTenant-Muster -> echte LDAP-Tabellen) und beantwortet die vom Wiedereinstieg verlangte Frage mit einer Messung statt mit einer Behauptung. Erst danach wird Dienstcode angefasst.
<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-09 zur Planungszeit am lebenden Baum gemessen, nicht aus Notizen uebernommen. Zahlen aus fremden Sitzungen sind Hinweise, keine Aenderungsvollmacht — die Fundstellen wurden einzeln aufgeschlagen.
Ausgangsstand (gemessen):
npm --prefix apps/api run test-> 53 Dateien, 701 Tests, gruen, 4,76 s.npm --prefix apps/api run type-check-> Rueckgabewert 0.pnpm --filter @tessera/api exec vitest run src/ldap-> 76 Tests gruen (9 inldap-config.service.spec.ts, 67 inldap.service.spec.ts).TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs-> "Alle 8 Pruefungen bestanden." Das Fundament ist damit JETZT belegt, nicht laut Bericht.docker inspect tessera-ctl-db-1liefert derzeit 172.19.0.2.
Die 21 Fundstellen, einzeln angesehen (nicht gezaehlt):
apps/api/src/ldap/ldap-config.service.ts — 9 Vorkommen von this.prisma.<Modell>:
| Zeile | Methode | Modell | Mandant am Aufrufort bekannt? |
|---|---|---|---|
| 57 | onApplicationBootstrap |
ldapConfig | NEIN — laeuft beim Start ueber alle Mandanten |
| 69 | onApplicationBootstrap |
ldapConfig | mittelbar (config.tenantId aus der Zeile) |
| 125 | getConfig |
ldapConfig | ja (Parameter) |
| 137 | createConfig |
ldapConfig | ja (Parameter) |
| 177 | updateConfig |
ldapConfig | ja (Parameter) |
| 215 | addFieldMapping |
ldapFieldMapping | NEIN — Signatur nimmt nur configId |
| 230 | removeFieldMapping |
ldapFieldMapping | NEIN — Signatur nimmt nur mappingId |
| 242 | removeFieldMapping |
ldapFieldMapping | NEIN — dito |
| 252 | getAllActiveConfigs |
ldapConfig | NEIN — liest bewusst alle Mandanten fuer den Planer |
apps/api/src/ldap/ldap.service.ts — 12 Vorkommen von this.prisma.<Modell> plus
9 bereits gebundene tenantPrisma.*-Stellen (4 forTenant()-Zuweisungen in Zeile
762, 905, 1179, 1342 — Zeilenangaben aus dem Klassifikationsdokument am lebenden
Baum bestaetigt):
| Zeile | Methode | Modell | Umstellen? |
|---|---|---|---|
| 346 | listGroups |
group | ja (Parameter tenantId) |
| 420 | resolveEmailForWrite |
user | NEIN — siehe Befund A |
| 452 | upsertMappedUser |
user | ja (Parameter) |
| 457 | upsertMappedUser |
user | ja |
| 475 | upsertMappedUser |
user | ja |
| 579 | searchUsers |
user | ja (Parameter) |
| 675 | importUsersByDn |
user | ja (Parameter) |
| 680 | importUsersByDn |
user | ja |
| 800 | importGroupsByDn |
group | ja — tenantPrisma liegt ab 762 bereits vor |
| 1003 | syncUsersForTenant |
user | ja — tenantPrisma liegt ab 905 bereits vor |
| 1014 | syncUsersForTenant |
user | ja |
| 1050 | syncUsersForTenant |
ldapConfig | ja |
9 + 12 = 21. Die dokumentierte Bereichszahl stimmt und traegt.
Befund A — resolveEmailForWrite darf NICHT gebunden werden.
prisma/schema.prisma fuehrt email String? @unique und username String @unique
plattformweit, nicht je Mandant. Die Kollisionspruefung aus T-Q3-01/WINDOWS #15 muss
deshalb ueber Mandantengrenzen sehen: sonst meldet sie "Adresse frei", der Schreibvorgang
laeuft in die Eindeutigkeitsverletzung der Datenbank, und der Sync bricht mit P2002 ab,
statt die Kollision zu berichten. Nach dem Scharfschalten liefert diese ungebundene
Abfrage null Zeilen und damit IMMER "frei" — ein bekannter, hier bewusst offen
gelassener Punkt fuer Etappe 3 (dort gehoert er zum Systemkontext, moeglicherweise als
vierte SECURITY-DEFINER-Funktion nach dem Muster des Anmeldewegs). Dieser Durchlauf
loest ihn NICHT, weil er eine Migration braeuchte und Migrationen hier ausgeschlossen
sind.
Befund B — zwei ldap-config-Zugriffe sind uebergreifend, nicht mandantengebunden.
getAllActiveConfigs() (Zeile 252) liefert dem Planer ldap-sync.scheduler.ts bewusst
die Konfigurationen ALLER Mandanten; die Schleife bindet danach je Mandant. Die
Start-Nachverschluesselung (Zeile 57/69) laeuft, bevor irgendein Mandantenkontext
existiert. Die heutige Klasse muss-mandantengebunden fuer das Paar
(ldap-config.service.ts, ldapConfig) ist damit falsch — richtig ist beides. Das
ist eine Korrektur des Dokuments, keine Verhaltensaenderung.
Befund C — Fremdzugriffsluecke beim Loeschen einer Feldzuordnung.
DELETE /ldap/config/mappings/:id (ldap.controller.ts, removeFieldMapping) nimmt
ausschliesslich die Kennung entgegen und reicht sie ungebunden an den Dienst weiter.
Ein Administrator des Mandanten A kann damit heute die Feldzuordnung des Mandanten B
loeschen, wenn er deren Kennung kennt. Die Rollenpruefung schuetzt nicht davor, sie
prueft nur die Rolle. Die Umstellung schliesst das als Nebenwirkung — deshalb wird sie
hier als eigener Sicherheitsbefund gefuehrt (T-IPC-01) und nicht beilaeufig miterledigt.
Befund D — der Loeschzweig ist bereits gebunden, seine Absicherung nicht.
Der als gefaehrlichster Punkt benannte Loeschzweig (heute Zeile 1559) laeuft schon ueber
tenantPrisma aus Zeile 1342, ebenso der Kandidaten-Lesezugriff in Zeile 1350. Die
Loeschentscheidung faellt ausserdem am VERZEICHNIS ("kein Treffer fuer objectGUID"),
nicht an der Datenbank; ein zu kleines Datenbankergebnis fuehrt dort zu WENIGER
Loeschungen, nicht zu mehr. Gefaehrlich ist stattdessen die Uebergabe unmittelbar davor:
this.groupsService.reassignDefaultBeforeDelete(tenantId, group.id) und
ensureDefaultGroup(tenantId) liegen in groups.service.ts und sind NICHT umgestellt.
Nach dem Scharfschalten liefert reassignDefaultBeforeDelete still false (kein
Ersatzkandidat sichtbar), der Standard-Marker wandert nicht mit, und die Gruppe wird
trotzdem geloescht — der Mandant bleibt still ohne Standardgruppe zurueck. Das ist eine
Reihenfolgebedingung fuer Etappe 4 und ein Argument, groups als naechsten Bereich zu
nehmen (so ist es ohnehin geplant). Dieser Durchlauf fasst groups.service.ts NICHT an.
Befund E — die stillste Stelle im Bereich ist der Planer.
Liefert getAllActiveConfigs() nach dem Scharfschalten null Zeilen, stellt der
LDAP-Abgleich fuer JEDEN Mandanten ohne Fehlermeldung, ohne Protokolleintrag und ohne
sichtbare Aenderung die Arbeit ein. Eine Laufzeitwarnung bei "null aktive
Konfigurationen" wurde erwogen und VERWORFEN: der Planer laeuft jede Minute, und auf
einer Installation ohne LDAP ist null der Normalfall — die Warnung waere Dauerlaerm.
Das Signal gehoert deshalb in die Kritikschrift (Aufgabe 1) und in die Vorabpruefung
von Etappe 4, nicht in den Minutentakt.
Befund F — die Tests wuerden die Umstellung nicht bemerken.
ldap.service.spec.ts ersetzt forTenant durch die Identitaet
(vi.mock(... forTenant: vi.fn((p) => p))). Ein Umbau von this.prisma.X auf
tenantPrisma.X laeuft dort also gruen durch, ohne irgendetwas zu beweisen. Aufgabe 3
braucht deshalb einen Test, der forTenant durch ein UNTERSCHEIDBARES zweites Objekt
ersetzt — sonst ist "umgestellt" eine Behauptung. ldap-config.service.spec.ts mockt
forTenant gar nicht und uebergibt ein blankes Objekt als Prisma-Ersatz; ohne den
gleichen Mock bricht es beim ersten $extends-Aufruf.
Befund G — die maschinelle Absicherung wuerde an der Umstellung zerbrechen.
rls-access-inventory.spec.ts sucht ausschliesslich this.prisma.<Modell>. Werden
Fundstellen auf tenantPrisma.<Modell> umgestellt, verschwindet das Paar aus dem
Quelltext, und der Test "jeder Eintrag hat eine tatsaechliche Fundstelle" schlaegt fehl —
fuer ldap-config.service.ts/ldapFieldMapping, ldap.service.ts/group und
ldap.service.ts/ldapConfig. Die Absicherung muss also erweitert werden, sonst zwingt
sie dazu, den Nachweis aus dem Dokument zu LOESCHEN statt ihn fortzuschreiben.
Nicht betroffen: grep -rn "searchProvider\|tenderRssFeedSource" apps/api/src/ldap/
liefert 0 Treffer — WINDOWS #19 reicht nicht in diesen Bereich hinein.
Gewaehltes Muster (bewusst, nicht stillschweigend): Der Mandantenkontext wird
weiterhin IM DIENST per forTenant(this.prisma, tenantId) erzeugt, wie an den vier
Bestandsstellen in ldap.service.ts und den drei in auth.service.ts. Der offene
Befund req.tenantPrisma (gesetzt in tenant.middleware.ts:44 und tenant.guard.ts:41,
nirgends gelesen) wird dadurch AUSDRUECKLICH NICHT entschieden — dieser Durchlauf legt
nur fest, was der Bereich ldap tut, und schreibt im Klassifikationsdokument fest, dass
die Frage fuer die uebrigen zehn Bereiche offen bleibt.
</planning_time_findings>
TEIL 1, apps/api/scripts/rls-scratch-check.mjs: einen dritten Abschnitt
runLdapAreaChecks(adminUrl, scratchRoleUrl, results) nach dem Vorbild des
vorhandenen runAuthLookupChecks ergaenzen und in main() nach diesem aufrufen. Der
Abschnitt legt in der Wegwerf-Datenbank schlanke Tabellen LdapConfig
(id, tenantId, serverUrl) und LdapFieldMapping (id, ldapConfigId, ldapField,
tesseraField) an — genau wie der auth-Abschnitt das fuer User tut —, aktiviert
darauf ENABLE plus FORCE ROW LEVEL SECURITY, vergibt SELECT/INSERT/UPDATE/DELETE an
die Wegwerf-Rolle und legt je eine Konfiguration fuer TENANT-A und TENANT-B samt je
einer Feldzuordnung an.
Die beiden Policies werden NICHT im Werkzeug neu getippt, sondern aus der
ausgelieferten Migration 20260618112133_rls_policies/migration.sql gelesen und
daraus die beiden CREATE POLICY-Anweisungen fuer "LdapConfig" und
"LdapFieldMapping" bis zum abschliessenden Semikolon herausgeschnitten (Vorbild:
readAuthLookupMigrationSql). Findet die Extraktion eine der beiden nicht, meldet der
Abschnitt eine FEHLGESCHLAGENE Pruefung ldap-policies-aus-migration-gefunden und
bricht ab — das Werkzeug darf nicht still durchlaufen, wenn es nichts zu messen
gefunden hat, sonst begeht es genau den Fehler, den dieser Plan beschreibt.
Gemessen werden unter der Rolle ohne BYPASSRLS, ueber das vorhandene
forTenantQuery-Hilfsmittel, fuenf Verhaltensweisen mit diesen Kennungen:
ldapconfig-gebunden-nur-eigene-zeile (forTenant(TENANT-A) sieht genau die Zeile von
A und keine von B), ldapconfig-ungebunden-null-zeilen (derselbe SELECT ohne Bindung
liefert 0 Zeilen — die Fehlerrichtung, an der echten Policy statt an der Hilfstabelle
probe gemessen), fieldmapping-folgt-join-auf-ldapconfig (forTenant(TENANT-A) sieht
genau die Feldzuordnung, die an A's Konfiguration haengt),
fieldmapping-schreiben-eigene-konfiguration-erlaubt (gebundenes INSERT mit A's
ldapConfigId gelingt) und fieldmapping-schreiben-fremde-konfiguration-abgelehnt
(gebundenes INSERT unter TENANT-A mit B's ldapConfigId wird abgewiesen; die
Abweisung ist das bestandene Ergebnis).
Das Werkzeug raeumt weiterhin ausschliesslich seine fest verdrahtete Wegwerf-Datenbank ab und bekommt keine steuerbaren Namen (T-EOR-07 bleibt gueltig).
TEIL 2, docs/mandantentrennung-etappe2-fehlerrichtung.md neu anlegen. Eine eigene
Datei statt eines Abschnitts im Klassifikationsdokument, mit ausgeschriebener
Begruendung im Kopf: das Klassifikationsdokument wird von
rls-access-inventory.spec.ts mit einem Zeilenmuster geparst, das JEDE Tabellenzeile
der Form Datei-Modell-Klasse einsammelt; eine Kritik mit eigenen Fundstellentabellen
wuerde diesem Parser in die Quere kommen. Ausserdem ist die Kritik etappenbezogen,
die Bestandsaufnahme dagegen laufend. Beide Dokumente verweisen aufeinander.
Inhalt der Kritikschrift, in ganzen Saetzen:
(a) Die Leitfrage und warum sie gestellt wird — bis heute war der Fehlerfall "sieht zu
viel", nach der Umstellung ist er "sieht nichts".
(b) Die Messung aus Teil 1 mit den tatsaechlich beobachteten Zeilen als Beleg, dass
ungebunden nach dem Scharfschalten null Zeilen bedeutet und nicht etwa alle.
(c) Eine Signaltabelle je umgestelltem Pfad des Bereichs ldap mit den Spalten Pfad,
Verhalten bei zu wenig Ergebnis, konkretes Signal. Mindestens diese Zeilen, jeweils
mit dem Signal, an dem man es merkt: der Abgleich-Bericht mit seinen Zaehlern
(created/updated/deactivated/groupMembershipsAdded/groupMembershipsRemoved/
groupsAdopted/groupsRenamed/groupsDeleted/defaultMarkerMoved), das Feld lastSyncAt
der Konfiguration, die Kennzeichnung "bereits importiert" in den Auswahllisten von
Gruppen und Benutzern, und die Protokollzeile "Starting LDAP sync for tenant ..." des
Planers.
(d) Ein eigener, hervorgehobener Abschnitt "Welcher Code deutet Leere als Abwesenheit"
mit genau diesen vier Stellen, jeweils mit Richtung der Gefahr:
syncGroupMembershipsForTenant — die Benutzerausloesung liefert zu wenig, danach
entfernt deleteMany mit notIn ALLE LDAP-Mitgliedschaften der Gruppe (gefaehrlich,
zerstoerend, bereits gebunden);
die Deaktivierungsschleife in syncUsersForTenant — liefert die Kandidatenliste zu
wenig, wird zu WENIG deaktiviert (harmlose Richtung, festhalten);
der Loeschzweig in syncBoundGroupsForTenant — die Entscheidung faellt am Verzeichnis,
das zu kleine Datenbankergebnis fuehrt zu weniger Loeschungen, ABER die Uebergabe
reassignDefaultBeforeDelete/ensureDefaultGroup liegt in groups.service.ts und ist
nicht umgestellt: sie meldet dann still "kein Ersatzkandidat", der Standard-Marker
wandert nicht mit, und der Mandant bleibt ohne Standardgruppe zurueck (Befund D);
getAllActiveConfigs — null Zeilen heisst, der Abgleich stellt fuer alle Mandanten
lautlos die Arbeit ein (Befund E), samt der Begruendung, warum dagegen KEINE
Laufzeitwarnung eingebaut wird und das Signal stattdessen in die Vorabpruefung von
Etappe 4 gehoert.
(e) Ein Abschnitt "Was dieser Durchlauf bewusst nicht loest" mit Befund A
(resolveEmailForWrite muss uebergreifend bleiben, weil email und username
plattformweit eindeutig sind — sonst wird aus einer berichteten Kollision ein
P2002-Abbruch; nach dem Scharfschalten liefert die Pruefung immer "frei", Loesung
gehoert nach Etappe 3, vermutlich als vierte SECURITY-DEFINER-Funktion), Befund D als
Reihenfolgebedingung fuer Etappe 4, und dem ausdruecklichen Hinweis, dass die Frage
req.tenantPrisma fuer die uebrigen Bereiche offen bleibt.
Der Text wird auf Deutsch geschrieben und kommt ohne Umlaut-Sonderzeichen in Dateinamen aus; im Fliesstext sind Umlaute in Ordnung, die Datei liegt im Repository und nicht auf einer Webseite. 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-ldap-check.log" && grep -q "fieldmapping-schreiben-fremde-konfiguration-abgelehnt: bestanden" "${TMPDIR:-/tmp}/rls-ldap-check.log" && grep -q "ldapconfig-ungebunden-null-zeilen: bestanden" "${TMPDIR:-/tmp}/rls-ldap-check.log" && grep -q "fieldmapping-folgt-join-auf-ldapconfig: bestanden" "${TMPDIR:-/tmp}/rls-ldap-check.log" && test -f docs/mandantentrennung-etappe2-fehlerrichtung.md Das Werkzeug meldet alle Pruefungen bestanden (8 aus Etappe 1 plus die 5 neuen plus die Fundpruefung der Policies) und beendet sich mit 0. Die Kritikschrift existiert, nennt je Pfad ein konkretes Signal, listet die vier Stellen, die Leere als Abwesenheit deuten, und traegt die tatsaechlich gemessenen Werte ein — nicht erwartete. Kein Dienstcode wurde in dieser Aufgabe angefasst.
Aufgabe 2: ldap-config.service.ts binden, Fremdzugriff beim Loeschen schliessen, Absicherung erweitern apps/api/src/ldap/ldap-config.service.ts, apps/api/src/ldap/ldap.controller.ts, apps/api/src/ldap/ldap-config.service.spec.ts, apps/api/src/prisma/rls-access-inventory.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md - `getConfig(tenantId)`, `createConfig(tenantId, dto)` und `updateConfig(tenantId, dto)` erzeugen `forTenant(this.prisma, tenantId)` und fuehren ihre Abfrage darauf aus; der Test prueft, dass `forTenant` mit genau diesem Mandanten aufgerufen wurde. - `addFieldMapping` nimmt den Mandanten entgegen und schreibt gebunden; ein Aufruf ohne Mandant ist typseitig unmoeglich. - `removeFieldMapping` nimmt den Mandanten entgegen, liest die Zuordnung gebunden und liefert null, wenn sie unter diesem Mandanten nicht sichtbar ist — die Steuerung macht daraus 404 statt einer Loeschung. - Der Schutz der Vorgabe-Zuordnungen (isDefault) bleibt unveraendert wirksam. - `getAllActiveConfigs()` und die Start-Nachverschluesselung bleiben ungebunden; ein Test haelt fest, dass fuer sie KEIN Mandantenkontext erzeugt wird. - Die neun Bestandstests der Datei (Verschluesselung at rest, Altbestand, Backfill) bleiben gruen. - Die Bestandsaufnahme-Pruefung erkennt gebundene Fundstellen und vergleicht sie gegen eine neue Stand-Spalte des Dokuments. Reihenfolge: erst die Absicherung erweitern, dann die Tests schreiben, dann umstellen.SCHRITT 1, apps/api/src/prisma/rls-access-inventory.spec.ts erweitern (Befund G).
Die Fundstellensuche bekommt neben this.prisma.<Modell> eine zweite Erkennung fuer
gebundene Zugriffe: je Datei werden die Zuweisungen der Form const <Name> = forTenant(
eingesammelt und danach die Vorkommen <Name>.<Modell> gesucht. Jede Fundstelle
traegt fortan zusaetzlich, ob sie gebunden oder ungebunden ist. Aus beiden Mengen
ergibt sich je Paar (Datei, Modell) ein Stand: gebunden, ungebunden oder
gemischt.
Die Bestandsaufnahme-Tabelle im Dokument bekommt eine vierte Spalte Stand zwischen
Klasse und Begruendung; das vorhandene Zeilenmuster verankert nur die ersten drei
Spalten und bleibt dadurch gueltig. Neue Pruefungen: jeder Eintrag traegt einen der
drei Stand-Werte, und der eingetragene Stand stimmt mit dem im Quelltext gemessenen
ueberein. Die Fehlermeldung dieser Pruefung nennt je abweichendem Paar den gemessenen
Wert, damit das Dokument aus der Messung gefuellt werden kann statt aus Vermutung.
Die bestehende Pruefung "jeder Eintrag hat eine tatsaechliche Fundstelle" gilt
kuenftig fuer gebundene wie ungebundene Fundstellen.
Die Erkennung hat eine bekannte Grenze: ein forTenant(...)-Ergebnis, das nicht an
eine Konstante gebunden, sondern direkt weiterverwendet wird, sieht sie nicht. Eine
eigene Pruefung haelt diese Grenze offen: jedes forTenant(-Vorkommen im Quelltext
muss entweder der erkannten Zuweisungsform entsprechen oder in einer kurzen,
begruendeten Ausnahmeliste stehen. In dieser Liste stehen zum Start genau
tenant.middleware.ts und tenant.guard.ts mit dem Vermerk, dass sie den gebundenen
Client auf dem Anfrageobjekt veroeffentlichen und dass genau dieser Weg die offene
Architekturfrage ist.
SCHRITT 2, apps/api/src/ldap/ldap-config.service.spec.ts: den Identitaets-Mock fuer
forTenant nach dem Vorbild aus ldap.service.spec.ts ergaenzen (ohne ihn bricht die
Datei am blanken Prisma-Ersatz, Befund F), und die in <behavior> beschriebenen
Erwartungen als Tests schreiben. Diese Tests laufen zunaechst rot.
SCHRITT 3, apps/api/src/ldap/ldap-config.service.ts umstellen. In getConfig,
createConfig und updateConfig je einmal am Methodenkopf einen gebundenen Client
erzeugen und die Abfrage darauf ausfuehren; die vorhandene Cast-Schreibweise der
Bestandsstellen uebernehmen, damit die Typpruefung gruen bleibt. addFieldMapping
bekommt den Mandanten als ersten Parameter, removeFieldMapping ebenso; beide binden
Lesen und Schreiben. Das verschachtelte Anlegen der drei Vorgabe-Zuordnungen in
createConfig bleibt eine einzige Prisma-Operation und laeuft damit in derselben
Transaktion wie das Setzen des Kontexts — dass diese Schreibweise unter der Policy
traegt, ist in Aufgabe 1 gemessen.
getAllActiveConfigs und onApplicationBootstrap bleiben unveraendert ungebunden.
Beide bekommen darueber einen ausgeschriebenen Absatz, der sagt, warum sie
uebergreifend lesen muessen, dass sie nach dem Scharfschalten null Zeilen sehen wuerden,
was das jeweils bedeutet (Abgleich stellt lautlos die Arbeit ein; Nachverschluesselung
wird stillschweigend zum Nichtstun) und dass die Loesung Etappe 3 gehoert. Die
Formulierung dieser Absaetze beschreibt den Sachverhalt, ohne die Klassennamen der
Bestandsaufnahme als isolierte Schlagworte zu setzen.
SCHRITT 4, apps/api/src/ldap/ldap.controller.ts: beide Aufrufstellen nachziehen. Bei
addFieldMapping liegt der Mandant bereits als lokale Variable vor. Bei
removeFieldMapping fehlt er ganz — die Methode bekommt das Anfrageobjekt, holt den
Mandanten daraus, weist wie die uebrigen Routen der Datei bei fehlendem Mandanten ab
und reicht ihn weiter (Befund C, T-IPC-01).
SCHRITT 5, docs/mandantentrennung-zugriffsklassifikation.md nachziehen: die
Stand-Spalte in die Bestandsaufnahme-Tabelle einfuegen und ALLE Zeilen mit dem
gemessenen Stand fuellen — dazu die erweiterte Pruefung laufen lassen und ihre
Ausgabe als Quelle nehmen, nicht schaetzen. Die Zeile
(ldap-config.service.ts, ldapConfig) wird von muss-mandantengebunden auf
beides korrigiert, mit Begruendung nach Befund B; die Zeile
(ldap-config.service.ts, ldapFieldMapping) behaelt ihre Klasse und wird gebunden.
Die Verteilungstabelle wird entsprechend nachgerechnet. Im Abschnitt "Was diese
Etappe NICHT entscheidet" wird festgehalten, dass der Bereich ldap den
Dienst-internen Weg gewaehlt hat und die Frage req.tenantPrisma fuer die uebrigen
Bereiche offen bleibt. Ein Verweis auf die Kritikschrift aus Aufgabe 1 kommt in den
Kopf des Dokuments.
npm --prefix apps/api run test -- src/ldap/ldap-config.service.spec.ts src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run type-check && npm --prefix apps/api run test
Die neun Bestandstests der Konfigurationsdatei sind weiterhin gruen, dazu die neuen Bindungstests. Die Bestandsaufnahme-Pruefung erkennt gebundene Fundstellen, prueft die Stand-Spalte gegen den Quelltext und ist gruen — mit aktualisiertem Dokument, nicht mit geloeschten Zeilen. Das Loeschen einer Feldzuordnung verlangt den Mandanten. Der gesamte Testlauf bleibt bei mindestens 701 Tests gruen, die Typpruefung liefert 0.
Reine Dienst- und Testaenderung ohne Schema-, Migrations- oder Konfigurationsanteil; ein einzelner Commit laesst sich zuruecknehmen.
SCHRITT 2, apps/api/src/ldap/ldap.service.ts umstellen. Die Fundstellen werden ueber
ihren Inhalt aufgesucht, nicht ueber die Zeilennummern aus diesem Plan — die Datei
verschiebt sich waehrend der eigenen Bearbeitung. Umzustellen sind genau elf
Abfragen in sechs Methoden:
in listGroups die Abfrage, die die bereits importierten Gruppen anhand ihrer
Verzeichniskennung markiert;
in upsertMappedUser die beiden Identitaetssuchen (ueber ldapDn und ueber den
Benutzernamen) sowie die anschliessende Aktualisierung;
in searchUsers die Abfrage, die die schon vorhandenen Konten markiert;
in importUsersByDn die Dublettenpruefung und die Aktualisierung, die den ldapDn
nachtraegt;
in importGroupsByDn die Idempotenzpruefung ueber die Verzeichniskennung;
in syncUsersForTenant die Kandidatenliste der Deaktivierung, die Deaktivierung
selbst und das Fortschreiben des Zeitpunkts der letzten Ausfuehrung.
In importGroupsByDn und syncUsersForTenant existiert der gebundene Client bereits
am Methodenkopf und wird schlicht mitbenutzt. In listGroups, upsertMappedUser,
searchUsers und importUsersByDn wird er einmal am Methodenkopf erzeugt, in der
Schreibweise der vier Bestandsstellen. Gebundene Clients werden NICHT zwischen
Methoden weitergereicht: jede Methode bleibt fuer sich lesbar, und die
Fundstellenerkennung aus Aufgabe 2 kann sie je Datei zuordnen. Die vorhandenen
Filterbedingungen auf den Mandanten bleiben stehen — sie sind das erste Netz, die
Policy das zweite.
Die zwoelfte Fundstelle, die Adressabfrage in resolveEmailForWrite, bleibt
ausdruecklich ungebunden. Darueber kommt ein ausgeschriebener Absatz mit dem
vollstaendigen Grund: die Spalten fuer Adresse und Benutzername sind im Schema
plattformweit eindeutig, nicht je Mandant; eine auf den eigenen Mandanten
eingeschraenkte Suche wuerde einen fremden Halter uebersehen, die Pruefung meldete
"frei", und aus einer sauber berichteten Kollision wuerde ein Abbruch an der
Eindeutigkeitsbedingung der Datenbank. Der Absatz haelt ausserdem fest, dass diese
Abfrage nach dem Scharfschalten null Zeilen liefert und deshalb in Etappe 3 einen
Systemkontext braucht — vermutlich nach dem Muster der Funktionen des Anmeldewegs.
SCHRITT 3, docs/mandantentrennung-zugriffsklassifikation.md schliessen. Die drei
Zeilen zu ldap.service.ts bekommen ihren gemessenen Stand und eine Begruendung, die
den Sonderfall der Adressabfrage benennt. Die Bereichsuebersicht wird NEU GEMESSEN,
nicht fortgeschrieben: die dort dokumentierte Zaehlung fuer den Bereich ldap erneut
ausfuehren, dazu die Zaehlung der gebundenen Fundstellen, beide Werte eintragen und
die Summenzeile nachrechnen. Die Kopfzeile der Uebersicht wird so umformuliert, dass
erkennbar ist, dass die Spalte kuenftig ungebundene Fundstellen zaehlt und die
gebundenen daneben stehen — eine unveraenderte Ueberschrift ueber veraenderter
Bedeutung waere die naechste stille Falle. Der Abschnitt zum Hintergrunddienst wird
fuer ldap.service.ts auf den neuen Stand gebracht: was jetzt gebunden ist, was
bewusst nicht, und dass die Uebergabe an den Bereich groups (Standardgruppe vor dem
Loeschen) offen bleibt und vor Etappe 4 erledigt sein muss.
npm --prefix apps/api run test -- src/ldap/ldap.service.spec.ts src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run type-check && npm --prefix apps/api run test && GESPERRT=$(git status --porcelain -- apps/api/prisma/schema.prisma apps/api/prisma/migrations docker-compose.yml docker-compose.prod.yml .env) && test -z "$GESPERRT"
Alle 67 Bestandstests der Datei plus die neuen Bindungsnachweise sind gruen; der Test mit zwei unterscheidbaren Clients belegt, dass die Adressabfrage ungebunden und der Rest gebunden laeuft. Die Bestandsaufnahme-Pruefung ist mit aktualisiertem Dokument gruen. Der gesamte Testlauf zeigt mindestens 701 Tests gruen, die Typpruefung liefert 0. Schema und Compose-Dateien sind unberuehrt.
Dienst- und Testaenderung ohne Schema- oder Konfigurationsanteil.
<threat_model>
Vertrauensgrenzen
| Grenze | Beschreibung |
|---|---|
| Browser/Administrator -> API | tenantId stammt aus dem Sitzungsnachweis, die Kennung der Feldzuordnung dagegen aus der URL — ungeprueft fremd |
| API -> PostgreSQL | Heute Rolle tessera mit BYPASSRLS; die Policies wirken erst nach Etappe 4. Bis dahin ist die Bindung Vorsorge, keine Durchsetzung |
| Verzeichnis (AD) -> API | Nur lesend, svc_tessera; Verzeichnisantworten steuern Loeschentscheidungen |
| Werkzeug -> PostgreSQL | Das Wegwerf-Werkzeug spricht dieselbe Instanz an wie die Entwicklungsdatenbank |
STRIDE-Register
| Kennung | Kategorie | Bauteil | Schwere | Umgang | Massnahme |
|---|---|---|---|---|---|
| T-IPC-01 | Elevation of Privilege | DELETE /ldap/config/mappings/:id in ldap.controller.ts |
high | mitigate | Die Route nimmt heute nur die Kennung; ein Administrator des Mandanten A kann die Feldzuordnung des Mandanten B loeschen. Aufgabe 2 fuehrt den Mandanten aus dem Sitzungsnachweis ein und bindet Lesen und Loeschen; eine fremde Kennung loest dann 404 aus. |
| T-IPC-02 | Information Disclosure | Lesen von LdapConfig/LdapFieldMapping |
high | mitigate | Alle mandantenbezogenen Lese- und Schreibpfade beider Dienste laufen ueber forTenant(); dass die Join-Policy fuer LdapFieldMapping traegt, wird in Aufgabe 1 unter einer Rolle ohne BYPASSRLS gemessen statt behauptet. |
| T-IPC-03 | Denial of Service (selbst verursacht, zerstoerend) | syncGroupMembershipsForTenant, Entfernen mit notIn |
high | mitigate | Eine zu kleine Benutzerausloesung entfernt saemtliche LDAP-Mitgliedschaften einer Gruppe. Der Pfad ist bereits gebunden; Aufgabe 1 haelt Richtung und Signal (groupMembershipsRemoved) schriftlich fest, Aufgabe 1 misst die Bindungswirkung an der echten Policy. |
| T-IPC-04 | Tampering | resolveEmailForWrite |
high | mitigate | Wuerde diese Abfrage mitgebunden, saehe sie einen fremden Halter nicht mehr, meldete "Adresse frei" und der Schreibvorgang liefe in die plattformweite Eindeutigkeitsbedingung. Aufgabe 3 laesst sie bewusst ungebunden, begruendet das am Ort und nagelt es mit einem Test fest, der zwei unterscheidbare Clients verwendet. |
| T-IPC-05 | Denial of Service | getAllActiveConfigs, Start-Nachverschluesselung |
medium | transfer | Nach Etappe 4 saehen beide null Zeilen: der Abgleich stellt lautlos die Arbeit ein, die Nachverschluesselung wird zum Nichtstun. Uebergabe an Etappe 3 (Systemkontext) mit Eintrag in Kritikschrift und Klassifikationsdokument; eine Laufzeitwarnung wurde erwogen und wegen Dauerlaerm im Minutentakt verworfen. |
| T-IPC-06 | Repudiation | Loeschzweig ohne Standardgruppen-Uebergabe | medium | transfer | reassignDefaultBeforeDelete/ensureDefaultGroup liegen im nicht umgestellten Bereich groups und wuerden nach Etappe 4 still versagen. Als Reihenfolgebedingung fuer Etappe 4 dokumentiert; groups ist ohnehin der naechste Bereich. |
| T-IPC-07 | Tampering | Wegwerf-Werkzeug trifft die echte Datenbank | high | mitigate | Der Name der Wegwerf-Datenbank bleibt im Werkzeug 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-IPC-08 | Spoofing | Policy-Text im Messwerkzeug | medium | mitigate | Die gemessenen Policies werden aus der ausgelieferten Migrationsdatei gelesen, nicht im Werkzeug nachgetippt; findet die Extraktion nichts, meldet das Werkzeug eine fehlgeschlagene Pruefung statt still durchzulaufen. |
Paketlegitimitaet: Dieser Durchlauf installiert kein Paket (npm/pip/cargo). Das Legitimitaetstor greift daher nicht; es wird kein Lieferketten-Eintrag erfunden.
Schema-Tor: prisma/schema.prisma wird 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.
</threat_model>
<success_criteria>
- Der Bereich ldap ist umgestellt: elf Abfragen in
ldap.service.tsund fuenf Methoden inldap-config.service.tslaufen gebunden; drei Zugriffe bleiben mit ausgeschriebener Begruendung uebergreifend. - Die Fremdzugriffsluecke beim Loeschen einer Feldzuordnung ist geschlossen.
- Die Frage "Woran wuerde ich merken, dass eine umgestellte Abfrage zu wenig liefert?" ist schriftlich beantwortet, je Pfad mit einem konkreten Signal, und die Aussage stuetzt sich auf eine Messung an der ausgelieferten Policy.
- Klassifikationsdokument und maschinelle Absicherung zeigen denselben, gemessenen Stand.
- Der Schalter ist unveraendert AUS; Schema, Migrationen, Compose-Dateien und
.envsind unberuehrt; am Verzeichnis wurde nichts geaendert. </success_criteria>