feat(quick-260910-das): messen die Kette user unsichtbar->frei->Eindeutigkeitsfehler
- runUserAreaChecks in rls-scratch-check.mjs: 12 neue Pruefungen gegen die
ausgelieferte User-Policy (baut auf der vom Anmeldeweg-Abschnitt
angelegten Tabelle auf, legt zusaetzlich Tenant ohne Zeilenschutz an)
- Belegt: ungebundene Suche nach vorhandenem Benutzernamen liefert 0
Zeilen, gebundene Suche nach fremdem Benutzernamen ebenso ("frei"), und
das anschliessende gebundene INSERT scheitert hart an SQLSTATE 23505
(Eindeutigkeitsverletzung), nicht an 42501 (Zeilenschutz)
- SQLSTATE wird aus err.meta.code gelesen, nicht err.code (das bei
$executeRaw-Fehlern immer den generischen Prisma-Code P2010 traegt,
empirisch gegen tessera-ctl-db-1 geprueft)
- docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt
"Bereich user" (u1-u5) mit der tatsaechlich beobachteten Ausgabe,
Signaltabelle, der vollstaendigen Kette (Befund L) und den Grenzen zu
auth.service.ts/ldap.service.ts
- Teil 2/3 gemessen: keine Transaktion in apps/api/src/user (Befund B
haelt), findByUsername hat genau einen Treffer, die eigene Definition
(Befund D haelt)
- 789 Tests weiterhin gruen, Typpruefung sauber, Wegwerf-Werkzeug meldet
alle 53 Pruefungen bestanden (41 bisherige + 12 neue)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -912,6 +912,276 @@ entscheidet sie nicht — er bindet dienst-intern, wie `ldap`, `groups` und
|
||||
Geprüft und bewusst gelassen — eine Lösung (Unterverzeichnisse je
|
||||
Mandant, Umzug der Bestandsdateien) ist ein eigener Auftrag, siehe (d4).
|
||||
|
||||
## Bereich user
|
||||
|
||||
Dieser Abschnitt erweitert die Kritikschrift um den Bereich `user`
|
||||
(Quick-Task 260910-das) und beschreibt ihn zum Zeitpunkt seiner Umstellung.
|
||||
Die Leitfrage aus Abschnitt (a) gilt unverändert weiter — dieser Abschnitt
|
||||
beantwortet sie erneut, für den Bereich, in dem das plattformweite
|
||||
Eindeutigkeitsproblem tatsächlich wohnt: `username` und `email` sind im
|
||||
Schema plattformweit eindeutig, nicht je Mandant, und dieser Bereich enthält
|
||||
als einziger BEIDE Formen gleichzeitig — Wege, die binden MÜSSEN
|
||||
(Benutzerverwaltung je Mandant), und einen Weg, der binden NICHT DARF
|
||||
(Nachschlagen auf dem plattformweit eindeutigen Schlüssel `username`). Ein
|
||||
Quer-Schreiben ist hier keine Offenlegung, sondern eine Rechteausweitung
|
||||
über die Mandantengrenze hinweg — die schwerste Klasse dieses ganzen
|
||||
Vorhabens.
|
||||
|
||||
### (u1) Die Messung
|
||||
|
||||
Aufgabe 1 hat `apps/api/scripts/rls-scratch-check.mjs` um einen siebten
|
||||
Abschnitt (`runUserAreaChecks`) erweitert. Die Tabelle `"User"` wird dabei
|
||||
NICHT neu angelegt — sie existiert bereits, vom Abschnitt des Anmeldewegs,
|
||||
samt eingeschaltetem und erzwungenem Zeilenschutz, beiden
|
||||
Eindeutigkeitsbedingungen (`username`, `email`) und zwei Testzeilen in zwei
|
||||
Mandanten (Befund O). Dieser Abschnitt baut darauf auf: er hält die im
|
||||
Anmeldeweg-Abschnitt von Hand getippte Policy GEGEN die aus der
|
||||
ausgelieferten Migration `20260618112133_rls_policies` geschnittene Fassung
|
||||
(beide sind nach Normalisierung von Leerraum und abschließendem Semikolon
|
||||
wortgleich — keine Ersetzung nötig), legt eine Tabelle `"Tenant"`
|
||||
ausdrücklich OHNE Zeilenschutz an (die zu messende Eigenschaft selbst), und
|
||||
ergänzt je eine weitere Benutzerzeile pro Mandant. Tatsächlich beobachtete
|
||||
Ausgabe dieses Laufs (2026-09-10, gegen `tessera-ctl-db-1`, Adresse
|
||||
`172.19.0.2`):
|
||||
|
||||
```
|
||||
user-policy-aus-migration-wortgleich: bestanden — die im Anmeldeweg-Abschnitt (runAuthLookupChecks) von Hand getippte Policy auf "User" ist nach Normalisierung von Leerraum und abschliessendem Semikolon wortgleich mit der aus 20260618112133_rls_policies geschnittenen
|
||||
user-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n): ["TENANT-A","TENANT-A"]
|
||||
user-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "User" liefert 0 Zeile(n)
|
||||
user-ungebundene-suche-nach-benutzername-liefert-keine-zeile: bestanden — ungebundenes SELECT ... WHERE username = 'bob' liefert 0 Zeile(n), obwohl der Benutzer existiert — der aufrufende Code liest daraus "diesen Benutzer gibt es nicht" und legt an
|
||||
user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile: bestanden — forTenant(TENANT-A) liefert fuer WHERE username = 'bob' (gehoert TENANT-B) 0 Zeile(n) — die Kollisionspruefung meldet faelschlich "frei"
|
||||
user-eindeutigkeit-greift-trotz-unsichtbarkeit: bestanden — gebundenes INSERT unter TENANT-A mit dem angeblich freien Benutzernamen "bob" wird abgewiesen mit SQLSTATE 23505 (Raw query failed. Code: `23505`. Message: `Unique constraint failed: `) — eine Eindeutigkeitsverletzung (23505), NICHT eine Zeilenschutz-Ablehnung: genau die im Auftrag beschriebene Kette
|
||||
user-gebundenes-einfuegen-fremder-mandant-abgelehnt: bestanden — gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B abgewiesen: Raw query failed. Code: `42501`. Message: `ERROR: new row violates row-level security policy for table "User"`
|
||||
user-ungebundenes-einfuegen-abgelehnt: bestanden — ungebundenes INSERT mit gueltiger Mandantenkennung abgewiesen: Raw query failed. Code: `42501`. Message: `ERROR: new row violates row-level security policy for table "User"`
|
||||
user-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen: bestanden — gebundenes UPDATE unter TENANT-A ueber die Kennung 'user-b' (gehoert TENANT-B) allein betrifft 0 Zeile(n) — die vorgeschalteten Besitz- und Rollenpruefungen bleiben deshalb erhalten und werden in Aufgabe 2/3 nicht durch die Datenbank ersetzt
|
||||
user-gebundenes-loeschen-ueber-kennung-allein-trifft-null-zeilen: bestanden — gebundenes DELETE unter TENANT-A ueber die Kennung 'user-b2' (gehoert TENANT-B) allein betrifft 0 Zeile(n)
|
||||
tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar: bestanden — ungebundenes SELECT auf "Tenant" liefert 2 Zeile(n): ["TENANT-A","TENANT-B"]; pg_class.relrowsecurity fuer "Tenant" = false
|
||||
user-fan-out-je-mandant-gebunden-liefert-alle-zeilen: bestanden — Vereinigung der je-Mandant gebundenen SELECTs liefert 4 Benutzernamen: ["alice","bob","carol","dave"]; Gesamtmenge (ueber die Wartungsrolle mit BYPASSRLS gemessen) sind 4: ["alice","bob","carol","dave"]
|
||||
Alle 53 Pruefungen bestanden.
|
||||
```
|
||||
|
||||
Drei Zeilen tragen diesen Abschnitt und werden hier ausdrücklich benannt und
|
||||
auseinandergehalten, weil sie zusammen die im Auftrag beschriebene Kette
|
||||
sind:
|
||||
|
||||
- **Die Belegzeile zur Unsichtbarkeit**, `user-ungebunden-null-zeilen`: der
|
||||
IDENTISCHE `SELECT "tenantId" FROM "User"` ohne vorheriges `set_config`
|
||||
liefert **0 Zeilen**, nicht etwa die 4 tatsächlich vorhandenen — an der
|
||||
echten, ausgelieferten Policy gemessen, nicht an einer im Werkzeug
|
||||
nachgebauten Hilfstabelle. Genau dieselbe Unsichtbarkeit trifft die
|
||||
ungebundene Suche nach einem GÜLTIGEN Benutzernamen
|
||||
(`user-ungebundene-suche-nach-benutzername-liefert-keine-zeile`) — die
|
||||
Form, die die Erstanlage-Prüfung beim Start heute benutzt.
|
||||
- **Die Zeile zum falschen „frei"**,
|
||||
`user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile`:
|
||||
dieselbe Suche, gebunden an TENANT-A, nach dem Benutzernamen `bob`
|
||||
(gehört TENANT-B), liefert ebenfalls 0 Zeilen — der Moment, in dem eine
|
||||
Kollisionsprüfung fälschlich „frei" meldet, obwohl der Name vergeben ist.
|
||||
- **Die Zeile zum harten Eindeutigkeitsfehler**,
|
||||
`user-eindeutigkeit-greift-trotz-unsichtbarkeit`: unmittelbar danach ein
|
||||
gebundenes `INSERT` unter TENANT-A mit genau diesem, angeblich freien
|
||||
Benutzernamen `bob`. Die Ablehnung trägt SQLSTATE **23505**
|
||||
(Eindeutigkeitsverletzung) — gelesen aus `err.meta.code`, nicht aus
|
||||
`err.code` (das bei einem fehlgeschlagenen `$executeRaw` immer den
|
||||
generischen Prisma-Code `P2010` trägt, empirisch gegen
|
||||
`tessera-ctl-db-1` geprüft, siehe Kopfkommentar von `sqlStateOf()` im
|
||||
Werkzeug) — und NICHT SQLSTATE 42501 (Zeilenschutz-Ablehnung), die dieser
|
||||
Abschnitt in zwei anderen Zeilen ebenfalls misst
|
||||
(`user-gebundenes-einfuegen-fremder-mandant-abgelehnt`,
|
||||
`user-ungebundenes-einfuegen-abgelehnt`). Diese Unterscheidung ist der
|
||||
Kern der Messung: nur die erste ist die im Auftrag beschriebene Kette,
|
||||
die zweite wäre eine ganz andere Geschichte.
|
||||
|
||||
Zusätzlich gemessen, statt behauptet: `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`
|
||||
hält sowohl das ungebundene `SELECT` (2 von 2 Zeilen sichtbar) als auch den
|
||||
Systemkatalog (`pg_class.relrowsecurity` für `"Tenant"` = `false`)
|
||||
gegeneinander — die Aussage hängt damit nicht allein daran, dass dieser
|
||||
Abschnitt selbst keinen Zeilenschutz eingeschaltet hat.
|
||||
`user-fan-out-je-mandant-gebunden-liefert-alle-zeilen` bildet die in
|
||||
Aufgabe 2/3 gewählte Form der Plattform-Administratorsicht nach (Mandanten
|
||||
ungebunden lesen, je Mandant EIN gebundener `SELECT`, Ergebnisse
|
||||
vereinigen) und hält das Ergebnis gegen eine über die Wartungsrolle (mit
|
||||
`BYPASSRLS`) gemessene Gesamtmenge, nicht gegen eine angenommene Zahl — beide
|
||||
Mengen sind identisch: `["alice","bob","carol","dave"]`.
|
||||
|
||||
**TEIL 2, Beleg statt Behauptung für Befund B** — keine Transaktion in
|
||||
diesem Bereich:
|
||||
|
||||
```
|
||||
$ grep -rn '\$transaction(' apps/api/src/user --include=*.ts | grep -v spec
|
||||
$ echo $?
|
||||
1
|
||||
```
|
||||
|
||||
Null Treffer, Rückgabewert 1. Der im Kopf von `prisma-tenant.extension.ts`
|
||||
verlangte erneute Test ist damit für diesen Bereich beantwortet: kein neuer
|
||||
Transaktionsfall, `withTenantTransaction()` wird hier nicht gebraucht und in
|
||||
Aufgabe 2/3 nicht eingeführt.
|
||||
|
||||
**TEIL 3, Beleg statt Behauptung für Befund D** — die Aufrufermessung für
|
||||
`findByUsername`:
|
||||
|
||||
```
|
||||
$ grep -rn "findByUsername" apps/api/src packages
|
||||
apps/api/src/user/user.service.ts:21: async findByUsername(username: string) {
|
||||
```
|
||||
|
||||
Genau EIN Treffer, die Definition selbst — kein Aufrufer. Etappe 1
|
||||
(260909-eor) hat den Anmeldeweg auf die drei schmalen
|
||||
SECURITY-DEFINER-Funktionen umgezogen; `auth.service.ts` sucht seither über
|
||||
`auth_lookup_user_by_username` und nicht mehr über diese Methode. Die
|
||||
Messung bestätigt Befund D unverändert: die Methode bleibt ungebunden
|
||||
(Entscheidung (c) aus den Planungszeit-Befunden), ihr Kopfkommentar wird in
|
||||
Aufgabe 2 richtiggestellt.
|
||||
|
||||
### (u2) Signaltabelle je umgestelltem Pfad
|
||||
|
||||
| Pfad | Verhalten bei zu wenig Ergebnis | Konkretes Signal, Ort |
|
||||
|---|---|---|
|
||||
| `UserService.findById` | Der gebundene `findUnique` liefert `null` statt des eigenen Benutzers | `GET /users/:id` liefert `404 User not found`, obwohl der Benutzer existiert |
|
||||
| `UserService.create` | Betrifft nicht das Lesen — ein gebundenes `INSERT` mit fremder Mandantenkennung wird von der Policy abgewiesen (`user-gebundenes-einfuegen-fremder-mandant-abgelehnt`) | `POST /users` scheitert mit einer Datenbank-Ablehnung statt einer verständlichen Meldung, sollte die Mandantenkennung je falsch ankommen — nach heutigem Code (Befund G, Selbstbedienungswege ausgenommen) nicht erreichbar, weil `tenantId` aus dem Sitzungsnachweis bzw. der ausdrücklichen SUPER_ADMIN-Übersteuerung stammt |
|
||||
| `UserService.update` | Der gebundene `update` über die Kennung allein trifft eine fremde Zeile still (0 betroffene Zeilen, `user-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen`), nicht laut | `PATCH /users/:id` würfe ohne die vorgeschaltete Prüfung in `user.controller.ts` keinen Fehler, sondern liefe ins Leere — die vorgeschaltete Mandantenprüfung bleibt deshalb Pflicht |
|
||||
| `UserService.deactivate`/`delete` | Dieselbe stille Form wie `update` | `DELETE /users/:id` bzw. das Deaktivieren würde ohne die vorgeschaltete Prüfung 0 Zeilen treffen, ohne Fehler |
|
||||
| Neue Methode: Plattform-Administratorsicht, Liste | Der Schleifentreiber (`tenant.findMany`, bewusst ungebunden) liefert 0 Mandanten statt der tatsächlich vorhandenen | Die Benutzerliste des `SUPER_ADMIN` ist für die gesamte Plattform leer, obwohl Mandanten mit Benutzern existieren — `user-fan-out-je-mandant-gebunden-liefert-alle-zeilen` misst die Vereinigung, nicht den Treiber; ein leerer Treiber ist eine Eigenschaft der Mandantentabelle, nicht dieses Bereichs |
|
||||
| Neue Methode: Plattform-Administratorsicht, Kennungs-Auflösung | Läuft der gebundene Lesezugriff für den Mandanten des Zielbenutzers leer, findet die Methode den Benutzer nicht | `GET /users/:id`, `PATCH /users/:id`, `DELETE /users/:id` liefern für den `SUPER_ADMIN` `404`, obwohl der Benutzer existiert |
|
||||
| `user.controller.ts`, `findAll` (ADMIN-Zweig) | Der gebundene `findMany` liefert 0 Benutzer statt der tatsächlich vorhandenen | Die Benutzerliste ist für einen Mandanten-Administrator leer — eine leere Liste sieht auf einer frischen Installation wie der Normalzustand aus |
|
||||
| Selbstbedienungswege (Bild hochladen/löschen, Akzentfarbe, Bild ausliefern) | Der gebundene Zugriff über die eigene Kennung aus dem Sitzungsnachweis liefert 0 Zeilen | `GET /users/me/avatar` liefert die vorhandene `404 No avatar set`, obwohl ein Bild hinterlegt ist — harmlos, siehe (u3) |
|
||||
| `UserService.findByUsername` (bewusst UNGEBUNDEN) | Betrifft nicht diesen Pfad selbst — er bindet nicht und liefert deshalb weiterhin korrekt. Das Risiko läge in einer KÜNFTIGEN Bindung | Würde man ihn binden: eine gebundene Suche nach einem fremden Benutzernamen meldete „frei" — hier ausdrücklich als Grenze festgehalten, nicht als heute beobachtbares Verhalten (Befund D) |
|
||||
| `AdminSeedService`, Erstanlage-Prüfung (bewusst UNGEBUNDEN) | Betrifft nicht diesen Pfad selbst — er bindet nicht, läuft aber NACH dem Scharfschalten für JEDEN Administrator ins Leere, weil ohne Mandantenkontext keine Zeile sichtbar ist | Siehe (u3) — die schwerste Ausprägung dieses gesamten Bereichs |
|
||||
| `AdminSeedService`, beide Zugriffe auf `tenant` (bewusst UNGEBUNDEN) | Betrifft nicht diese Pfade selbst — `Tenant` trägt keinen Zeilenschutz, ein ungebundenes Lesen/Schreiben bleibt korrekt | `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar` misst die tragende Eigenschaft |
|
||||
|
||||
### (u3) Welcher Code Leere als Abwesenheit deutet
|
||||
|
||||
Dies ist der Kern dieses Abschnitts. Die sechs Stellen aus Befund L,
|
||||
namentlich benannt und nach ihrer Wirkung sortiert:
|
||||
|
||||
**Startverhindernd (eine Stelle, die schwerste Ausprägung im ganzen
|
||||
Vorhaben):**
|
||||
|
||||
1. **`AdminSeedService.seedAdmin()`, die Erstanlage-Prüfung** — die
|
||||
vollständige, im Auftrag beschriebene Kette in Reinform: eine
|
||||
unsichtbare Zeile wird als Abwesenheit gelesen
|
||||
(`user.findUnique({ where: { username } })` liefert nach dem
|
||||
Scharfschalten `null`, nicht weil der Administrator fehlt, sondern weil
|
||||
ohne gesetzten Mandantenkontext keine Zeile der Benutzertabelle sichtbar
|
||||
ist — `user-ungebundene-suche-nach-benutzername-liefert-keine-zeile`),
|
||||
die natürliche Folgehandlung ist Anlegen (Schritt 4 läuft, weil Schritt
|
||||
3 entfällt), und das Anlegen scheitert hart an der plattformweiten
|
||||
Eindeutigkeit von `username`
|
||||
(`user-eindeutigkeit-greift-trotz-unsichtbarkeit`, SQLSTATE 23505, KEINE
|
||||
Zeilenschutz-Ablehnung). Weil `seedAdmin()` bewusst NICHT gekapselt ist
|
||||
— der Dateikopf sagt ausdrücklich, ein Fehlschlag solle den Start
|
||||
weiterhin laut scheitern lassen —, wird aus dieser einen unsichtbaren
|
||||
Zeile eine **Startsperre**: die Anwendung startet nach dem
|
||||
Scharfschalten nicht mehr, für jede bestehende Installation mit
|
||||
gesetzten Administrator-Umgebungswerten. Eine Absicht (laut scheitern)
|
||||
wird damit ungewollt zu einer Sperre für den Normalfall.
|
||||
|
||||
**Kollisionserzeugend (drei Stellen — dieselbe Kette, an anderen Stellen im
|
||||
Bereich):**
|
||||
|
||||
2. **`user.controller.ts`, `findAll` im ADMIN-Zweig** — eine leere Liste
|
||||
heißt „dieser Mandant hat keine Benutzer". Ein Administrator, der seine
|
||||
Kollegen nicht mehr sieht, legt sie erneut an; jede dieser Anlagen
|
||||
kollidiert auf `username` bzw. `email`. Die Oberfläche zeigt dabei
|
||||
nichts Auffälliges: eine leere Benutzerliste ist auf einer frischen
|
||||
Installation der Normalzustand.
|
||||
3. **`user.controller.ts`, `findAll` im SUPER_ADMIN-Zweig** — dieselbe
|
||||
Leere, eine Ebene höher: die Plattformverwaltung sieht eine
|
||||
Installation ohne jeden Benutzer und würde, bliebe die neue
|
||||
übergreifende Methode ungebunden statt als gebundene Schleife gebaut,
|
||||
ebenfalls zum Neuanlegen verleiten.
|
||||
4. **Eine gebundene Suche nach Benutzername oder Adresse** (Befund D,
|
||||
`user-gebundene-suche-nach-fremdem-benutzernamen-liefert-keine-zeile`)
|
||||
— meldet „frei" für einen Namen, den es gibt. Die Kollisionsprüfung wird
|
||||
zur Kollisionserzeugung; genau deshalb bleibt `findByUsername`
|
||||
ungebunden und übersetzt `UserService.create`/`update` die
|
||||
Eindeutigkeitsverletzung stattdessen an ihrem eigenen Erzeugungspunkt in
|
||||
eine verständliche Meldung (Aufgabe 2).
|
||||
|
||||
**Bereits gebunden, als Beleg benannt (eine Stelle, nicht Gegenstand
|
||||
dieses Durchlaufs):**
|
||||
|
||||
5. **`ldap.service.ts`, `upsertMappedUser`** — bereits gebunden, seit
|
||||
260909-ipc, in einem ANDEREN Bereich, hier nur zu benennen und nicht
|
||||
anzufassen: die Identitätssuche entscheidet über Anlegen-oder-
|
||||
Aktualisieren und läuft in dieselbe Kette. Sie ist der Beleg, dass die
|
||||
Kette nicht erst nach dem Scharfschalten existiert — die
|
||||
Suchbedingung trägt bereits heute den Mandanten, ein fremder Halter ist
|
||||
also bereits heute unsichtbar. Die Übersetzung der
|
||||
Eindeutigkeitsverletzung gehört deshalb an den gemeinsamen Anlegepunkt
|
||||
in `user.service.ts` (Aufgabe 2), der in diesem Plan ohnehin angefasst
|
||||
wird — dort und nicht in `ldap`.
|
||||
|
||||
**Harmlos (eine Stelle):**
|
||||
|
||||
6. **Die Selbstbedienungswege für Bild und Akzentfarbe** (Befund G) — ein
|
||||
leerer Lesezugriff heißt „kein Bild hinterlegt". Harmlos in der
|
||||
Wirkung, aber vollständigkeitshalber in der Tabelle (u2) festgehalten.
|
||||
|
||||
**Was ein GEBUNDENER Nachschlageweg auf `username` mit dem Anmeldeweg
|
||||
machen würde, und warum die Frage hier gegenstandslos ist:** der Anmeldeweg
|
||||
läuft seit Etappe 1 (260909-eor) über die drei SECURITY-DEFINER-Funktionen
|
||||
und nicht mehr über `UserService.findByUsername` — belegt durch die
|
||||
Aufrufermessung aus TEIL 3 oben (genau ein Treffer, die Definition selbst),
|
||||
nicht behauptet. Würde `findByUsername` dennoch gebunden, säße das Problem
|
||||
nicht im Anmeldeweg (der diese Methode gar nicht mehr aufruft), sondern
|
||||
genau in der unter Punkt 4 beschriebenen Kollisionserzeugung — derselbe
|
||||
Grund, aus dem `resolveEmailForWrite` im Bereich `ldap` ungebunden bleibt.
|
||||
|
||||
**Gegenrichtung, ebenfalls nachgesehen und in diese Kritikschrift
|
||||
gehörend:** ein gebundenes Ändern oder Löschen über die Kennung allein
|
||||
trifft eine fremde Zeile NICHT still im Sinne einer Zeilenschutz-Ablehnung,
|
||||
sondern schlicht mit null betroffenen Zeilen
|
||||
(`user-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen`,
|
||||
`user-gebundenes-loeschen-ueber-kennung-allein-trifft-null-zeilen`) — die
|
||||
vorgeschalteten Besitz- und Rollenprüfungen in `user.controller.ts` bleiben
|
||||
deshalb Pflicht und werden in Aufgabe 3 nicht durch die Datenbank ersetzt.
|
||||
Ein gebundenes Einfügen mit fremder Mandantenkennung wird laut abgewiesen
|
||||
(`user-gebundenes-einfuegen-fremder-mandant-abgelehnt`, SQLSTATE 42501).
|
||||
Das sind die lauten Stellen, und dieser Abschnitt besteht nicht nur aus
|
||||
Alarm.
|
||||
|
||||
### (u4) Was dieser Durchlauf bewusst nicht löst
|
||||
|
||||
Die plattformweite Eindeutigkeit von `username` und `email` selbst — die
|
||||
Ursache, aus der jede Ausnahme dieses Plans folgt. Die ehrliche Reparatur
|
||||
wäre eine Schemaänderung (eine Eindeutigkeit mit Mandantendimension); das
|
||||
ist eine Produktentscheidung — darf dieselbe Adresse zwei Mandanten
|
||||
gehören — und sie ist für Etappe 3 bereits vorgemerkt. Hier wird sie
|
||||
festgehalten, nicht entschieden, und ausdrücklich nicht durch eine
|
||||
Migration vorweggenommen (Broken-Windows-Register, Aufgabe 2).
|
||||
|
||||
Die Übergabe in den noch nicht umgestellten Bereich `auth`: `auth.service.ts`
|
||||
ist aus demselben Grund `gemischt` (Befund E) und NICHT Gegenstand dieses
|
||||
Plans. Die Grenze ist gemessen, nicht aus Erinnerung gezogen: die Datei hat
|
||||
drei bereits über `forTenant()` gebundene Schreibzugriffe (Anmeldezeitstempel,
|
||||
Kennwortwechsel nach Zurücksetzen) und fünf noch ungebundene Zugriffe auf
|
||||
`user`, die zu `getMe`, `changePassword` und `adminResetPassword` gehören.
|
||||
Die drei Anmelde-/Zurücksetz-Nachschlagewege laufen über `$queryRaw` auf die
|
||||
drei SECURITY-DEFINER-Funktionen. Dieser Plan fasst `auth.service.ts` an
|
||||
KEINER Stelle an.
|
||||
|
||||
Die offene Architekturfrage `req.tenantPrisma` — auch der Bereich `user`
|
||||
entscheidet sie nicht. Er bindet dienst-intern, wie `ldap`, `groups`,
|
||||
`tenders` und `dkv` es vormachen.
|
||||
|
||||
### (u5) Was dieser Durchlauf bewusst NICHT anfasst
|
||||
|
||||
- `auth.service.ts` an keiner Stelle (siehe (u4)).
|
||||
- Die drei SECURITY-DEFINER-Funktionen aus Etappe 1 und ihre Rechte nicht —
|
||||
der Anmeldeweg bleibt unberührt, belegt durch die Aufrufermessung aus
|
||||
TEIL 3 oben.
|
||||
- Schema und Migrationen nicht.
|
||||
- Die Verdrängung im gemeinsamen Ablagverzeichnis der Profilbilder
|
||||
(`user-files/avatars/`) nicht — anders als bei den Ausfuhrdateien des
|
||||
Bereichs `dkv` (Befund F dort) ist die Dateibenennung hier
|
||||
`{userId}.{ext}` und damit bereits kollisionsfrei über Mandanten hinweg;
|
||||
es ist ohnehin keine Bindungsfrage.
|
||||
|
||||
Jeweils mit der Feststellung, dass sie geprüft und bewusst gelassen sind —
|
||||
nicht übersehen.
|
||||
|
||||
## Verweis
|
||||
|
||||
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
|
||||
|
||||
Reference in New Issue
Block a user