From d7f8448be5d765f2d72b1c3615bba36efeb3a561 Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 6 Aug 2026 17:10:38 +0200 Subject: [PATCH] test(16): persist human verification items as UAT --- .../16-ad-gruppen-synchronisation/16-UAT.md | 62 ++++++++ .../16-VERIFICATION.md | 149 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 .planning/phases/16-ad-gruppen-synchronisation/16-UAT.md create mode 100644 .planning/phases/16-ad-gruppen-synchronisation/16-VERIFICATION.md diff --git a/.planning/phases/16-ad-gruppen-synchronisation/16-UAT.md b/.planning/phases/16-ad-gruppen-synchronisation/16-UAT.md new file mode 100644 index 0000000..80e87ad --- /dev/null +++ b/.planning/phases/16-ad-gruppen-synchronisation/16-UAT.md @@ -0,0 +1,62 @@ +--- +status: testing +phase: 16-ad-gruppen-synchronisation +source: [16-VERIFICATION.md] +started: 2026-08-06T15:15:00Z +updated: 2026-08-06T15:15:00Z +--- + +## Current Test + +number: 1 +name: AD-Umbenennung behaelt dieselbe Kennung (Annahme A1) +expected: | + Eine im Active Directory umbenannte Gruppe behaelt ihre unveraenderliche + objectGUID. Nur dann kann der Sync eine Umbenennung von einem Verschwinden + unterscheiden — die Grundlage von Erfolgskriterium 3. +awaiting: user response + +## Tests + +### 1. AD-Umbenennung behaelt dieselbe Kennung (Annahme A1) +expected: Gegen ein echtes AD (ViCoTest, balios.ctl.local), read-only — eine importierte Gruppe suchen, ihre objectGUID notieren, die Gruppe im AD umbenennen, erneut suchen, GUID vergleichen. Die GUID muss identisch bleiben. +result: [pending] + +### 2. Binaerer Existenz-Filter liefert korrekte Treffer (Annahme A2) +expected: Gegen dasselbe AD, read-only — eine Suche mit dem byteweise escapten (objectGUID=...)-Binaerfilter absetzen. Eine weiterhin existierende Gruppe muss einen Treffer liefern, eine tatsaechlich geloeschte keinen. Ein negatives Ergebnis bei Test 1 ODER 2 ist ein Stopp-Grund fuer die Loeschsemantik aus D-05. +result: [pending] + +### 3. Umbenennung im AD zieht in Tessera nach (SC-3, end-to-end) +expected: Gruppe im echten AD umbenennen, Sync laufen lassen. Group.name und Group.ldapDn ziehen nach, der Zaehler groupsRenamed steigt, es findet KEINE Loesch-und-Neuanlage statt, Mitgliedschaften und Modulfreigaben bleiben erhalten, ein gesetzter interner Name bleibt unveraendert. +result: [pending] + +### 4. Loeschung im AD entfernt die Gruppe — Verschiebung nicht (SC-4, end-to-end) +expected: Gruppe im echten AD loeschen, Sync laufen lassen — die Tessera-Gruppe wird samt GroupMembership und ModuleGrant kaskadierend entfernt, und war sie die Standardgruppe, wandert die Markierung weiter. Zusaetzlich: eine lediglich in eine andere OU VERSCHOBENE Gruppe darf NICHT geloescht werden, sondern erzeugt eine Fehlerzeile (WR-03-Fix). +result: [pending] + +### 5. Browser: Import-Sektion auf /admin/ldap +expected: Sektion "AD-Gruppen importieren" — Discovery liefert nur Gruppen (keine OUs), Checkbox-Auswahl funktioniert, Import-Button traegt die Auswahlzahl und ist bei leerer Auswahl deaktiviert, bereits importierte Gruppen tragen das Badge und sind deaktiviert, Ergebnisblock erscheint, Fehlerzustaende sind sichtbar (nicht still). Deckt die als `verification: backstop` markierten UI-Zustaende ab: leer, ladend, Fehler, Overflow, lange Namen. +result: [pending] + +### 6. Browser: Gruppen-Dialog in allen drei Zustaenden +expected: /admin/groups — Anlegen zeigt den Hinweis mit Link zum LDAP-Bereich und keine AD-Auswahl mehr; eine lokale Gruppe laesst sich umbenennen; bei einer importierten Gruppe ist das Namensfeld sichtbar gesperrt mit Herkunftshinweis, die AD-DN-Zeile ist sichtbar, der interne Name speichert korrekt, und ein Fehler beim Speichern bleibt im Dialog sichtbar mit erhaltenen Eingaben. +result: [pending] + +### 7. Browser: Sync-Bericht und Freigabe-Matrix +expected: /admin/ldap — Sync ausloesen, alle drei Zahlenzeilen erscheinen auch bei Werten von 0; die gelbe Zeile zur verschobenen Standardmarkierung erscheint nur, wenn das tatsaechlich passiert ist; ein fehlgeschlagener Sync zeigt eine Fehlermeldung statt eines Null-Berichts. /admin/modules/grants — die Spaltensuche findet eine Gruppe sowohl unter ihrem internen als auch unter ihrem AD-Namen. +result: [pending] + +### 8. Nebenlaeufigkeit und Layoutstress (backstop-Aussagen) +expected: Kein beobachtbares Fehlverhalten bei parallelen Anfragen, bei Reihenfolgeunabhaengigkeit, bei Sortier-Divergenz zwischen AD-Name und internem Namen, und bei sehr langen Texten in Listen und Dialogen. Diese Aussagen sind bewusst als `verification: backstop` deklariert — kein Code-Beleg reicht zu ihrer Bestaetigung. +result: [pending] + +## Summary + +total: 8 +passed: 0 +issues: 0 +pending: 8 +skipped: 0 +blocked: 0 + +## Gaps diff --git a/.planning/phases/16-ad-gruppen-synchronisation/16-VERIFICATION.md b/.planning/phases/16-ad-gruppen-synchronisation/16-VERIFICATION.md new file mode 100644 index 0000000..4d71866 --- /dev/null +++ b/.planning/phases/16-ad-gruppen-synchronisation/16-VERIFICATION.md @@ -0,0 +1,149 @@ +--- +phase: 16-ad-gruppen-synchronisation +verified: 2026-08-06T15:07:32Z +status: human_needed +score: 5/5 must-haves (code-level) — 2 davon PRESENT_BEHAVIOR_UNVERIFIED wegen ungeprüfter Live-AD-Annahmen +behavior_unverified: 2 +overrides_applied: 0 +human_verification: + - test: "Read-only gegen ein echtes Active Directory (ViCoTest, balios.ctl.local): eine importierte Gruppe suchen, ihren objectGUID notieren, die Gruppe im AD umbenennen, erneut suchen, GUID vergleichen (Annahme A1)." + expected: "objectGUID bleibt über die Umbenennung hinweg identisch — nur dann kann der Sync eine Umbenennung von einem Verschwinden unterscheiden (SC-3 Grundlage)." + why_human: "Kein Active Directory in dieser Umgebung erreichbar; die Unterscheidung Rename-vs-Delete ist ausschließlich gegen gemockte LDAP-Antworten getestet, nie gegen ein echtes Verzeichnis." + - test: "Read-only gegen dasselbe AD: eine Suche mit dem byteweise escapten (objectGUID=...)-Binärfilter absetzen und prüfen, ob sie exakt die erwartete Gruppe zurückliefert (Annahme A2)." + expected: "Der binäre Existenz-Sweep liefert bei einer weiterhin existierenden Gruppe einen Treffer und bei einer tatsächlich gelöschten Gruppe keinen — sonst würde SC-4 entweder fälschlich löschen oder fälschlich nicht löschen." + why_human: "RFC-4515-Byte-Escaping für Binärwerte ist nur gegen ldapts-Mocks verifiziert, nicht gegen die tatsächliche Filter-Interpretation eines realen Domain Controllers. Ein negatives Ergebnis bei A1 oder A2 ist laut RESEARCH.md/16-03-PLAN.md ein Stopp-Grund für die D-05-Löschsemantik." + - test: "Browser-Durchklick: /admin/ldap → Sektion 'AD-Gruppen importieren' — Discovery, Checkbox-Auswahl, Import-Button mit Auswahlzahl, alreadyImported-Badge, sichtbare Fehlerzustände." + expected: "Deckt sich mit den in 16-01-PLAN.md must_haves als `verification: backstop` markierten UI-Zuständen (leer, ladend, Fehler, Overflow, lange Namen) — keiner davon ist automatisiert testbar." + why_human: "Kein Browser-Tool in den Ausführungssitzungen verfügbar; WINDOWS.md Eintrag #5 (16-04) und #6 (16-05) sind offen." + - test: "Browser-Durchklick: /admin/groups → GroupFormModal in allen drei Zuständen (Anlegen mit Hinweis-Link, lokale Gruppe umbenennen, importierte Gruppe mit gesperrtem Namensfeld + internem Namen speichern inkl. 409-Fehleranzeige)." + expected: "Gesperrtes Feld ist visuell erkennbar, AD-DN-Zeile sichtbar, interner Name speichert korrekt, Fehlerzustand bleibt im Dialog sichtbar mit erhaltenen Eingaben." + why_human: "WINDOWS.md #5 — kein Browser-Tool verfügbar; nur Codepfad-Review, keine gerenderte Prüfung." + - test: "Browser-Durchklick: /admin/ldap → Sync auslösen, alle drei/vier Berichtszeilen prüfen (inkl. Amber-Zeile bei verschobener Standardmarkierung); /admin/modules/grants → Spaltensuche unter internem und AD-Namen." + expected: "Bericht zeigt Benutzer-, Mitgliedschafts- und Gruppenzahlen auch bei 0; Amber-Zeile erscheint nur bei defaultMarkerMoved > 0; Matrix-Suche findet die Gruppe unter beiden Namen." + why_human: "WINDOWS.md #6 — kein Browser-Tool verfügbar." + - test: "Alle mit `verification: backstop` markierten must_haves aus 16-01/16-02/16-04/16-05-PLAN.md (u. a. gleichzeitige/parallele Requests, Reihenfolgeunabhängigkeit, Sortier-Divergenz zwischen name und internalName, lange Texte in Listen/Modals)." + expected: "Kein beobachtbares Fehlverhalten unter Nebenläufigkeit oder Layoutstress." + why_human: "Explizit als `verification: backstop` deklariert — kein Code-Beleg reicht zur Bestätigung; Race-Conditions und CSS-Overflow-Verhalten sind nicht durch die vorhandenen Unit-/Komponententests abgedeckt." +behavior_unverified_items: + - truth: "SC-3: Wird eine übernommene Gruppe im AD umbenannt, zieht der Name der Tessera-Gruppe beim nächsten Sync nach, weil objectGUID über die Umbenennung hinweg stabil bleibt (Annahme A1)." + test: "Gruppe im echten AD umbenennen, Sync laufen lassen, prüfen dass Group.name/ldapDn nachziehen und KEINE Lösch+Neuanlage stattfindet." + expected: "Ein Rename-Zweig greift (groupsRenamed++), kein Delete+Create-Paar." + why_human: "Die Rename-vs-Delete-Unterscheidung hängt vollständig an A1, die nur gegen Mocks getestet ist (siehe ldap.service.spec.ts), nie gegen ein echtes Verzeichnis." + - truth: "SC-4: Verschwindet eine übernommene Gruppe aus dem AD, wird die Tessera-Gruppe samt Mitgliedschaften und Modulfreigaben entfernt — vorausgesetzt der binäre Existenz-Sweep (Annahme A2) funktioniert gegen ein reales AD korrekt." + test: "Gruppe im echten AD löschen, Sync laufen lassen, prüfen dass die Tessera-Gruppe samt GroupMembership/ModuleGrant kaskadierend entfernt wird — UND dass eine lediglich verschobene (nicht gelöschte) Gruppe NICHT gelöscht wird (WR-03-Fix)." + expected: "Löschung nur bei echtem Verschwinden; eine Verschiebung außerhalb der konfigurierten Base-DN erzeugt eine Fehlerzeile statt einer Löschung." + why_human: "A2 (byteweise Hex-Filter-Syntax) ist nicht gegen einen echten Domain Controller geprüft; ein negatives Ergebnis würde SC-4 in beide Richtungen falsch machen können (WINDOWS.md #4)." +--- + +# Phase 16: AD-Gruppen-Synchronisation Verification Report + +**Phase Goal:** Ausgewählte AD-Gruppen werden als Tessera-Gruppen angelegt und dauerhaft nachgeführt, statt jede Gruppe von Hand anzulegen und zu binden. Der Admin wählt aus der AD-Gruppenliste aus, welche Gruppen übernommen werden; der bestehende Benutzer-Sync hält Bestand, Namen und Mitgliedschaften danach aktuell. +**Verified:** 2026-08-06T15:07:32Z +**Status:** human_needed +**Re-verification:** No — initial verification + +## Zusammenfassung der Prüfung + +Alle fünf Erfolgskriterien sind im Code vollständig, sauber verdrahtet und durch Unit-Tests belegt — ich habe jede Behauptung aus den fünf SUMMARY.md-Dateien gegen die tatsächlichen Quelldateien gegengeprüft, nicht nur die Zusammenfassungen gelesen. Zwei Punkte verhindern trotzdem ein glattes „passed": Erstens hängen SC-3 (Umbenennung) und SC-4 (Löschung) an zwei Annahmen über das Verhalten eines echten Active Directory (A1: `objectGUID` übersteht eine Umbenennung; A2: die byteweise Hex-Filter-Syntax wird von einem realen Domain Controller akzeptiert), die in dieser Umgebung nicht gegen ein echtes Verzeichnis geprüft werden konnten — nur gegen Mocks. Zweitens sind mehrere manuelle Browser-Durchklicke und eine Reihe von Plan-intern als `verification: backstop` markierten Aussagen (Nebenläufigkeit, Sortierabweichungen, Layout-Overflow) nicht automatisiert nachweisbar. Beides ist in den Plan-Artefakten selbst bereits ehrlich als offen dokumentiert (WINDOWS.md #4/#5/#6) — diese Verifikation bestätigt das, statt es zu überschreiben. + +## Goal Achievement + +### Observable Truths (fünf Erfolgskriterien) + +| # | Truth | Status | Evidence | +| --- | --- | --- | --- | +| SC-1 | Admin wählt AD-Gruppen einzeln aus und importiert sie; je Auswahl entsteht eine Tessera-Gruppe mit `ldapDn` + `ldapObjectGuid` | ✓ VERIFIED | `apps/api/src/ldap/ldap.service.ts:643` `importGroupsByDn()` legt pro DN genau eine `Group`-Zeile mit `name`, `ldapDn`, `ldapObjectGuid` an (`tenantPrisma.group.create`); `POST /ldap/groups/import` (`ldap.controller.ts:190`) unter `@Roles(ADMIN, SUPER_ADMIN)`; UI-Sektion „AD-Gruppen importieren" in `admin/ldap/page.tsx` (Checkbox-Auswahl, Import-Button mit Auswahlzahl). 60 grüne Fälle in `ldap.service.spec.ts`, inkl. Doppel-Import (skip), Namenskollision (reject-with-report), fehlender objectGUID-Buffer. | +| SC-2 | Nicht ausgewählte AD-Gruppen werden nicht angelegt — keine OU-weite Automatik | ✓ VERIFIED | `importGroupsByDn(config, tenantId, dns)` verarbeitet ausschließlich die im `dns`-Array übergebenen Einträge; kein Codepfad iteriert über ein Suchergebnis oder eine OU. `ImportGroupsDto` lehnt ein leeres Array ab (`@ArrayNotEmpty()`). Frontend postet nur `selectedImportDns`. | +| SC-3 | Umbenennung im AD zieht beim nächsten Sync in `Group.name`/`Group.ldapDn` nach, Mitgliedschaften/Freigaben bleiben erhalten | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code vollständig vorhanden und korrekt verdrahtet: `syncBoundGroupsForTenant()` (`ldap.service.ts:1223`) läuft nachweislich vor `syncGroupMembershipsForTenant()` (Zeile 920 vor 927, Call-Order-Test vorhanden), Rename-Zweig schreibt `name`/`ldapDn` byte-exakt aus dem AD, `internalName` bleibt unangetastet (`grep -c internalName` im Methodenkörper = 0), case-insensitive Change-Detection (WR-04-Fix) verhindert Idempotenz-Verletzung durch Groß-/Kleinschreibung. **Aber:** die gesamte Unterscheidung Rename-vs-Delete hängt an Annahme A1 (`objectGUID` bleibt über eine AD-Umbenennung stabil), die nur gegen `ldapts`-Mocks getestet ist — nie gegen ein echtes Verzeichnis (WINDOWS.md #4). Mitgliedschaften/Freigaben werden bei einem reinen Rename nicht angefasst (kein `group.delete`/`create` im Rename-Zweig), das ist strukturell korrekt, sofern A1 hält. | +| SC-4 | Verschwinden im AD löscht die Tessera-Gruppe samt Mitgliedschaften und Modulfreigaben | ⚠️ PRESENT_BEHAVIOR_UNVERIFIED | Code vollständig vorhanden: binärer Existenz-Sweep mit 32-Hex-Validierung vor jeder Filter-Interpolation, `reassignDefaultBeforeDelete()` läuft nachweislich vor `group.delete()` (Zeilenreihenfolge geprüft), Cascade auf `GroupMembership`/`ModuleGrant` existiert seit Migration 15-01, `ensureDefaultGroup()` läuft nach jeder Löschung. Der Review-Fund WR-03 (fälschliche Löschung bei reiner AD-Verschiebung außerhalb der Base-DN) ist behoben und mit drei neuen Tests abgesichert (Commit `00de6a6`). **Aber:** ob der binäre `(objectGUID=...)`-Filter (Annahme A2) von einem echten Domain Controller tatsächlich als Treffer/Nicht-Treffer korrekt interpretiert wird, ist nicht geprüft — nur simuliert. Ein negatives Ergebnis wäre laut RESEARCH.md/16-03-PLAN.md ein Stopp-Grund für die Löschsemantik. | +| SC-5 | Lokal angelegte Gruppen ohne AD-Bindung bleiben vom Gruppen-Sync unberührt | ✓ VERIFIED | Kandidaten-Query in `syncBoundGroupsForTenant()` filtert `OR: [{ldapObjectGuid: {not: null}}, {ldapDn: {not: null}}]` — eine rein lokale Gruppe (beide Felder `null`) erfüllt keine der beiden Bedingungen und erscheint nie im Ergebnis, löst also keine LDAP-Suche und keine Schreiboperation aus. Zusätzlich verhindert `GroupFormModal.tsx` (nach D-07-Rückbau) jede nachträgliche AD-Bindung einer lokalen Gruppe: kein Radio-Element (`grep -c 'type="radio"'` = 0), kein Discovery-Aufruf (`grep -c 'ldap/groups'` = 0), `UpdateGroupDto` besitzt kein `ldapDn`-Feld mehr. | + +**Score:** 5/5 Erfolgskriterien code-vollständig; 3/5 zusätzlich per Unit-Test gegen echtes Laufzeitverhalten bewiesen (SC-1, SC-2, SC-5); 2/5 (SC-3, SC-4) präsent und verdrahtet, aber deren Kernannahme (A1/A2) nicht gegen ein echtes Active Directory geprüft. + +### D-Entscheidungen (Auswahl der wichtigsten, code-geprüft) + +| Entscheidung | Status | Evidence | +| --- | --- | --- | +| D-03 (Namensfeld importierter Gruppen serverseitig gesperrt) | ✓ VERIFIED | `GroupsService.update()` wirft `BadRequestException`, wenn `existing.ldapObjectGuid \|\| existing.ldapDn` gesetzt ist (Zeile 84 in `groups.service.ts`) — das ist der Review-Fund WR-01, der ursprünglich nur `ldapObjectGuid` prüfte und damit Alt-Bindungen vor dem ersten Sync-Lauf umgehbar machte; Fix in Commit `dd59bf5` bestätigt im Code vorhanden. | +| D-04 (interner Name, sync-immun, vier Anzeigestellen) | ✓ VERIFIED | Spalte existiert (Schema + Migration + laufende DB bestätigt), `syncBoundGroupsForTenant()` schreibt `internalName` nirgends, alle vier geplanten Anzeigestellen bestätigt: `listForTenant()`, `module-grants.service.ts#getUserAccess` (beide Projektionen), `admin/groups/page.tsx` Namenszelle, `admin/modules/grants/page.tsx` Spaltenkopf — überall Nullish-Fallback `internalName ?? name`, keine Truthiness-Variante gefunden. | +| D-05 (Löschung im AD löscht ohne Warndialog, bewusst akzeptiert) | ✓ VERIFIED (Sichtbarkeitsmaßnahme) | Kein Warndialog im Sync-Pfad (bewusst); stattdessen zeigt der Sync-Bericht `groupsDeleted`/`defaultMarkerMoved` in `admin/ldap/page.tsx` — die einzige laut D-05 vorgesehene Gegenmaßnahme ist vorhanden. | +| D-06 (Standardmarkierung wandert vor Löschung) | ✓ VERIFIED | `reassignDefaultBeforeDelete()` deterministisch (DEFAULT_GROUP_NAME zuerst, sonst älteste Gruppe per `createdAt asc`), nicht werfend, wird in `syncBoundGroupsForTenant()` nachweislich vor `group.delete()` aufgerufen; `ensureDefaultGroup()` läuft nach jeder Löschung. 6 gezielte Testfälle grün. | +| D-07 (lokale Gruppe kann nicht mehr an AD gebunden werden) | ✓ VERIFIED | Radio-Auswahl vollständig aus `GroupFormModal.tsx` entfernt; `UpdateGroupDto` ohne `ldapDn`-Feld; Kandidaten-Query des Sync berührt lokale Gruppen nie. | + +## Requirements Coverage + +| Requirement | Quelle | Beschreibung | Status | Evidence | +| --- | --- | --- | --- | --- | +| PERM-02 | Alle 5 Pläne (`requirements: [PERM-02]`) | Selektiver AD-Gruppen-Import mit Nachführung (Rename/Delete), lokale Gruppen unberührt, keine nachträgliche Bindung | ✓ SATISFIED auf Code-Ebene, mit offener Live-Verifikationsschuld | REQUIREMENTS.md markiert PERM-02 als `[x]` und „Complete" seit Plan 16-05 (2026-08-06). Diese Markierung ist auf Code-Vollständigkeits-Ebene gerechtfertigt — alle fünf Erfolgskriterien sind implementiert, verdrahtet und wo automatisiert prüfbar auch getestet. Sie verschweigt aber nicht die offene Live-AD-Prüfung: 16-03-SUMMARY.md, 16-05-SUMMARY.md und WINDOWS.md #4 halten A1/A2 ausdrücklich als unverifiziert fest und benennen ein negatives Ergebnis explizit als Stopp-Grund für die D-05-Löschsemantik. Die Requirements-Traceability-Tabelle selbst transportiert diese Nuance nicht (sie kennt nur „Complete"/„Pending") — das ist eine Grenze des Tools, keine Falschaussage der Pläne. | + +Keine verwaisten Requirement-IDs gefunden — PERM-02 ist die einzige der Phase zugeordnete ID und wird durchgängig in allen fünf Plan-Frontmatter-Blöcken referenziert. + +## Required Artifacts + +| Artifact | Erwartet | Status | Details | +| --- | --- | --- | --- | +| `apps/api/prisma/schema.prisma` | `Group.internalName`, `Group.ldapObjectGuid`, `@@unique([tenantId, ldapObjectGuid])` | ✓ VERIFIED | Zeilen 143-153 bestätigt | +| `apps/api/prisma/migrations/20260806133916_add_group_internal_name_and_object_guid/` | Versionierte Migration | ✓ VERIFIED | Existiert, per `migration-sql.spec.ts` (14 Fälle) festgenagelt, **und** live gegen die lokale DB angewendet (`psql \d "Group"` zeigt beide Spalten + Unique-Index; `_prisma_migrations` bestätigt `finished_at` gesetzt) | +| `apps/api/src/ldap/ldap.service.ts` | `listGroups()` mit `alreadyImported`, `importGroupsByDn()`, `syncBoundGroupsForTenant()`, `escapeLdapFilterBuffer()` | ✓ VERIFIED | Alle vier Symbole vorhanden und aufgerufen (kein toter Code) | +| `apps/api/src/ldap/ldap.controller.ts` | `POST /ldap/groups/import` unter RBAC | ✓ VERIFIED | Zeile 190, `@Roles(ADMIN, SUPER_ADMIN)`, statische Route vor jeder künftigen `:id`-Route dokumentiert | +| `apps/api/src/groups/groups.service.ts` | `DEFAULT_GROUP_NAME`, `reassignDefaultBeforeDelete()`, Namenssperre, `internalName`-Handling | ✓ VERIFIED | Wie oben belegt | +| `apps/api/src/groups/dto/update-group.dto.ts` | `internalName` statt `ldapDn` | ✓ VERIFIED | `ldapDn` nicht mehr vorhanden, `internalName?: string \| null` vorhanden | +| `apps/api/src/groups/module-grants.service.ts` | `internalName ?? name` an beiden Projektionsstellen | ✓ VERIFIED | Zeilen 25/34/49 bestätigt | +| `apps/web/.../admin/ldap/page.tsx` | Import-Sektion, erweiterter Sync-Bericht, `syncRequestError` | ✓ VERIFIED | Alle State-Variablen, Handler und Berichtszeilen vorhanden; `deactivated: 0`-Fallback-Objekt entfernt | +| `apps/web/.../admin/groups/components/GroupFormModal.tsx` | Drei Zustände ohne AD-Bindung | ✓ VERIFIED | Kein Radio, kein Discovery-Aufruf, ein einziger `PATCH`-Aufrufblock | +| `apps/web/.../admin/groups/page.tsx` | Namenszelle mit Fallback + Tooltip | ✓ VERIFIED | `internalName ?? group.name` mit `title={group.name}` | +| `apps/web/.../admin/modules/grants/page.tsx` | Spaltenkopf + Suchfilter mit Fallback | ✓ VERIFIED | Beide Stellen bestätigt, Nullish-Variante | +| `apps/web/src/messages/{de,en}.json` | Neue Schlüssel, verwaiste `ldapBind.*` entfernt | ✓ VERIFIED | Node-Paritätsprüfungen aus allen Plänen liefen zuletzt grün mit; keine erneute Prüfung nötig, da `tsc`/`vitest` diese implizit mitprüfen | + +## Key Link Verification + +| From | To | Via | Status | Details | +| --- | --- | --- | --- | --- | +| `GroupFormModal.tsx` (importierte Gruppe) | `PATCH /groups/:id` | `{ internalName }`, niemals `name` | ✓ WIRED | Payload-Verzweigung über `isImported`, serverseitig durch `GroupsService.update()`-Sperre abgesichert (defense-in-depth, nicht nur UI) | +| `syncBoundGroupsForTenant()` | `syncGroupMembershipsForTenant()` | Aufrufreihenfolge in `syncUsersForTenant()` | ✓ WIRED | Zeile 920 vor Zeile 927, zusätzlich durch Call-Order-Spy-Test abgesichert | +| `syncBoundGroupsForTenant()` (Löschzweig) | `GroupsService.reassignDefaultBeforeDelete()` → `group.delete()` → `ensureDefaultGroup()` | Reihenfolge im Methodenkörper | ✓ WIRED | Grep-Zeilenvergleich bestätigt Reihenfolge; `ensureDefaultGroup` läuft einmal nach der Schleife bei mind. einer Löschung | +| `LdapService.importGroupsByDn` | `forTenant(prisma, tenantId).group.create` | RLS-Scoping | ✓ WIRED | Bestätigt in Zeile ~700 | +| Backend `LdapSyncResult` | Frontend `SyncResult` | Feldnamen-Parität | ✓ WIRED | `groupsAdopted/groupsRenamed/groupsDeleted/defaultMarkerMoved/groupMembershipsAdded/groupMembershipsRemoved` — alle sechs Felder im Frontend-Interface vorhanden, Namen identisch | + +## Anti-Patterns + +Keine Debt-Marker (`TBD`/`FIXME`/`XXX`/`TODO`/`HACK`/`PLACEHOLDER`) in den elf geprüften Kern-Dateien der Phase gefunden. Zwei vom Code-Review dokumentierte, bewusst offen gelassene Info-Findings bleiben unverändert bestehen (nicht blockierend, laut 16-REVIEW.md-Frontmatter `open_findings: [IN-01, IN-02]`): + +- **IN-01** — veralteter Kommentar in `groups.controller.ts:65` behauptet noch „AD-Bindung setzen/lösen" über `PATCH /groups/:id`, obwohl das seit D-07 nicht mehr möglich ist (das DTO selbst wurde korrekt aktualisiert). Kosmetisch, keine funktionale Auswirkung. +- **IN-02** — `internalName` trägt kein `@MaxLength()` in `update-group.dto.ts`. Kein Sicherheitsrisiko (Spalte ist `TEXT`, RLS greift), aber ein potenziell sehr langer Wert würde unbegrenzt persistiert. + +Diese beiden Punkte sind Informationsfunde des Code-Reviews, keine neuen Beobachtungen dieser Verifikation, und wurden bewusst nicht behoben (siehe `16-REVIEW.md` Frontmatter). Sie sind kein Grund für `gaps_found`. + +## Anti-Patterns explizit NICHT gefunden + +- Kein stiller `catch {}` in den geänderten Frontend-Handlern (Discovery, Import, Sync, GroupFormModal-Speichern) — alle vier zeigen einen sichtbaren `text-*-destructive`-Zustand. +- Keine Truthiness-Fallbacks (`internalName ||`) an den vier Anzeigestellen — durchgängig `??`. +- Kein zweiter Schreibpfad, der eine AD-Bindung setzen könnte. + +## Testlage + +- `cd apps/api && npx vitest run` — 40 Dateien, 566 Tests, grün. +- `cd apps/web && npx vitest run` — 32 Dateien, 192 Tests, grün. +- `cd apps/api && npx tsc --noEmit` — fehlerfrei. +- `cd apps/web && npx tsc --noEmit` — fehlerfrei. +- Migration live gegen die lokale Datenbank angewendet und per `\d "Group"` gegengeprüft. +- Gezielter Re-Run der drei phasenrelevanten Testdateien (`ldap.service.spec.ts`, `groups.service.spec.ts`, `module-grants.service.spec.ts`): 130 Tests, grün. +- Kein Probe-Skript für diese Phase vorgesehen (`scripts/*/tests/probe-*.sh` existiert nicht für Phase 16) — Schritt 7c entfällt. + +## Human Verification Required + +Siehe YAML-Frontmatter `human_verification` oben für die vollständige, strukturierte Liste. Zusammengefasst, in Prioritätsreihenfolge: + +1. **A1/A2 Live-AD-Prüfung (höchste Priorität, WINDOWS.md #4)** — read-only gegen ViCoTest (`balios.ctl.local`): objectGUID-Stabilität über eine Umbenennung, byteweise Hex-Filter-Treffer. Ein negatives Ergebnis ist laut Plan-Vorgabe selbst ein Stopp-Grund für die Löschsemantik aus D-05 und müsste zu einer neuen Planungsentscheidung führen, nicht zu einem stillen Codefix. +2. **Browser-Durchklick GroupFormModal + Gruppenliste (WINDOWS.md #5)** — alle drei Dialogzustände, Fehleranzeige, Namensanzeige nach dem Speichern. +3. **Browser-Durchklick Sync-Bericht + Freigabe-Matrix (WINDOWS.md #6)** — alle Berichtszeilen inkl. Amber-Zeile, Matrix-Spaltensuche unter beiden Namen. +4. **Backstop-Aussagen** aus den `must_haves.truths` der fünf Pläne (Nebenläufigkeit bei parallelen Import-/Sync-Requests, Reihenfolgeunabhängigkeit, lange Texte in Listen/Dialogen) — laut eigener Plan-Deklaration nicht automatisiert prüfbar. + +## Gaps Summary + +Kein Gap im Sinne von „fehlender oder kaputter Code" gefunden. Alle Artefakte existieren, sind substanziell (keine Stubs), verdrahtet und (soweit automatisiert möglich) getestet. Die Einstufung `human_needed` statt `passed` beruht ausschließlich darauf, dass zwei der fünf Erfolgskriterien an einer gegen echtes Active-Directory-Verhalten ungeprüften Annahme hängen, und dass mehrere manuelle Verifikationsschritte in dieser Umgebung nicht ausführbar waren (kein Browser-Tool, kein erreichbares AD). Das ist keine neue Erkenntnis dieser Verifikation — alle fünf SUMMARY.md-Dateien und WINDOWS.md dokumentieren diese Lücken bereits selbst und ehrlich; diese Verifikation bestätigt sie unabhängig, statt sie zu wiederholen oder zu übergehen. + +--- + +_Verified: 2026-08-06T15:07:32Z_ +_Verifier: Claude (gsd-verifier)_