diff --git a/.planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-PLAN.md b/.planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-PLAN.md new file mode 100644 index 0000000..c31c111 --- /dev/null +++ b/.planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-PLAN.md @@ -0,0 +1,520 @@ +--- +quick_id: 260909-ab3 +slug: matrix-suche-und-sync-meldungen-reparier +date: 2026-09-09 +status: planned +relates_to: 15-modul-berechtigungen-gruppen-user-grants, 16-ad-gruppen-synchronisation +windows_ref: 14, 15 +severity: high + +phase: quick-260909-ab3 +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - apps/web/src/app/(portal)/admin/modules/grants/page.tsx + - apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx + - apps/api/prisma/schema.prisma + - apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql + - apps/api/src/ldap/ldap.service.ts + - apps/api/src/ldap/ldap.service.spec.ts + - apps/api/src/user/user.service.ts + - apps/api/src/tenders/tender-digest.scheduler.ts + - apps/api/src/tenders/tender-matching.service.ts + - apps/web/src/app/(portal)/admin/ldap/page.tsx + - apps/web/src/app/(portal)/admin/users/page.tsx + - apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx + - apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + - apps/web/src/messages/umlaut-dictionary.ts +autonomous: true +requirements: [PERM-02, PERM-03] + +estimate: + tokens: 85000 + raw_tokens: 85000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "Freigaben-Matrix: ein Suchbegriff, der nur einen GRUPPENNAMEN trifft, laesst ALLE Modulzeilen stehen — die Kreuzung Modul x Gruppe bleibt anklickbar." + - "Freigaben-Matrix: ein Suchbegriff, der nur einen MODULNAMEN trifft, laesst ALLE Gruppenspalten stehen — die Kreuzung bleibt anklickbar." + - "Freigaben-Matrix: die Spaltensuche findet eine importierte Gruppe weiterhin sowohl unter ihrem internen Namen als auch unter ihrem AD-Namen (Bestandsverhalten aus WINDOWS #6c bleibt erhalten)." + - "Freigaben-Matrix: ein Begriff, der weder ein Modul noch eine Gruppe trifft, zeigt eine sichtbare Meldung statt einer stumm leeren Tabelle." + - "AD-Sync: ein Konto, dessen mail-Adresse bereits einem ANDEREN Konto gehoert, wird angelegt — ohne Adresse. Das zuerst angelegte Konto behaelt seine Adresse unveraendert." + - "AD-Sync: keine Zeile des Sync-Berichts enthaelt rohen ORM-Ausnahmetext oder englischen Techniktext; die technischen Angaben landen ausschliesslich im Serverprotokoll." + - "AD-Sync: Verzeichniseintraege ohne Anmeldenamen erscheinen als neutraler Hinweis (normaler Vorgang), nicht als Fehler." + - "Ein Benutzer ohne E-Mail-Adresse bricht weder die Benutzerliste noch die Mitgliedersuche der Gruppenverwaltung." + artifacts: + - apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql + - apps/web/src/app/(portal)/admin/modules/grants/page.tsx + - apps/api/src/ldap/ldap.service.ts + - apps/web/src/app/(portal)/admin/ldap/page.tsx + key_links: + - "upsertMappedUser() -> UserService.create({ email? }) -> Prisma User.email nullable — die Kette muss durchgaengig optional sein, sonst scheitert die Anlage weiterhin." + - "LdapSyncResult (apps/api/src/ldap/ldap.service.ts) <-> interface SyncResult (apps/web/src/app/(portal)/admin/ldap/page.tsx) — neue Felder muessen auf beiden Seiten gleich heissen, sonst bleibt der Bericht leer." + - "de.json <-> en.json Schluesselgleichheit — abgesichert durch den dritten Test in apps/web/src/messages/umlaut-guard.spec.ts (flacht BEIDE Dateien vollstaendig ab)." +--- + + +Zwei unabhaengige, am 2026-09-07/09 im Browser auf alpha gemessene Defekte reparieren. + +**Defekt A (WINDOWS #14)** — Die Suche in der Freigaben-Matrix filtert beide Achsen mit +demselben Begriff und leert dadurch die jeweils andere: `page.tsx:137` filtert Module +ueber `m.name`, `page.tsx:145` filtert Gruppen ueber `internalName ?? name`. Ein Begriff, +der nur eine Achse trifft, entfernt die andere vollstaendig — es bleibt nie ein Kaestchen +zum Klicken uebrig. Damit scheitert genau der Zweck der Suche. + +**Defekt B (WINDOWS #15)** — Der AD-Sync reicht rohe Techniktexte durch und laesst echte +Konten still liegen. Vier Funktionskonten (CN=uvertrieb_ro, uvertrieb_rw, uvertrieb_ro_ss, +usoftware_rw aus OU=CTL_PWS_Gruppen) tragen alle dieselbe Adresse `mbuntz@ctl.de` — die +ihres Vorgesetzten. Sie scheitern deshalb an der Eindeutigkeitsregel auf `User.email` und +werden NIE importiert; der Administrator liest nur die woertliche Prisma-Meldung. Sechs +weitere Eintraege (Kontakte/Ressourcen ohne sAMAccountName) melden englisch +`no username mapped (check sAMAccountName mapping)`, obwohl sie voellig korrekt +uebersprungen werden. + +**Gesperrte Produktentscheidung des Nutzers vom 2026-09-09 (nicht verhandelbar):** solche +Konten MUESSEN angelegt werden, nur eben OHNE E-Mail-Adresse. Wer eine Adresse zuerst +belegt, behaelt sie; jedes spaetere Konto mit derselben Adresse entsteht ohne Adresse. +Begruendung: die Anmeldung laeuft ueber den Benutzernamen, nicht ueber die Adresse — das +Konto funktioniert also, lediglich Benachrichtigungen per E-Mail erreichen es nicht. +Nichts geht mehr still verloren. Generisch umzusetzen: Tessera ist ein Mehrmandanten- +Produkt, also keine firmenspezifischen Werte und keine Sonderfaelle fuer einzelne Konten +im Code (Konvention "Keine Kunden-spezifischen Defaults"). + +Purpose: Die Freigaben-Matrix wird bedienbar, und der Sync verliert keine Konten mehr und +spricht mit dem Administrator in verstaendlichem Deutsch. +Output: Drei getrennte, jeweils fuer sich lauffaehige Commits — A, B-Verhalten, B-Anzeige. + +## Gemessener Ausgangszustand (am Arbeitsbaum geprueft, 2026-09-09) + +| Beleg | Fundstelle | +|-------|------------| +| Beide Achsen unabhaengig gefiltert | `apps/web/src/app/(portal)/admin/modules/grants/page.tsx:133-147` | +| `User.email` ist PFLICHTFELD (`String @unique`, kein `?`) | `apps/api/prisma/schema.prisma:32` | +| Kollisionsquelle: Anlage schreibt die Adresse ungeprueft | `apps/api/src/ldap/ldap.service.ts:421` (Sync) und `:604` (Handimport) | +| Englische Uebersprungen-Meldung | `apps/api/src/ldap/ldap.service.ts:862` (Sync) und `:576` (Handimport) | +| Roher ORM-Text landet in `errors` | `apps/api/src/ldap/ldap.service.ts:885` (generischer Auffang je Eintrag) | +| Bericht rendert `errors` unveraendert in Rot | `apps/web/src/app/(portal)/admin/ldap/page.tsx:1341-1350` | +| Absturzrisiko bei leerer Adresse | `apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx:104` (`u.email.toLowerCase()`) | + +## Zur Datenbank-Aenderung (bitte woertlich so weitergeben) + +`User.email` muss leer sein duerfen. Das ist eine Schema-Aenderung mit Migration. +**Die Migration wird in diesem Plan NUR geschrieben, NICHT ausgefuehrt** — weder lokal +noch auf dem Testserver. Das Ausrollen macht der Nutzer. + +Wichtige Richtigstellung zur Aufgabenbeschreibung: die API ruft `prisma migrate deploy` +sehr wohl beim Start auf — im Startbefehl des Images (`apps/api/Dockerfile:38`). Die +Migration greift also automatisch, sobald der Nutzer die API neu baut und neu startet. +Solange sie NICHT angewandt ist, bleibt die Spalte ein Pflichtfeld: ein Konto mit +kollidierender Adresse wird dann weiterhin nicht angelegt, scheitert aber sauber als +gezaehlter Eintragsfehler im Bericht statt mit rohem Techniktext. Kein Absturz, keine +stille Luecke. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@CLAUDE.md + +Bestandscode, der beim Umsetzen gelesen werden muss: +@apps/web/src/app/(portal)/admin/modules/grants/page.tsx +@apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx +@apps/api/src/ldap/ldap.service.ts +@apps/api/src/ldap/ldap.service.spec.ts +@apps/web/src/app/(portal)/admin/ldap/page.tsx +@apps/web/src/messages/umlaut-guard.spec.ts + +Verbindliche Konventionen aus dem Bestand: +- Strukturierte Rueckgabe statt uebersetzter Backend-Prosa: `LdapGroupImportResult.nameCollisions` + ist genau dafuer eingefuehrt worden (`ldap.service.ts:74-86`) — Backend liefert Daten, das + Frontend formuliert den Satz aus `de.json`/`en.json`. +- Sprachschluessel-Gate: der dritte Test in `apps/web/src/messages/umlaut-guard.spec.ts` flacht + de.json UND en.json vollstaendig ab und vergleicht die Schluesselmengen. Ein Schluessel nur in + einer Datei faellt sofort auf. +- Umlaut-Gate: jedes NEUE deutsche Wort in `de.json`, das die Folge `ae`, `oe`, `ue` oder `ss` + enthaelt, muss in `UMLAUT_ALLOWLIST` (`apps/web/src/messages/umlaut-dictionary.ts`) stehen, + sonst wird der Test rot. Betroffen von den hier geplanten Texten waeren z. B. `Adressen` + oder `Ressourcen` (`Adresse`, `muss`, `Passwort` stehen bereits drin). Die Fehlermeldung des + Tests nennt das fehlende Wort woertlich. + + + + + + Task 1: Matrix-Suche filtert nur noch die getroffene Achse (WINDOWS #14) + apps/web/src/app/(portal)/admin/modules/grants/page.tsx, apps/web/src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json + + + Verbindliche Semantik (in dieser Reihenfolge auszuwerten), `moduleHits` = Module deren + `name` passt, `groupHits` = Gruppen deren `internalName ?? name` ODER `name` passt: + + 1. Leeres Suchfeld -> alle Module, alle Gruppen. (Bestandsverhalten) + 2. `moduleHits` nicht leer, `groupHits` leer -> nur `moduleHits` als Zeilen, ALLE Gruppen + als Spalten. + 3. `groupHits` nicht leer, `moduleHits` leer -> ALLE Module als Zeilen, nur `groupHits` + als Spalten. + 4. Beide nicht leer -> beide Achsen gefiltert. Das leert keine Achse, weil beide Mengen + Treffer enthalten; die Kreuzung bleibt klickbar. + 5. Beide leer (Begriff trifft nichts) -> Tabelle gar nicht rendern, stattdessen eine + sichtbare Meldung. + + Neue Testfaelle in `grants-matrix.test.tsx` (im bestehenden + `describe('AdminModuleGrantsPage (Permission-Matrix)')`): + - Test 1: Suche 'Buchhaltung' -> 'Ausschreibungs-Radar' UND 'DKV Flotte' sind sichtbar, + 'Alle Benutzer' ist verschwunden, `screen.getAllByRole('checkbox')` ist nicht leer. + - Test 2: Suche 'Flotte' -> 'Alle Benutzer' UND 'Buchhaltung' sind sichtbar, + 'Ausschreibungs-Radar' ist verschwunden. + - Test 3: Suche 'Vertrieb' (interner Name) und danach — nach `clear()` — 'Claude_VT' + (AD-Name) -> in beiden Faellen ist die Spaltenueberschrift 'Vertrieb' sichtbar UND + beide Modulnamen sind sichtbar. Regressionsschutz fuer WINDOWS #6c. + - Test 4: Suche 'zzz' -> die neue Meldung ist sichtbar und + `screen.queryAllByRole('checkbox')` hat Laenge 0. + + Vor der Umsetzung gegen den unveraenderten Bestand laufen lassen und die Rotfaerbung + protokollieren. Erwartete Ursachen: Test 1 scheitert, weil beide Modulnamen fehlen; + Test 2, weil beide Gruppenueberschriften fehlen; Test 3, weil die Modulnamen fehlen; + Test 4, weil der Meldungstext nirgends existiert. Sind die Ursachen andere, erst klaeren, + dann umsetzen. + + + + Zuerst die Testdatei erweitern, dann die Seite anpassen. + + In `grants-matrix.test.tsx`: `mockMatrix.groups` um einen dritten Eintrag ergaenzen, der + eine importierte AD-Gruppe abbildet — `{ id: 'g3', name: 'Claude_VT', internalName: 'Vertrieb' }`. + Bestehende Tests duerfen dadurch nicht rot werden (sie pruefen namentlich genannte Texte, + eine zusaetzliche Spalte stoert nicht). Den lokalen `messages`-Mock im Namensraum + `adminModules.grants` um den neuen Schluessel ergaenzen, damit der Meldungstext im Test + aufgeloest wird. Danach die vier oben beschriebenen Testfaelle ergaenzen. + + In `page.tsx`: `matches` (Zeile 134) unveraendert lassen. `filteredModules` (Zeile 136-140) + und `filteredGroups` (Zeile 141-147) durch einen gemeinsamen `useMemo` ueber + `[modules, groups, searchLower]` ersetzen, der `moduleHits`, `groupHits`, `filteredModules`, + `filteredGroups` und ein `noMatch`-Flag nach der Semantik oben liefert. Die vorhandene + Erklaerung zur doppelten Gruppen-Namenspruefung (interner Name UND AD-Name, D-04) bleibt als + Kommentar erhalten — dieses Verhalten ist gemessen richtig und darf nicht wegfallen. + `groupedModules` (Zeile 149-160) bleibt unveraendert und liest weiter `filteredModules`. + + Im Renderteil: die Tabelle ab Zeile 205 nur noch rendern, wenn `noMatch` falsch ist. Bei + `noMatch` stattdessen einen Absatz mit `t('noSearchResults', { search })` ausgeben, gestaltet + wie der vorhandene Leerzustand weiter oben (`rounded-md border border-border bg-card p-6 + text-center`, Text in `text-sm text-muted-foreground`). Das Suchfeld selbst bleibt immer + sichtbar, sonst kann der Nutzer seinen Begriff nicht mehr korrigieren. + + Sprachdateien: `adminModules.grants.noSearchResults` in BEIDE Dateien. + de: `Kein Treffer fuer "{search}" — weder bei den Modulen noch bei den Gruppen.` — dabei die + typografischen Anfuehrungszeichen und das korrekte Wort mit u-Umlaut verwenden, keine + Ersatzschreibung (das Umlaut-Gate prueft genau das). + en: `No match for "{search}" — neither in the modules nor in the groups.` + + + + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run "src/app/(portal)/admin/modules/grants/grants-matrix.test.tsx" "src/messages/umlaut-guard.spec.ts" && pnpm --filter @tessera/web type-check + + + + Die vier neuen Testfaelle sind gruen, die acht bestehenden Testfaelle der Datei ebenfalls. + Das Sprachschluessel-Gate (umlaut-guard.spec.ts) ist gruen, also existiert der neue + Schluessel in de.json UND en.json. `pnpm --filter @tessera/web type-check` ist sauber. + Eigener Commit, der ausschliesslich Defekt A enthaelt. + + + + + Task 2: Kollidierende AD-Konten werden angelegt — ohne Adresse (WINDOWS #15, Verhalten) + apps/api/prisma/schema.prisma, apps/api/prisma/migrations/20260909120000_user_email_optional/migration.sql, apps/api/src/ldap/ldap.service.ts, apps/api/src/ldap/ldap.service.spec.ts, apps/api/src/user/user.service.ts, apps/api/src/tenders/tender-digest.scheduler.ts, apps/api/src/tenders/tender-matching.service.ts + + Das Aufheben der Pflicht auf `User.email` ist rueckgaengig zu machen, solange keine Zeile leer ist; sobald der erste Sync Konten ohne Adresse angelegt hat, verlangt die Rueckkehr zum Pflichtfeld ein Nachtragen oder Loeschen dieser Zeilen. Die Produktentscheidung ist vom Nutzer am 2026-09-09 gesperrt, daher kein Halt. + + + Neue Testfaelle in `ldap.service.spec.ts` — Konventionen des Bestands uebernehmen + (gemocktes `ldapts`, `prisma` als Objekt-Mock, `userService.create` als `vi.fn()`). + Der neue Kollisionscheck laeuft ueber `prisma.user.findUnique`; das ist im Bestand noch + nicht gemockt und muss dem Prisma-Mock hinzugefuegt werden (Vorgabe `null`). + + - Test 1 "legt ein Konto mit belegter Adresse trotzdem an, nur ohne Adresse": + zwei Eintraege mit derselben `mail`, `findUnique` liefert beim zweiten den bereits + angelegten Fremdbenutzer. Erwartung: `userService.create` wurde ZWEIMAL gerufen; + der erste Aufruf traegt die Adresse, der zweite traegt keine (`email` ist + `undefined`/nicht gesetzt). `result.created === 2`. + - Test 2 "meldet die Kollision strukturiert und nicht als Fehler": + `result.emailConflicts` enthaelt genau einen Eintrag mit Kontoname und der belegten + Adresse; `result.errors` ist leer. + - Test 3 "nimmt einem bestehenden Konto seine Adresse nicht weg": + ein bereits vorhandener Benutzer (Treffer ueber `findFirst`) bekommt eine Adresse + zugeordnet, die einem DRITTEN Benutzer gehoert. Erwartung: `prisma.user.update` wird + OHNE `email` im `data`-Objekt gerufen; die Kollision steht in `emailConflicts`. + - Test 4 "meldet Eintraege ohne Anmeldenamen getrennt und nicht als Fehler": + ein Eintrag ohne `sAMAccountName`. Erwartung: seine Kennung steht in + `result.skippedNoLogin`, `result.errors` ist leer. + - Test 5 "reicht keinen rohen Datenbanktext an den Bericht durch": + `userService.create` wirft einmal einen Fehler, dessen `message` den Wortlaut einer + ORM-Ausnahme traegt. Erwartung: die Kennung des Eintrags steht in + `result.entryFailures`; KEIN Element von `errors`, `entryFailures`, `skippedNoLogin` + oder der Kollisionsliste enthaelt den geworfenen Ausnahmetext (Vergleich per + `expect(JSON.stringify(result)).not.toContain()`). + + Vor der Umsetzung gegen den unveraenderten Bestand laufen lassen. Erwartete Rotfaerbung: + Test 1 scheitert, weil der zweite `create`-Aufruf die Adresse mitschickt; Test 2/4 scheitern, + weil die Felder nicht existieren; Test 3, weil `update` die Adresse mitschreibt; Test 5, + weil der Ausnahmetext woertlich in `errors` steht. Andere Ursachen erst klaeren. + + + + **Schritt 1 — Schema und Migration (nur schreiben, nicht ausfuehren).** + In `apps/api/prisma/schema.prisma:32` das Feld auf optional stellen (`String? @unique`). + Neues Verzeichnis `apps/api/prisma/migrations/20260909120000_user_email_optional/` mit + `migration.sql`, das die Pflicht auf der Spalte aufhebt (`ALTER TABLE "User" ALTER COLUMN + "email" DROP NOT NULL;`). Den vorhandenen Eindeutigkeitsindex NICHT anfassen: PostgreSQL + behandelt leere Werte in einer Eindeutigkeitsregel als jeweils verschieden, mehrere Konten + ohne Adresse sind also erlaubt. Kopfkommentar auf Deutsch im Stil der Bestandsmigrationen + (siehe `20260812110000_tender_rss_feed_owner/migration.sql`): Anlass, gesperrte + Nutzerentscheidung vom 2026-09-09, und der Hinweis, dass Bestandszeilen unangetastet + bleiben. Danach den Client neu erzeugen, damit die Typen die Optionalitaet kennen — + das ist reine Codegenerierung, kein Ausrollen. + + **Schritt 2 — geteilter Kollisionsentscheider in `ldap.service.ts`.** + Eine private Hilfsmethode ergaenzen, die zu einer gewuenschten Adresse und der Kennung + eines eventuell schon vorhandenen eigenen Datensatzes entscheidet, ob die Adresse + geschrieben werden darf. Sie fragt `this.prisma.user.findUnique({ where: { email } })` und + gibt die Adresse nur zurueck, wenn niemand sie haelt oder der Halter derselbe Datensatz + ist; andernfalls gibt sie keine Adresse und einen Kollisionsvermerk aus Kontoname und + Adresse zurueck. Bewusst `findUnique` und nicht `findFirst`: die Spalte ist eindeutig, und + die bestehende `findFirst`-Nutzung in `upsertMappedUser` bleibt dadurch eindeutig + unterscheidbar — auch im Test. + + In `upsertMappedUser` (ab Zeile 389) den Rueckgabetyp von `'created' | 'updated'` auf ein + Objekt erweitern, das den Status UND einen optionalen Kollisionsvermerk traegt. Im + Aktualisierungszweig (Zeile 410) die Adresse nur uebernehmen, wenn der Entscheider sie + freigibt — eine fremde Adresse wird niemals umgehaengt, der bestehende Wert bleibt stehen. + Im Anlagezweig (Zeile 419-427) die Adresse nur setzen, wenn sie freigegeben ist; ist sie + belegt, das Konto ohne Adresse anlegen. Der vorhandene Ersatzwert `${username}@ldap.local` + fuer Eintraege OHNE `mail`-Attribut bleibt unveraendert — er ist nicht Teil dieses Defekts + und wird hier ausdruecklich nicht angefasst. + + Denselben Entscheider auch in `importUsersByDn` an der Anlagestelle (Zeile 602-609) + verwenden, damit der Handimport nicht als zweiter Weg mit rohem Datenbanktext bestehen + bleibt. `LdapUserImportResult` bekommt dabei KEIN neues Feld: das Konto entsteht, wird als + `created` gezaehlt, und die Anzeige dieses Wegs bleibt unveraendert. + + **Schritt 3 — strukturierter Sync-Bericht.** + `LdapSyncResult` (ab Zeile 11) um drei Felder ergaenzen: eine Liste der Konten, die ohne + Adresse angelegt oder aktualisiert wurden (je Eintrag Kontoname und belegte Adresse); eine + Liste der Kennungen, die mangels Anmeldenamen uebersprungen wurden; eine Liste der + Kennungen, bei denen ein unerwarteter Fehler auftrat. Die Initialisierung bei Zeile 768 + entsprechend ergaenzen — es ist die einzige Stelle im Code, an der ein `LdapSyncResult` + entsteht. + + In `syncUsersForTenant`: die Meldung bei Zeile 861-863 durch einen Eintrag in der + Uebersprungen-Liste ersetzen; den generischen Auffang bei Zeile 880-886 so umbauen, dass + nur noch die Kennung des Eintrags in die Fehlerliste wandert, waehrend der technische + Wortlaut ueber `this.logger.error` ins Serverprotokoll geht und den Bericht nie erreicht. + Den Kollisionsvermerk aus `upsertMappedUser` in die Kollisionsliste uebernehmen. + + Nicht anfassen: `result.errors` traegt weiterhin die Lauffehler (`Sync failed: ...`) und + die Gruppenmeldungen aus `syncBoundGroupsForTenant`/`syncGroupMembershipsForTenant`. Diese + sind bereits deutsche Prosa und nicht Gegenstand dieses Defekts. + + **Schritt 4 — Folgeanpassungen der Typkette.** + `UserService.create` (`user.service.ts:49-58`) nimmt die Adresse ab jetzt optional + entgegen. Die Typpruefung wird nach der Neuerzeugung des Clients an zwei weiteren Stellen + rot, weil eine Adresse jetzt leer sein kann; beide sind Mailversand und beide muessen den + Empfaenger ueberspringen statt zu senden: `tender-digest.scheduler.ts:140` und + `tender-matching.service.ts:131` — vor dem Aufruf abbrechen, wenn der Benutzer keine + Adresse hat, und mit `continue` zum naechsten weitergehen (die umgebenden Schleifen tun das + bei fehlendem Benutzer bereits genauso). Das ist genau das zugesagte Verhalten: das Konto + funktioniert, nur Benachrichtigungen erreichen es nicht. Tauchen weitere Fundstellen auf, + nach demselben Muster behandeln und im SUMMARY vermerken. + + + + cd /home/vicolab/projects/tessera-ctl/apps/api && DATABASE_URL="postgresql://u:p@localhost:5432/db" npx prisma validate && DATABASE_URL="postgresql://u:p@localhost:5432/db" npx prisma generate && cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/api exec vitest run src/ldap/ldap.service.spec.ts && pnpm --filter @tessera/api exec vitest run && pnpm --filter @tessera/api type-check + + + + Die fuenf neuen Testfaelle sind gruen, die 62 bestehenden Testfaelle der Datei ebenfalls, + und die vollstaendige API-Testsuite laeuft durch. `prisma validate` bestaetigt das Schema, + `pnpm --filter @tessera/api type-check` ist sauber. Die Migrationsdatei existiert und wurde + NICHT ausgefuehrt. Eigener Commit, getrennt von Task 1 und Task 3. + + + + + Task 3: Verstaendlicher deutscher Sync-Bericht und Konten ohne Adresse in der Oberflaeche (WINDOWS #15, Anzeige) + apps/web/src/app/(portal)/admin/ldap/page.tsx, apps/web/src/app/(portal)/admin/users/page.tsx, apps/web/src/app/(portal)/admin/groups/components/GroupMembersModal.tsx, apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json, apps/web/src/messages/umlaut-dictionary.ts + + + Neuer Testfall in `groups-page.test.tsx`, im bestehenden + `describe('GroupMembersModal (via AdminGroupsPage)')`: + "Die Benutzersuche stuerzt bei einem Konto ohne Adresse nicht ab". `buildRouterFetchMock` + so erweitern, dass der `/users`-Zweig (heute `[]`) einen Benutzer ohne Adresse liefert + (`{ id: 'u12', username: 'funktionskonto', displayName: null, email: null }`). Im Test die + Mitgliederansicht der Gruppe 'Buchhaltung' oeffnen, in das Suchfeld mit dem Platzhalter + 'Benutzer suchen...' den Begriff 'funktion' tippen und erwarten, dass 'funktionskonto' in + der Ergebnisliste erscheint. + + Vor der Umsetzung gegen den unveraenderten Bestand laufen lassen. Erwartete Rotfaerbung: + `GroupMembersModal.tsx:104` ruft `.toLowerCase()` auf einem leeren Wert auf und die + Darstellung bricht ab. Ist die Ursache eine andere, erst klaeren. + + + + **Sync-Bericht.** In `apps/web/src/app/(portal)/admin/ldap/page.tsx` das `interface + SyncResult` (Zeile 62-76) um die drei in Task 2 ergaenzten Felder erweitern — gleiche + Namen, gleiche Formen wie im Backend, sonst bleibt der Bericht leer. Im Anzeigeblock + (Zeile 1312-1351) unter den drei Zahlenzeilen drei getrennte Abschnitte ergaenzen, jeweils + nur wenn die zugehoerige Liste nicht leer ist, und in dieser Reihenfolge: + + 1. Konten ohne Adresse — braucht Aufmerksamkeit, aber es ist kein Fehler. Farbe wie die + vorhandene Bernstein-Zeile `defaultMarkerMoved` (`text-amber-700 dark:text-amber-400`). + Ueberschriftssatz aus dem Sprachkatalog, darunter je Eintrag eine Zeile mit Kontoname + und belegter Adresse. + 2. Uebersprungene Eintraege ohne Anmeldenamen — neutraler Hinweis, Farbe + `text-muted-foreground`, ausdruecklich NICHT `text-destructive`. Ueberschriftssatz aus + dem Katalog, darunter die Kennungen als Datenzeilen. + 3. Unerwartete Fehler je Eintrag — `text-destructive`, Ueberschriftssatz aus dem Katalog mit + dem Hinweis, dass die technischen Angaben im Serverprotokoll stehen, darunter die + Kennungen. + + Der bestehende `errors`-Block bleibt unveraendert bestehen: er traegt weiterhin Lauf- und + Gruppenfehler. + + **Sprachkatalog.** Unter `admin.ldap.sync` in BEIDE Dateien drei Ueberschriftsschluessel + und einen Zeilenschluessel fuer die Kollision ergaenzen. Inhaltlich verbindlich (Wortlaut + darf gefeilt werden, Aussage nicht): + - Kollision, Ueberschrift: diese Konten wurden ohne E-Mail-Adresse angelegt, weil die + Adresse bereits zu einem anderen Konto gehoert; Anmeldung und Zugriff funktionieren, + nur Benachrichtigungen per E-Mail erreichen sie nicht. + - Kollision, Zeile: Kontoname und die bereits vergebene Adresse, mit Platzhaltern + `{account}` und `{email}`. + - Uebersprungen, Ueberschrift: ohne Anmeldenamen uebersprungen — normal fuer Kontakte und + Verteiler im Verzeichnis. + - Unerwarteter Fehler, Ueberschrift: bei diesen Eintraegen trat ein unerwarteter Fehler + auf, die technischen Angaben stehen im Protokoll des Servers. + Kein Fachbegriff, kein Produktname einer Bibliothek, keine englische Wendung im deutschen + Text. Jedes neue deutsche Wort mit der Folge `ae`/`oe`/`ue`/`ss`, das noch nicht in + `UMLAUT_ALLOWLIST` steht, dort ergaenzen — die Fehlermeldung des Gates nennt das Wort + woertlich, und `Adresse`, `muss`, `Passwort` stehen bereits drin. + + **Konten ohne Adresse in der Oberflaeche.** In `GroupMembersModal.tsx` das Feld im + `interface TenantUser` als moeglicherweise leer deklarieren und den Suchvergleich bei + Zeile 104 gegen einen leeren Wert absichern — nach demselben Muster, das die Zeile darueber + fuer `displayName` bereits verwendet. Die Anzeige bei Zeile 233 bleibt wie sie ist und + stellt bei leerem Wert nichts dar. In `apps/web/src/app/(portal)/admin/users/page.tsx` das + Feld im `interface User` (Zeile 13) ebenfalls als moeglicherweise leer deklarieren; die + Uebernahme in das Formular (Zeile 96) faellt dann auf eine leere Zeichenkette zurueck, damit + das Eingabefeld ein kontrolliertes Feld bleibt, und die Tabellenzelle (Zeile 225) zeigt bei + leerem Wert einen Gedankenstrich statt einer leeren Zelle. `UserFormData.email` bleibt eine + Pflichtangabe: die Anlage von Hand verlangt weiterhin eine Adresse. + + + + cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run "src/app/(portal)/admin/groups/groups-page.test.tsx" "src/messages/umlaut-guard.spec.ts" && pnpm --filter @tessera/web exec vitest run && pnpm --filter @tessera/web type-check + + + + Der neue Testfall ist gruen, die uebrigen Testfaelle von `groups-page.test.tsx` ebenfalls, + und die vollstaendige Web-Testsuite laeuft durch. Das Sprachschluessel-Gate ist gruen, also + stehen alle neuen Schluessel in de.json UND en.json, und das Umlaut-Gate meldet kein + ungepruefte neues Wort. `pnpm --filter @tessera/web type-check` ist sauber. Eigener Commit, + getrennt von Task 1 und Task 2. + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Active Directory -> Tessera-Sync | Fremdgepflegte Verzeichnisdaten (Anzeigename, mail, sAMAccountName) fliessen ungeprueft in eigene Datensaetze. | +| API -> Administrations-Oberflaeche | Der Sync-Bericht transportiert Servertexte in den Browser eines Administrators. | +| Nutzereingabe -> Freigaben-Matrix | Der Suchbegriff steuert, welche Kaestchen sichtbar und damit klickbar sind. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-Q3-01 | Spoofing | `upsertMappedUser` Aktualisierungszweig (`ldap.service.ts:410`) | high | mitigate | Eine Adresse wird NIE von einem Datensatz auf einen anderen umgehaengt. Der Entscheider vergleicht die Kennung des Halters mit der des eigenen Datensatzes; passt sie nicht, bleibt die Adresse beim bisherigen Konto. Sonst koennte ein Verzeichniseintrag die Adresse eines echten Menschen an sich ziehen und darueber dessen Passwort-Zuruecksetzung empfangen. Abgesichert durch Test 3 aus Task 2. | +| T-Q3-02 | Information Disclosure | Sync-Bericht (`ldap.service.ts:885` -> `admin/ldap/page.tsx:1341`) | medium | mitigate | Roher Ausnahmetext der Datenbankschicht nennt Tabellen-, Spalten- und Aufrufnamen und wird einem Administrator im Klartext gezeigt. Ab jetzt wandert nur die Kennung des Eintrags in den Bericht, der technische Wortlaut ausschliesslich ins Serverprotokoll. Abgesichert durch Test 5 aus Task 2. | +| T-Q3-03 | Information Disclosure | Auflistung der Verzeichniskennungen im Bericht | low | accept | Die Seite ist bereits auf ADMIN/SUPER_ADMIN beschraenkt, und wer den Sync ausloest, kennt das Verzeichnis ohnehin. Kein zusaetzlicher Schutz. | +| T-Q3-04 | Spoofing | Passwort-Zuruecksetzung bei leerer Adresse (`auth.service.ts:143-146`) | medium | mitigate | Die Suche laeuft ueber eine angefragte Zeichenkette; ein leerer Wert in der Spalte wird davon nie getroffen, ein Konto ohne Adresse ist also ueber diesen Weg nicht ansprechbar — genau die zugesagte Wirkung. Keine Codeaenderung noetig, aber vor Abschluss zu bestaetigen. | +| T-Q3-05 | Denial of Service | Zusaetzliche Adressabfrage je Verzeichniseintrag | low | accept | Eine zusaetzliche indizierte Punktabfrage je Eintrag; bei den gemessenen 405 Konten vernachlaessigbar, und der Sync laeuft ohnehin nicht im Anfragepfad eines Nutzers. | +| T-Q3-06 | Tampering | Anzeige der Matrix nach Suche | low | accept | Die Suche veraendert ausschliesslich die Sichtbarkeit; jeder Umschaltvorgang laeuft weiterhin ueber den bestehenden, serverseitig geprueften Endpunkt. Eine gefilterte Ansicht kann keine Freigabe setzen, die der Server nicht erlaubt. | +| T-Q3-SC | Tampering | Paketinstallationen | low | accept | Dieser Plan installiert kein einziges Paket (npm/pip/cargo). Das Legitimitaets-Gate greift daher nicht; entsteht beim Umsetzen doch ein Installationsbedarf, ist die Arbeit zu stoppen und der Bedarf vorzulegen. | + + + +Nach allen drei Tasks, vom Wurzelverzeichnis aus: + +``` +pnpm --filter @tessera/api exec vitest run +pnpm --filter @tessera/api type-check +pnpm --filter @tessera/web exec vitest run +pnpm --filter @tessera/web type-check +``` + +Alle vier muessen sauber durchlaufen. Der Ausgangsstand vor der Arbeit ist gemessen: +`ldap.service.spec.ts` 62/62 gruen, `grants-matrix.test.tsx` 8/8 gruen, API-Typpruefung +sauber. + +**Vom Nutzer im Browser nachzuholen (nicht durch den Ausfuehrenden, siehe Konvention "Kein +Docker-Deploy auf Testserver"), nach seinem naechsten Neubau von API und Web:** + +1. Freigaben-Matrix oeffnen und nach `Claude_VT` bzw. `Vertrieb` suchen: die Gruppenspalte + bleibt stehen UND alle Modulzeilen bleiben stehen, es gibt anklickbare Kaestchen. +2. Dieselbe Matrix, Suche nach `Cert`: die Modulzeile bleibt stehen UND alle Gruppenspalten + bleiben stehen. +3. Dieselbe Matrix, Begriff ohne Treffer: eine verstaendliche Meldung statt einer leeren + Tabelle. +4. AD-Sync ausloesen: die vier Konten `uvertrieb_ro`, `uvertrieb_rw`, `uvertrieb_ro_ss` und + `usoftware_rw` erscheinen ab jetzt in der Benutzerliste — ohne Adresse. Das Konto, das + `mbuntz@ctl.de` bereits haelt, behaelt die Adresse unveraendert. +5. Derselbe Bericht: keine Zeile enthaelt englischen Techniktext oder Datenbankwortlaut; die + sechs Kontakte ohne Anmeldenamen stehen als neutraler Hinweis, nicht in Rot. + + + +1. Suche nach einem Gruppennamen laesst alle Modulzeilen stehen; Suche nach einem Modulnamen + laesst alle Gruppenspalten stehen; in beiden Faellen bleibt mindestens ein Kaestchen + klickbar. Belegt durch zwei Testfaelle, die gegen den heutigen Stand rot sind. +2. Die Spaltensuche findet eine importierte Gruppe weiterhin unter internem UND AD-Namen. +3. Ein Begriff ohne Treffer erzeugt eine sichtbare Meldung, keine stumm leere Tabelle. +4. Ein Verzeichniskonto mit bereits belegter Adresse wird angelegt — ohne Adresse — und der + bisherige Halter der Adresse behaelt sie. +5. Kein Bestandteil des Sync-Berichts traegt rohen Datenbank-Ausnahmetext oder englischen + Techniktext; die technischen Angaben stehen nur im Serverprotokoll. +6. Uebersprungene Eintraege ohne Anmeldenamen erscheinen als neutraler Hinweis, getrennt von + echten Fehlern. +7. Ein Konto ohne Adresse bricht weder die Benutzerliste noch die Mitgliedersuche. +8. Alle neuen Anzeigetexte stehen in de.json UND en.json; beide Sprachgates sind gruen. +9. Beide Testsuiten und beide Typpruefungen sind vollstaendig gruen. +10. Drei getrennte Commits: Defekt A, Verhalten von Defekt B, Anzeige von Defekt B. + + + +Dieser Plan rollt NICHTS aus: kein `docker compose build`, kein `up`, kein `restart`, kein +`prisma migrate deploy`, keine Aktion auf dem Testserver. Die Migration wird ausschliesslich +als Datei geschrieben. + +Fuer den Nutzer: die Migration steckt im API-Image und wird vom Startbefehl des Containers +angewandt (`apps/api/Dockerfile:38` ruft `prisma migrate deploy` vor dem Start auf). Ein +Neubau und Neustart der API genuegt also. Solange das nicht geschehen ist, bleibt die Spalte +ein Pflichtfeld und die vier Konten werden weiterhin nicht angelegt — dann aber mit einer +verstaendlichen Zeile im Bericht statt mit rohem Techniktext. + + + +Create `.planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-SUMMARY.md` when done +