efcf11c988
Verifikation unabhaengig nachgemessen: 651/651 API-Tests, 233/233 Web-Tests, beide Typpruefungen sauber. Der Sicherheitsfund T-Q3-01 wurde nicht nur gruen getestet, sondern falsifiziert: der Verifizierer hat die neue Besitzpruefung testweise zurueckgebaut, woraufhin der Test fehlschlug und die fremde Adresse tatsaechlich in prisma.user.update() landete. Danach sauber zurueckgesetzt. Ebenso gegengeprueft: beide Anlege-Wege (Sync und Einzelimport) nutzen denselben Kollisionsentscheider, im ausgelieferten Code stehen keine kundenspezifischen Namen oder Adressen, rohe ORM-Texte erreichen die Oberflaeche nicht mehr, und die Gruppensuche unter internem wie AD-Namen (#6c) ist per Regressionstest gesichert. Eine Abweichung des Ausfuehrenden ist dokumentiert und bestaetigt: die im Plan vorgesehene Testvorlage war wegen Kurzschlussauswertung schon gegen den unveraenderten Code gruen; mit einem zweiten, nicht passenden Konto reproduziert sie den Absturz nun wirklich. Status human_needed: die fuenf Browser-Pruefungen brauchen einen Neubau und sind Sache des Users. WINDOWS #14 und #15 bleiben bis dahin offen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
130 lines
12 KiB
Markdown
130 lines
12 KiB
Markdown
---
|
||
phase: quick-260909-ab3
|
||
plan: 01
|
||
subsystem: ldap
|
||
tags: [ldap, sync, i18n, ui, prisma, react]
|
||
status: complete
|
||
dependency-graph:
|
||
requires: []
|
||
provides:
|
||
- "Freigaben-Matrix: Suche filtert Modul- und Gruppen-Achse unabhaengig — ein Treffer auf nur einer Achse leert die andere nicht mehr"
|
||
- "resolveEmailForWrite(): geteilter Kollisionsentscheider fuer AD-Kontoanlage (LdapService.upsertMappedUser + importUsersByDn)"
|
||
- "LdapSyncResult.{emailConflicts,skippedNoLogin,entryFailures}: strukturierter Sync-Bericht ohne rohen ORM-/englischen Techniktext"
|
||
- "User.email optional (Migration geschrieben, nicht ausgefuehrt)"
|
||
affects:
|
||
- apps/web/src/app/(portal)/admin/modules/grants/page.tsx
|
||
- apps/api/src/ldap/ldap.service.ts
|
||
- apps/api/src/user/user.service.ts
|
||
- apps/web/src/app/(portal)/admin/ldap/page.tsx
|
||
- apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx
|
||
- apps/web/src/app/(portal)/admin/users/page.tsx
|
||
tech-stack:
|
||
added: []
|
||
patterns:
|
||
- "Kollisionsentscheider als private Methode, die vor jeder E-Mail-Schreibung prisma.user.findUnique befragt und nur zurueckgibt, wenn niemand die Adresse haelt oder der Halter der eigene Datensatz ist — nie ein Umhaengen zwischen Konten"
|
||
- "Rohe ORM-/Techniktexte gehen ausschliesslich ueber logger.error ins Serverprotokoll; der Administrator sieht nur die Kennung des betroffenen Eintrags"
|
||
actuals:
|
||
tokens: 10082
|
||
tasks: 3
|
||
commits: 3
|
||
decisions:
|
||
- "Task 3 Testfixture von der Plan-Vorgabe abgewichen: ein einzelnes Konto mit passendem Benutzernamen loest den Absturz wegen OR-Kurzschlussauswertung nie aus (bestaetigt: Test war mit der Plan-Fixture gruen gegen den unveraenderten Bestand). Ein zweites, nicht-treffendes Konto ohne Adresse in derselben Mock-Liste macht den Test echt rot — siehe Deviations."
|
||
metrics:
|
||
duration: 13 min
|
||
completed: 2026-09-09
|
||
---
|
||
|
||
# Quick Task 260909-ab3: Matrix-Suche und Sync-Meldungen reparieren Summary
|
||
|
||
Freigaben-Matrix-Suche filtert Modul- und Gruppenachse jetzt unabhaengig (WINDOWS #14 geschlossen); AD-Sync legt Konten mit kollidierender E-Mail-Adresse an — ohne Adresse — statt sie stillschweigend zu uebergehen, und der Bericht spricht durchgehend verstaendliches Deutsch (WINDOWS #15 geschlossen).
|
||
|
||
## What Was Built
|
||
|
||
Drei getrennte, jeweils fuer sich lauffaehige Commits — exakt wie im Plan vorgegeben.
|
||
|
||
**Task 1 — Matrix-Suche filtert nur noch die getroffene Achse (commit `e8c2411`)**
|
||
- `page.tsx`: `filteredModules`/`filteredGroups` durch einen gemeinsamen `useMemo` ersetzt, der `moduleHits`/`groupHits` getrennt ermittelt und nach der im Plan festgelegten Wahrheitstabelle kombiniert (leeres Feld -> alles; nur eine Achse trifft -> die andere Achse bleibt vollstaendig stehen; beide treffen -> beide gefiltert; nichts trifft -> `noMatch`).
|
||
- Regressionsschutz fuer WINDOWS #6c (Spaltensuche findet eine importierte Gruppe sowohl unter internem als auch AD-Namen) unveraendert erhalten und mit einem eigenen Testfall abgesichert.
|
||
- Neue Meldung `adminModules.grants.noSearchResults` in de.json/en.json — de.json folgt der im Bestand bereits etablierten typografischen Anfuehrungszeichen-Konvention (`„...\"`, siehe `nameCollisionError`).
|
||
- Vier neue Testfaelle in `grants-matrix.test.tsx` vorab gegen den unveraenderten Bestand rot gelaufen — alle vier aus den im Plan vorhergesagten Gruenden (fehlende Modul-/Gruppennamen bzw. fehlender Meldungstext).
|
||
|
||
**Task 2 — Kollidierende AD-Konten werden angelegt, ohne Adresse (commit `1222951`)**
|
||
- `schema.prisma`: `User.email` auf `String? @unique` gestellt; Migration `20260909120000_user_email_optional` geschrieben (nur `DROP NOT NULL`, Eindeutigkeitsindex unangetastet) — **nicht ausgefuehrt**, wie vom Plan verlangt.
|
||
- `ldap.service.ts`: neue private Methode `resolveEmailForWrite(desiredEmail, ownRecordId)` fragt `prisma.user.findUnique({where:{email}}))` und gibt die Adresse nur frei, wenn niemand sie haelt oder der Halter der eigene Datensatz ist (T-Q3-01) — sonst wird kein Konto umgehaengt, sondern ein Kollisionsvermerk erzeugt.
|
||
- In `upsertMappedUser` (Sync-Pfad) UND `importUsersByDn` (Handimport-Pfad) verdrahtet, damit der zweite Anlageweg nicht als Luecke mit rohem Datenbanktext bestehen bleibt.
|
||
- `LdapSyncResult` um `emailConflicts` ({account, email}[]), `skippedNoLogin` (string[]) und `entryFailures` (string[]) erweitert. Die englische "no username mapped"-Meldung im Sync-Pfad ist verschwunden (wandert jetzt in `skippedNoLogin`); der generische Catch-Block schreibt den technischen Wortlaut nur noch via `this.logger.error`, nie in den Bericht (T-Q3-02).
|
||
- `UserService.create` nimmt `email` jetzt optional entgegen. Type-Check deckte genau die im Plan vorhergesagten zwei Fundstellen auf (`tender-digest.scheduler.ts:140`, `tender-matching.service.ts:131`) — beide brechen jetzt mit `continue` ab, wenn der Empfaenger keine Adresse hat.
|
||
- Fuenf neue Testfaelle in `ldap.service.spec.ts` vorab gegen den unveraenderten Bestand rot gelaufen, jeweils aus dem im Plan vorhergesagten Grund. Eine bestehende `toEqual`-Assertion (`empty base DN no-op`-Test) musste um die drei neuen Ergebnisfelder ergaenzt werden (legitime Bestandspflege, keine Verhaltensaenderung).
|
||
|
||
**Task 3 — Verstaendlicher Sync-Bericht und Konten ohne Adresse in der UI (commit `2167046`)**
|
||
- `admin/ldap/page.tsx`: `SyncResult`-Interface um die drei neuen Felder erweitert; drei neue Berichtsabschnitte in der vorgegebenen Reihenfolge (Kollision: Bernstein wie `defaultMarkerMoved`; uebersprungen ohne Anmeldenamen: `text-muted-foreground`, ausdruecklich kein Rot; unerwartete Fehler: `text-destructive`).
|
||
- Vier neue Sprachschluessel unter `admin.ldap.sync` in de.json/en.json; Umlaut-Gate lief gruen ohne neue Allowlist-Eintraege (nur "Adresse" traf das `ss`-Muster, war bereits erlaubt).
|
||
- `GroupMembersModal.tsx`: `TenantUser.email` auf `string | null`, Suchvergleich (Zeile 104) analog zum bestehenden `displayName`-Muster mit `(u.email ?? '')` abgesichert.
|
||
- `users/page.tsx`: `User.email` auf `string | null`; Formularuebernahme faellt auf `''` zurueck, Tabellenzelle zeigt einen Gedankenstrich (`–`) statt einer leeren Zelle. `UserFormData.email` bleibt Pflichtfeld fuer die Handanlage.
|
||
|
||
## Verification Results
|
||
|
||
- **API-Testsuite:** `pnpm --filter @tessera/api exec vitest run` — **651/651 gruen** (46 Dateien), inklusive `ldap.service.spec.ts` mit 67/67 (62 bestehend + 5 neu).
|
||
- **API-Typpruefung:** `pnpm --filter @tessera/api type-check` — sauber.
|
||
- **`prisma validate`/`prisma generate`:** beide sauber; Migration existiert, wurde **nicht ausgefuehrt**.
|
||
- **Web-Testsuite:** `pnpm --filter @tessera/web exec vitest run` — **233/233 gruen** (38 Dateien), inklusive `grants-matrix.test.tsx` (12/12) und `groups-page.test.tsx` (13/13).
|
||
- **Web-Typpruefung:** `pnpm --filter @tessera/web type-check` — sauber.
|
||
- **Sprachschluessel-/Umlaut-Gate:** `umlaut-guard.spec.ts` gruen bei jedem Zwischenstand — alle neuen Schluessel existieren in de.json UND en.json, kein neues, ungeprueftes Umlaut-Ersatzwort.
|
||
- **Alle vier Befehle aus `<verification>` des Plans** ein weiteres Mal am Ende, vom Wurzelverzeichnis aus, gruen.
|
||
|
||
## Deviations from Plan
|
||
|
||
### Auto-fixed Issues
|
||
|
||
**1. [Rule 1 - Bug] Task-3-Testfixture konnte den bezeichneten Absturz nicht ausloesen**
|
||
- **Found during:** Task 3, RED-Lauf vor der Implementierung
|
||
- **Issue:** Der Plan gibt exakt einen Mock-Benutzer vor (`{ id: 'u12', username: 'funktionskonto', displayName: null, email: null }`) und den Suchbegriff `'funktion'`. Da `'funktionskonto'.includes('funktion')` bereits ueber `u.username` matcht, greift die JavaScript-OR-Kurzschlussauswertung (`||`) VOR der ungesicherten `u.email.toLowerCase()`-Zeile — der Testlauf gegen den unveraenderten Bestand war **gruen**, nicht rot wie vom Plan vorhergesagt. Empirisch mit `node -e` bestaetigt (siehe Commit-Nachricht).
|
||
- **Fix:** Ein zweites Konto (`{ id: 'u13', username: 'anderes.konto', displayName: null, email: null }`) in dieselbe Mock-`/users`-Antwort aufgenommen. Sein Benutzername matcht den Suchbegriff nicht, sodass die OR-Kette bis zur ungesicherten Zeile durchlaeuft und dort tatsaechlich abstuerzt — Testlauf danach korrekt rot, exakt an `GroupMembersModal.tsx:104`. `funktionskonto` bleibt Teil der Fixture und der Assertion, wie vom Plan verlangt.
|
||
- **Files modified:** apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx
|
||
- **Verification:** Test rot vor der Implementierung (TypeError an der vorhergesagten Zeile), gruen danach; volle Testsuite (233/233) bleibt gruen.
|
||
- **Committed in:** 2167046 (Task-3-Commit)
|
||
|
||
---
|
||
|
||
**Total deviations:** 1 auto-fixed (Rule 1 — Testfixture-Korrektur, kein Produktionscode-Verhalten geaendert)
|
||
**Impact on plan:** Der zugrunde liegende Defekt (ungesichertes `.toLowerCase()` auf einer moeglicherweise leeren Adresse) war real und wurde wie geplant behoben; nur die Testdaten mussten angepasst werden, damit der Test die Behauptung tatsaechlich pruefte statt trivial gruen zu sein.
|
||
|
||
## Threat Model Verification
|
||
|
||
Alle sechs `mitigate`-Eintraege aus dem Plan-Threat-Register sind umgesetzt:
|
||
|
||
- **T-Q3-01 (Spoofing, `upsertMappedUser`-Aktualisierungszweig):** Mitigiert — `resolveEmailForWrite` vergleicht Halter-ID gegen die eigene Datensatz-ID; Test 3 aus Task 2 belegt, dass `prisma.user.update` ohne `email`-Feld aufgerufen wird, wenn die Adresse einem Dritten gehoert.
|
||
- **T-Q3-02 (Information Disclosure, Sync-Bericht):** Mitigiert — Test 5 aus Task 2 belegt per `JSON.stringify(result)`-Negativpruefung, dass der geworfene ORM-Text nirgends im Bericht landet; er erscheint nur im `logger.error`-Aufruf.
|
||
- **T-Q3-04 (Spoofing, Passwort-Zuruecksetzung bei leerer Adresse):** Bestaetigt ohne Codeaenderung — die Suche in `auth.service.ts` laeuft ueber eine angefragte Zeichenkette, ein `null`-Wert in der Spalte wird davon nie getroffen.
|
||
- **T-Q3-03/T-Q3-05/T-Q3-06/T-Q3-SC (accept):** Keine Codeaenderung noetig, Einschaetzung des Plans bestaetigt (keine neuen Pakete installiert, kein zusaetzliches DoS-Risiko bei den gemessenen Groessenordnungen).
|
||
|
||
## Self-Check: PASSED
|
||
|
||
**Files:**
|
||
- FOUND: apps/web/src/app/(portal)/admin/modules/grants/page.tsx
|
||
- FOUND: apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx
|
||
- FOUND: apps/api/prisma/schema.prisma
|
||
- FOUND: apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql
|
||
- FOUND: apps/api/src/ldap/ldap.service.ts
|
||
- FOUND: apps/api/src/ldap/ldap.service.spec.ts
|
||
- FOUND: apps/api/src/user/user.service.ts
|
||
- FOUND: apps/api/src/tenders/tender-digest.scheduler.ts
|
||
- FOUND: apps/api/src/tenders/tender-matching.service.ts
|
||
- FOUND: apps/web/src/app/(portal)/admin/ldap/page.tsx
|
||
- FOUND: apps/web/src/app/(portal)/admin/users/page.tsx
|
||
- FOUND: apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx
|
||
- FOUND: apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx
|
||
- FOUND: apps/web/src/messages/de.json
|
||
- FOUND: apps/web/src/messages/en.json
|
||
|
||
**Commits:**
|
||
- FOUND: e8c2411 (Task 1)
|
||
- FOUND: 1222951 (Task 2)
|
||
- FOUND: 2167046 (Task 3)
|
||
|
||
## Deployment Note (vom Plan uebernommen)
|
||
|
||
Dieser Plan hat **nichts ausgerollt**: kein `docker compose build`, kein `up`, kein `prisma migrate deploy`. Die Migration `20260909120000_user_email_optional` steckt im API-Image und wird beim naechsten Neubau/Neustart der API automatisch angewandt (`apps/api/Dockerfile:38` ruft `prisma migrate deploy` vor dem Start auf). Solange das nicht geschehen ist, bleibt `User.email` in der laufenden Datenbank ein Pflichtfeld — ein kollidierendes Konto wird dann weiterhin nicht angelegt, scheitert aber sauber als gezaehlter `entryFailures`-Eintrag statt mit rohem Techniktext.
|
||
|
||
Die fuenf im Plan genannten Browser-Nachpruefungen (Punkte 1-5 unter `<verification>`) sind vom Nutzer nach seinem naechsten Neubau von API und Web nachzuholen — nicht Teil dieser Ausfuehrung.
|