dbeab8c38a
WINDOWS #14 (Suche leert die jeweils andere Achse der Freigaben-Matrix) und WINDOWS #15 (roher Prisma-/englischer Techniktext im AD-Sync, vier Konten wegen geteilter E-Mail-Adresse nie importiert) als drei getrennte Tasks. Gesperrte Nutzerentscheidung vom 2026-09-09: kollidierende Konten werden angelegt, nur ohne Adresse. Dafuer wird User.email optional — Migration wird geschrieben, nicht ausgefuehrt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
521 lines
34 KiB
Markdown
521 lines
34 KiB
Markdown
---
|
|
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)."
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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.
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Matrix-Suche filtert nur noch die getroffene Achse (WINDOWS #14)</name>
|
|
<files>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</files>
|
|
|
|
<behavior>
|
|
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.
|
|
</behavior>
|
|
|
|
<action>
|
|
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.`
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Kollidierende AD-Konten werden angelegt — ohne Adresse (WINDOWS #15, Verhalten)</name>
|
|
<files>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</files>
|
|
|
|
<reversibility rating="costly">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.</reversibility>
|
|
|
|
<behavior>
|
|
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(<geworfener Text>)`).
|
|
|
|
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.
|
|
</behavior>
|
|
|
|
<action>
|
|
**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.
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 3: Verstaendlicher deutscher Sync-Bericht und Konten ohne Adresse in der Oberflaeche (WINDOWS #15, Anzeige)</name>
|
|
<files>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</files>
|
|
|
|
<behavior>
|
|
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.
|
|
</behavior>
|
|
|
|
<action>
|
|
**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.
|
|
</action>
|
|
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## 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. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
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.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
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.
|
|
</success_criteria>
|
|
|
|
<deployment_note>
|
|
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.
|
|
</deployment_note>
|
|
|
|
<output>
|
|
Create `.planning/quick/260909-ab3-matrix-suche-und-sync-meldungen-reparier/260909-ab3-SUMMARY.md` when done
|
|
</output>
|