The user has no write access to the company AD, so tests 1, 3, 4 and 8 cannot be run there, and building a throwaway domain controller for them was judged disproportionate now that the one substantive defect at this spot is found and fixed (UAT test 2 -> quick task 260811-f9i). Recorded rather than hidden: A1 (objectGUID survives a rename) now rests on Microsoft's documentation, not on our own measurement. The Tessera-side rename and delete logic stays covered by unit tests against fixtures. If a rename ever fails to propagate in production, 16-VERIFICATION.md names that as the starting point. Phase 16 marked complete in STATE.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
24 KiB
phase, verified, status, accepted_at, accepted_by, score, behavior_unverified, overrides_applied, human_verification, behavior_unverified_items
| phase | verified | status | accepted_at | accepted_by | score | behavior_unverified | overrides_applied | human_verification | behavior_unverified_items | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 16-ad-gruppen-synchronisation | 2026-08-06T15:07:32Z | accepted_with_open_assumptions | 2026-08-11 | user | 5/5 must-haves (code-level) — 2 davon PRESENT_BEHAVIOR_UNVERIFIED wegen ungeprüfter Live-AD-Annahmen; A2 am 2026-08-11 geprüft, widerlegt und der gefundene Fehler behoben (260811-f9i) | 2 | 0 |
|
|
Phase 16: AD-Gruppen-Synchronisation Verification Report
Nachtrag 2026-08-11 — Abschluss der offenen Punkte
Die sechs human_verification-Punkte unten sind am 2026-08-11 abgearbeitet
worden, mit einem substanziellen Fund und einer bewussten Entscheidung:
Geprüft und bestanden (Browser, alpha.tessera.ctl.de, Playwright): die
Import-Sektion, der Gruppen-Dialog in allen drei Zuständen, der Fehlerpfad des
Sync-Berichts und die Spaltensuche der Freigabe-Matrix unter internem wie
AD-Namen. Details in 16-UAT.md, Tests 5 bis 7.
Geprüft und WIDERLEGT — Annahme A2: der binäre (objectGUID=...)-Filter
wurde als escapter String gebaut und fand am echten AD nie etwas, auch nicht für
existierende Objekte. Beide Suchen der Existenzprüfung teilten sich diesen
Filter, also hätte der erste echte Sync-Lauf jede AD-gebundene Gruppe samt
Mitgliedschaften und Modulfreigaben gelöscht. Behoben in Quick-Task 260811-f9i
(Commit d2019dc): EqualityFilter über den rohen Buffer, gegengeprüft am
echten Verzeichnis, plus zwei Regressionstests. Damit ist SC-4 nicht mehr
PRESENT_BEHAVIOR_UNVERIFIED, sondern in seiner kritischen Mechanik belegt.
Bewusst nicht geprüft — Annahme A1 und die End-to-End-Läufe (Tests 1, 3, 4, 8):
der User hat keine Schreibrechte im AD, und der Ersatzweg über einen eigenen
Wegwerf-Domänencontroller wurde als unverhältnismäßig verworfen, nachdem der eine
substanzielle Fehler bereits gefunden war. A1 (objectGUID übersteht eine
Umbenennung) stützt sich damit auf die Microsoft-Dokumentation, nicht auf eine
eigene Messung. Die Tessera-seitige Umbenennungs- und Löschlogik ist durch
Unit-Tests gegen Testdaten belegt. Fällt im Betrieb auf, dass eine Umbenennung
nicht nachzieht, ist das der Ansatzpunkt — siehe 16-UAT.md, Abschnitt
"Entscheidung 2026-08-11".
Ebenfalls offen und bewusst so belassen: die Zahlenzeilen eines ERFOLGREICHEN Sync-Laufs (Test 7, zweiter Teil). Die lassen sich ohne AD-Rechte nachholen, sobald einmal ein Sync mit eng gesetztem Filter auf alpha läuft.
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:65behauptet noch „AD-Bindung setzen/lösen" überPATCH /groups/:id, obwohl das seit D-07 nicht mehr möglich ist (das DTO selbst wurde korrekt aktualisiert). Kosmetisch, keine funktionale Auswirkung. - IN-02 —
internalNameträgt kein@MaxLength()inupdate-group.dto.ts. Kein Sicherheitsrisiko (Spalte istTEXT, 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 sichtbarentext-*-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-*.shexistiert 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:
- 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. - Browser-Durchklick GroupFormModal + Gruppenliste (WINDOWS.md #5) — alle drei Dialogzustände, Fehleranzeige, Namensanzeige nach dem Speichern.
- Browser-Durchklick Sync-Bericht + Freigabe-Matrix (WINDOWS.md #6) — alle Berichtszeilen inkl. Amber-Zeile, Matrix-Spaltensuche unter beiden Namen.
- Backstop-Aussagen aus den
must_haves.truthsder 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)