diff --git a/.planning/phases/16-ad-gruppen-synchronisation/16-01-SUMMARY.md b/.planning/phases/16-ad-gruppen-synchronisation/16-01-SUMMARY.md new file mode 100644 index 0000000..b34d5ee --- /dev/null +++ b/.planning/phases/16-ad-gruppen-synchronisation/16-01-SUMMARY.md @@ -0,0 +1,199 @@ +--- +phase: 16-ad-gruppen-synchronisation +plan: 01 +subsystem: auth +tags: [prisma, postgresql, nestjs, ldap, ldapts, nextjs, react, rls] + +# Dependency graph +requires: + - phase: 15-gruppen-und-modulzuweisung + provides: "Group-Modell, forTenant()-RLS-Helper, GroupsService, tenant_isolation_policy auf Group/GroupMembership/ModuleGrant" +provides: + - "Group.internalName + Group.ldapObjectGuid Spalten samt Unique-Index (tenantId, ldapObjectGuid), versioniert und lokal angewendet (D-04)" + - "LdapService.listGroups(config, tenantId) mit alreadyImported-Flag und stabiler Sortierung" + - "LdapService.importGroupsByDn(config, tenantId, dns) — selektiver Import mit Namenskollisions- und Duplikat-Behandlung" + - "LdapService.escapeLdapFilterBuffer() — binäres LDAP-Filter-Escaping (noch ungenutzt, Vorbereitung für Plan 16-03)" + - "POST /ldap/groups/import Route (RBAC ADMIN/SUPER_ADMIN)" + - "Sektion 'AD-Gruppen importieren' in /admin/ldap mit sichtbaren Fehlerzuständen" +affects: [16-02-anzeige-fallback, 16-03-rekonziliation, 16-04-dialog-umbau, 16-05-anzeige-fallback-frontend] + +# Actuals (#2632) +actuals: + tokens: 10020 + tasks: 3 + commits: 2 + +# Tech tracking +tech-stack: + added: [] + patterns: + - "explicitBufferAttributes: ['objectGUID'] bei jeder LDAP-Suche, die objectGUID liest — ldapts würde sonst UTF-8-dekodieren und den 16-Byte-Wert korrumpieren" + - "Reject-with-Report je Zeile statt Batch-Abbruch: P2002 auf (tenantId, name) -> nameCollisions, P2002 auf (tenantId, ldapObjectGuid) -> skipped, alles andere -> errors" + - "Baselinen einer lokalen Dev-Datenbank via `prisma migrate resolve --applied ` je Altmigration, bevor `prisma migrate deploy` nur die neue Migration anwendet — Ersatz für `migrate dev`, das in dieser Shell keine TTY hat" + +key-files: + created: + - apps/api/prisma/migrations/20260806133916_add_group_internal_name_and_object_guid/migration.sql + modified: + - apps/api/prisma/schema.prisma + - apps/api/src/ldap/ldap.service.ts + - apps/api/src/ldap/ldap.controller.ts + - apps/api/src/ldap/dto/ldap-config.dto.ts + - apps/api/src/ldap/ldap.service.spec.ts + - apps/api/src/groups/migration-sql.spec.ts + - apps/web/src/app/(portal)/admin/ldap/page.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + +key-decisions: + - "Checkpoint 1 (Task 1, checkpoint:decision): approve-both — beide nullable Spalten (internalName, ldapObjectGuid) plus Unique-Index in einer versionierten Migration. Freigegeben 2026-08-06." + - "Post-Tracer human-verify-Checkpoint: verified — die Tracer-Scheibe (Diff gelesen, alle automatisierten Verifies grün) wurde geprüft und akzeptiert." + - "Drei kosmetische Beobachtungen aus dem Checkpoint-Review bewusst zurückgestellt (siehe Abschnitt unten) — keine davon blockiert die Success Criteria dieses Plans." + - "Migrationsverfahren angepasst: `prisma migrate dev` verweigert in dieser nicht-interaktiven Shell den Betrieb (auch mit CI=true, gepipetem stdin oder --create-only). Statt eine Pseudo-TTY zu erzwingen wurde der äquivalente, nicht-interaktive Weg genutzt: `prisma migrate diff` liefert die exakte SQL, die Migration wurde von Hand als Datei angelegt (Stil identisch zu den bestehenden hand-editierten Migrationen des Repos) und per `prisma migrate deploy` angewendet." + +patterns-established: + - "Nicht-interaktive lokale Migrationen: migrate diff (SQL erzeugen) -> Datei von Hand anlegen -> migrate deploy (anwenden), statt migrate dev in einer Shell ohne TTY" + +requirements-completed: [PERM-02] + +coverage: + - id: D1 + description: "Group.internalName und Group.ldapObjectGuid existieren in Schema, Migration und laufender lokaler Datenbank samt Unique-Index (tenantId, ldapObjectGuid) (D-04)" + requirement: "PERM-02" + verification: + - kind: unit + ref: "apps/api/src/groups/migration-sql.spec.ts#add_group_internal_name_and_object_guid migration.sql (D-04)" + status: pass + - kind: other + ref: "docker exec tessera-ctl-db-1 psql -U tessera -d tessera -c '\\d \"Group\"' — zeigt internalName, ldapObjectGuid, Group_tenantId_ldapObjectGuid_key" + status: pass + human_judgment: false + - id: D2 + description: "LdapService.listGroups()/importGroupsByDn() — Discovery mit alreadyImported-Flag und stabiler Sortierung, Import mit Duplikat-/Namenskollisions-Behandlung, Buffer-basiertem objectGUID-Handling (SC-1, SC-2, D-01, D-02)" + requirement: "PERM-02" + verification: + - kind: unit + ref: "apps/api/src/ldap/ldap.service.spec.ts#LdapService — AD group import (SC-1/SC-2, D-01/D-02) (8 it-Fälle)" + status: pass + human_judgment: false + - id: D3 + description: "POST /ldap/groups/import Route hinter @Roles(ADMIN, SUPER_ADMIN), tenantId ausschließlich aus req.tenantId, ImportGroupsDto lehnt leeres dns-Array ab" + requirement: "PERM-02" + verification: + - kind: unit + ref: "apps/api/src/ldap/ldap.service.spec.ts (Controller-Verhalten indirekt über Service-Vertrag) + acceptance-criteria grep auf ldap.controller.ts" + status: pass + human_judgment: false + - id: D4 + description: "Sektion 'AD-Gruppen importieren' in /admin/ldap — Auswahl, Import-Button mit Auswahlzahl, alreadyImported-Badge, sichtbare Fehlerzustände statt stillem Verschlucken" + verification: + - kind: unit + ref: "cd apps/web && npx tsc --noEmit (typkorrekt) + acceptance-criteria grep auf handleDiscoverGroupsToImport/handleImportGroups (setGroupImportError >= 4x)" + status: pass + human_judgment: true + rationale: "Visuelle Interaktion (Checkbox-Zustände, Badge-Styling, Fehlerbanner) wurde im Rahmen des Post-Tracer-Checkpoints per Code-Review als 'verified' bestätigt, aber nicht gegen einen live gerenderten Browser erneut geprüft — echte UAT bleibt sinnvoll, bevor Plan 16-02 darauf aufbaut." + +duration: 34min +completed: 2026-08-06 +status: complete +--- + +# Phase 16 Plan 1: AD-Gruppen-Import (Tracer + Schema) Summary + +**Selektiver AD-Gruppen-Import über alle Schichten (Prisma bis Next.js), rename-stabile Identität via objectGUID, Migration lokal angewendet und per SQL-Test festgenagelt** + +## Performance + +- **Duration:** 34 min (Tracer-Commit 15:09 Uhr bis Migrations-Commit 15:43 Uhr; Checkpoint-Wartezeiten nicht mitgerechnet) +- **Started:** 2026-08-06T13:09:35Z (Tracer-Commit) +- **Completed:** 2026-08-06T13:43:19Z (Migrations-Commit) +- **Tasks:** 3 (Checkpoint:decision, Tracer, Migration) +- **Files modified:** 10 + +## Accomplishments +- `Group.internalName` (sync-immuner Anzeigename) und `Group.ldapObjectGuid` (rename-stabiler Identitätsschlüssel) als versionierte, one-way-Migration angelegt, lokal angewendet und per SQL-Textest festgenagelt +- `LdapService.listGroups()` erweitert um `alreadyImported`-Kennzeichnung und stabile Sortierung; `LdapService.importGroupsByDn()` neu, mit Duplikat-Erkennung (skip), Namenskollisions-Meldung (reject-with-report) und Fehlerzeilen statt Batch-Abbruch +- `objectGUID` wird korrekt als `Buffer` gelesen (`explicitBufferAttributes`) und als 32-stelliger Hex-String gespeichert — der zuvor als offene Annahme geführte Punkt (RESEARCH.md A2) ist für den Schreibpfad bewiesen +- `POST /ldap/groups/import` (RBAC ADMIN/SUPER_ADMIN) und neue Sektion "AD-Gruppen importieren" in `/admin/ldap` mit sichtbaren Discovery-/Import-Fehlerzuständen (bewusste Abweichung vom sonst stillen Fehler-Verschlucken der Seite) +- Vollständige API-Testsuite grün: 40 Testdateien, 526 Tests + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Freigabe der one-way Schema-Erweiterung (D-04)** — `checkpoint:decision`, kein eigener Commit; Freigabe `approve-both` +2. **Task 2: Tracer — AD-Gruppe auswählen und importieren, durch alle Schichten** - `3523e43` (feat, tdd) +3. **Task 3: Migration erzeugen, gegen die lokale Datenbank anwenden, per SQL-Test festnageln** - `626e296` (feat) + +**Plan metadata:** wird nach diesem Summary committet + +## Files Created/Modified +- `apps/api/prisma/schema.prisma` - `Group.internalName`, `Group.ldapObjectGuid`, `@@unique([tenantId, ldapObjectGuid])` +- `apps/api/prisma/migrations/20260806133916_add_group_internal_name_and_object_guid/migration.sql` - neue, versionierte Migration (zwei `ADD COLUMN`, ein `CREATE UNIQUE INDEX`, keine RLS-Anweisung) +- `apps/api/src/ldap/ldap.service.ts` - `listGroups(config, tenantId)` mit `alreadyImported`, `importGroupsByDn()`, `escapeLdapFilterBuffer()`, `LdapGroupImportResult` +- `apps/api/src/ldap/ldap.controller.ts` - `GET groups` reicht `tenantId` durch, neue Route `POST groups/import` +- `apps/api/src/ldap/dto/ldap-config.dto.ts` - `ImportGroupsDto` +- `apps/api/src/ldap/ldap.service.spec.ts` - neuer describe-Block "LdapService — AD group import (SC-1/SC-2, D-01/D-02)" (8 Fälle + Sortierung + alreadyImported) +- `apps/api/src/groups/migration-sql.spec.ts` - neuer describe-Block für die neue Migration (4 Fälle) +- `apps/web/src/app/(portal)/admin/ldap/page.tsx` - Sektion "AD-Gruppen importieren", sieben neue State-Variablen, zwei Handler mit sichtbarem Fehlerzustand +- `apps/web/src/messages/de.json` / `en.json` - `admin.ldap.groupImport.*` + +## Decisions Made + +- **Checkpoint 1 (Task 1):** `approve-both` — beide Spalten plus Unique-Index in einer Migration. Freigegeben 2026-08-06 vom Projektinhaber. +- **Post-Tracer-Checkpoint:** `verified` — Tracer-Scheibe per Code-Review akzeptiert, bevor Task 3 (Migration) begonnen wurde. +- **Drei zurückgestellte kosmetische Beobachtungen aus dem Review** (bewusst nicht umgesetzt in diesem Plan): + 1. `listGroups()` und `existingByGuid` in `importGroupsByDn()` nutzen `this.prisma` statt `tenantPrisma` — beide Lesepfade filtern aber explizit auf `tenantId`, kein Cross-Tenant-Leck. Belassen wie ist. + 2. `escapeLdapFilterBuffer()` ist aktuell ungenutzt — Plan 16-03 ist der erste Aufrufer. Belassen im Code. + 3. Legacy-Gruppen mit `ldapDn`, aber ohne `ldapObjectGuid`, melden `alreadyImported: false`. Wird durch das GUID-Backfill in Plan 16-03 aufgelöst. Belassen wie ist. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] `prisma migrate dev` verweigert nicht-interaktive Shell — Migrationsablauf über `migrate diff` + Handdatei + `migrate deploy` ersetzt** +- **Found during:** Task 3 +- **Issue:** Der geplante Befehl `prisma migrate dev --name add_group_internal_name_and_object_guid` bricht in dieser Shell sofort mit "Prisma Migrate has detected that the environment is non-interactive" ab — auch mit `CI=true`, gepipetem `y`-stdin und `--create-only`. Ein Versuch, über `script` eine Pseudo-TTY zu erzwingen, hing unbegrenzt an einem echten interaktiven Prompt und musste per SIGKILL beendet werden. +- **Fix:** Äquivalenter, vollständig nicht-interaktiver Weg: `prisma migrate diff --from-migrations ... --to-schema-datamodel ... --script` erzeugt exakt dieselbe SQL, die dann von Hand in ein neues Migrationsverzeichnis (`20260806133916_add_group_internal_name_and_object_guid/migration.sql`) geschrieben wurde — im Stil der bereits im Repo vorhandenen hand-editierten Migrationen. `prisma migrate deploy` (nicht-interaktiv per Design) hat sie anschließend angewendet. +- **Files modified:** apps/api/prisma/migrations/20260806133916_add_group_internal_name_and_object_guid/migration.sql +- **Verification:** `prisma migrate status` zeigt "Database schema is up to date!"; `\d "Group"` zeigt beide Spalten und den Unique-Index +- **Committed in:** 626e296 + +**2. [Rule 3 - Blocking] `_prisma_migrations`-Tabelle fehlte in der lokalen Datenbank — Baseline nachgezogen** +- **Found during:** Task 3 +- **Issue:** Nach dem SIGKILL des hängenden `script`-Prozesses (siehe Punkt 1) meldete `prisma migrate status`, dass ALLE 25 Migrationen unangewendet seien, und `\dt` zeigte, dass die Tabelle `_prisma_migrations` in der Datenbank gar nicht existierte — obwohl alle 28 Anwendungstabellen inklusive der bereits angewendeten Group-RLS-Policy vorhanden und `Group` mit 0 Zeilen leer war. Kein Datenverlust, aber die Migrationshistorie war nicht (mehr) nachverfolgbar. +- **Fix:** Vor dem Anwenden der neuen Migration wurden die 24 bereits im Schema sichtbaren Altmigrationen einzeln per `prisma migrate resolve --applied ` als bereits angewendet markiert (reine Metadaten-Operation, keine DDL). Erst danach lieferte `migrate status` korrekt genau eine ausstehende Migration (die neue), die per `migrate deploy` angewendet wurde. +- **Files modified:** keine Codedateien — nur Datenbank-Metadaten (`_prisma_migrations`-Tabelle) auf dem lokalen Dev-Container +- **Verification:** `SELECT migration_name, finished_at FROM "_prisma_migrations"` zeigt alle 25 Migrationen mit `finished_at` gesetzt; volle Testsuite (526 Tests) grün +- **Committed in:** 626e296 (kein DB-Zustand wird versioniert; die Baseline-Aktion selbst ist nicht committbar) + +**3. [Rule 1 - Bug] Eigene Migrations-Kommentarzeile enthielt versehentlich das Suchmuster "row level security"** +- **Found during:** Task 3 +- **Issue:** Der erklärende Kommentar in der neuen `migration.sql` begründete den fehlenden RLS-Schritt mit dem wörtlichen Ausdruck "FORCE ROW LEVEL SECURITY" — dadurch schlug sowohl das Acceptance-Criterion (`grep -ci 'row level security'` soll 0 ergeben) als auch der eigene neue Spec-Test fehl. +- **Fix:** Kommentar umformuliert, ohne die Begründung zu verlieren ("die entsprechende Absicherung" statt der wörtlichen SQL-Phrase); Spec-Test auf ein Regex-Muster für tatsächliche `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY`-Anweisungen umgestellt statt auf reinen Substring-Match. +- **Files modified:** apps/api/prisma/migrations/20260806133916_add_group_internal_name_and_object_guid/migration.sql, apps/api/src/groups/migration-sql.spec.ts +- **Verification:** `grep -ci 'row level security' migration.sql` = 0; `npx vitest run src/groups/migration-sql.spec.ts` grün (14 Tests) +- **Committed in:** 626e296 + +--- + +**Total deviations:** 3 auto-fixed (2 blocking / Migrationsverfahren, 1 Bug in eigenem Kommentartext) +**Impact on plan:** Alle drei Anpassungen waren notwendig, um Task 3 überhaupt abzuschließen bzw. um die eigenen Acceptance-Kriterien zu erfüllen. Kein Scope Creep — die Migration selbst entspricht exakt dem im Plan vorgesehenen Inhalt (zwei nullable Spalten, ein Unique-Index, keine RLS-Anweisung). + +## Issues Encountered +- Siehe Deviations oben — alle drei Punkte betrafen ausschließlich das Migrationsverfahren in dieser nicht-interaktiven Shell, nicht die fachliche Logik des Plans. + +## User Setup Required + +None - keine externe Service-Konfiguration erforderlich. Der Testserver wurde von diesem Plan nicht angefasst; `prisma migrate deploy` läuft dort ohnehin automatisch beim Container-Start (`apps/api/Dockerfile:38`). + +## Next Phase Readiness + +- Beide Identitätsspalten (`internalName`, `ldapObjectGuid`) existieren jetzt in Schema, Migration und laufender lokaler Datenbank — Plan 16-03 (Rekonziliation/Rename-Erkennung) kann darauf aufsetzen +- `escapeLdapFilterBuffer()` liegt bereit, aber ungenutzt, für den binären Existenz-Sweep in Plan 16-03 +- Die drei zurückgestellten kosmetischen Beobachtungen (siehe Decisions) sind dokumentiert und sollten bei der Planung von 16-02/16-03 im Blick behalten werden +- **Flagged assumption weiterhin offen:** RESEARCH.md A1 (objectGUID bleibt über AD-Umbenennung stabil) und A2 (Binärfilter-Syntax) sind gegen kein echtes Verzeichnis geprüft — dieser Plan schreibt die GUID nur, liest sie nicht filternd zurück. Live-Prüfung ist Bestandteil von Plan 16-03 gegen ViCoTest. + +--- +*Phase: 16-ad-gruppen-synchronisation* +*Completed: 2026-08-06*