Files

45 KiB

Phase 16: AD-Gruppen-Synchronisation - Research

Researched: 2026-08-05 Domain: LDAP/AD-Gruppen-Import und -Nachführung auf einem bestehenden NestJS/Prisma/PostgreSQL-Stack mit Zeilenebenen-Mandantentrennung (RLS) Confidence: HIGH (Codebasis vollständig gelesen und live durch die installierte ldapts-Bibliothek verifiziert) / MEDIUM-LOW für das konkrete AD-Laufzeitverhalten von objectGUID (keine Live-AD-Verbindung in dieser Session möglich — siehe Assumptions Log)

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01: Der Import sitzt im LDAP-Bereich (/admin/ldap), nicht in der Gruppenverwaltung: AD-Gruppe auswählen, importieren — dasselbe Muster wie der dort bereits vorhandene Benutzer-Import. Die vorhandene AD-Gruppensuche wird wiederverwendet, keine zweite Suchmechanik.
  • D-02: Nur ausdrücklich ausgewählte Gruppen werden übernommen. Keine pauschale Übernahme einer OU oder eines Filters — Reversibility: reversible.
  • D-03: Group.name gehört dem AD. Wird die Gruppe dort umbenannt, zieht Tessera beim nächsten Sync nach. In Tessera ist das Feld für importierte Gruppen gesperrt, mit Hinweis auf die Herkunft.
  • D-04: Dazu kommt eine neue Spalte für einen internen Namen, die der Sync niemals anfasst. Ist sie gesetzt, zeigt die Oberfläche diesen Namen — in der Gruppenliste, als Spaltenkopf der Freigabe-Matrix und in den Chips im Benutzer-Detail. Der AD-Name bleibt im Bearbeiten-Dialog sichtbar, damit die Herkunft nachvollziehbar bleibt. Löst den Zielkonflikt zwischen „AD ist die Wahrheit" und „wir wollen einen eigenen Namen", ohne dass der Sync ihn zurücksetzt — Reversibility: one-way — neue Spalte plus Migration; nach Vergabe echter interner Namen ist ein Rückbau Datenverlust.
  • D-05: Verschwindet eine importierte Gruppe aus dem AD, wird die Tessera-Gruppe entfernt — samt Mitgliedschaften und Modulfreigaben. Die betroffenen Benutzer verlieren den darüber vergebenen Zugriff, ohne den Warndialog aus Phase 15 D-17; der greift nur beim Löschen über die Tessera-Oberfläche. Bewusst akzeptiert.
  • D-06: War die gelöschte Gruppe die markierte Standardgruppe, wandert die Markierung weiter statt zu verschwinden: bevorzugt auf die lokale Gruppe „Alle Benutzer", sonst auf eine andere vorhandene Gruppe. Es darf kein Zustand entstehen, in dem ein Mandant ohne Standardgruppe dasteht und neue Benutzer nirgends beitreten. Der Sync protokolliert den Wechsel.
  • D-07: Eine lokal angelegte Gruppe kann nicht an eine AD-Gruppe gebunden werden. Die in Plan 15-06 gebaute Radio-Auswahl im Anlegen- und Bearbeiten-Dialog wird wieder entfernt. Begründung: Mit dem internen Namen (D-04) entfällt ihr Hauptzweck, und zwei Wege zum selben Ergebnis sind eine Fehlerquelle. Bestehende Gruppen mit gesetztem ldapDn gelten als importiert und werden vom Sync verwaltet.

Claude's Discretion

  • Benennung und Typ der neuen Spalte für den internen Namen sowie die Migration
  • Ob der Import einen Sync sofort auslöst oder erst der nächste turnusmäßige Lauf die Mitglieder füllt
  • Form des Sync-Berichts: welche Zahlen zurückgemeldet werden (importiert, umbenannt, gelöscht, Markierung verschoben)
  • Verhalten bei Namenskollision, wenn eine importierte AD-Gruppe genauso heißt wie eine bestehende lokale Gruppe — @@unique([tenantId, name]) erzwingt hier eine Entscheidung
  • Ob der Import-Dialog bereits importierte Gruppen als solche kennzeichnet oder ausblendet

Deferred Ideas (OUT OF SCOPE)

  • DÖE-Ausschreibungen verlinken auf die API statt auf die Bekanntmachungsseite — separater, bereits diagnostizierter Todo, ausdrücklich erst nach Phase 16.
  • Verschachtelte AD-Gruppen (indirekte Mitgliedschaft über Untergruppen) — seit Phase 15 bewusst außerhalb.
  • OU-weite oder filterbasierte Automatik statt Einzelauswahl — vom User verworfen. </user_constraints>

<phase_requirements>

Phase Requirements

Für Phase 16 sind noch keine REQ-IDs vergeben (werden in /gsd-plan-phase 16 vergeben). Die Abdeckung wird stattdessen direkt aus den fünf Roadmap-Success-Criteria und den Entscheidungen D-01..D-07 abgeleitet:

Kriterium Beschreibung Research Support
SC-1 Admin wählt AD-Gruppen aus der Liste, jede ausgewählte Gruppe entsteht als Tessera-Gruppe mit gesetzter AD-Bindung Import-Endpoint-Muster (Q1), stabile Identität via objectGUID (Q2), neue Spalten (Q3)
SC-2 Nicht ausgewählte AD-Gruppen werden nicht angelegt Discovery-Response mit alreadyImported-Flag statt Auto-Import; kein OU-weiter Sweep
SC-3 Umbenennung im AD zieht beim nächsten Sync nach Stabile Identität via objectGUID als Match-Key statt ldapDn (Q2), Reihenfolge im Sync-Lauf (Q7)
SC-4 Verschwinden im AD löscht die Tessera-Gruppe inkl. Mitgliedschaften/Freigaben Bestehende onDelete: Cascade-Regeln (Q4), Default-Gruppen-Handoff (Q5)
SC-5 Manuell angelegte Gruppen ohne AD-Bindung bleiben unberührt Bestehendes ldapDn: { not: null }-Filtermuster aus D-21 (syncGroupMembershipsForTenant), unverändert übernommen
D-04 Interner Name überlebt jeden Sync, wird an drei Stellen angezeigt Anzeige-Fundstellen identifiziert (Gruppenliste, Matrix-Header, Benutzer-Chips)
D-06 Kein Mandant ohne Standardgruppe nach Sync-Löschung ensureDefaultGroup-Muster + neue Vor-Lösch-Transaktion (Q5)
D-07 Radio-Auswahl in GroupFormModal entfernt Exakte zu entfernende Codeabschnitte identifiziert
</phase_requirements>

Summary

Phase 16 ist zu über 90% eine Erweiterung bereits bestehenden, gut verstandenen Codes — kein neues Fundament. Die drei tragenden Bausteine existieren schon: LdapService.listGroups() liefert die AD-Gruppen-/OU-Liste (bereits von Phase-15-D-18 genutzt), LdapService.syncGroupMembershipsForTenant() (D-21, Commit 614de28) pflegt Mitgliedschaften für JEDE Gruppe mit gesetztem ldapDn bereits vollautomatisch bei jedem Sync-Lauf, und Group/GroupMembership/ModuleGrant tragen bereits onDelete: Cascade-Regeln, die eine Gruppenlöschung ohne zusätzlichen Aufräumcode sauber durchreichen. Phase 16 muss diese Bausteine um drei neue Fähigkeiten ergänzen: (1) eine Import-UI im LDAP-Bereich, die aus der vorhandenen Gruppenliste gezielt einzelne Group-Zeilen anlegt statt nur ein Suchfilter-Array zu pflegen; (2) eine Identitäts-Strategie, die eine AD-Umbenennung von einem AD-Verschwinden unterscheiden kann — der ldapDn, den Phase 15 als Bindungsschlüssel nutzt, enthält den CN und ändert sich bei jeder Umbenennung, ist also für sich allein untauglich; und (3) eine neue Spalte für den sync-immunen internen Namen (D-04) plus deren Anzeige an drei Frontend-Stellen.

Die Kernentscheidung dieser Phase ist die Identitätsfrage aus Punkt (2). Die installierte ldapts@8.1.8-Bibliothek unterstützt nachweislich binäre LDAP-Attribute über die Option explicitBufferAttributes (im installierten Quellcode verifiziert, siehe Code Examples) — das erlaubt, objectGUID als Buffer abzufragen und hex-kodiert als stabilen, umbenennungsresistenten Schlüssel zu speichern. Diese Fähigkeit der Bibliothek ist verifiziert; das Verhalten eines echten Active Directory (ob objectGUID bei einer CN-Umbenennung tatsächlich unverändert bleibt, wie in der Windows-Dokumentation allgemein beschrieben) konnte in dieser Session nicht gegen einen echten AD-Server geprüft werden und ist entsprechend als Annahme markiert, mit derselben Live-Verifikations-Empfehlung wie beim EWS-Postfach-Pfad aus Phase 14 (14-03).

Primary recommendation: Neue Spalte Group.ldapObjectGuid (nullable, hex-kodiert, @@unique([tenantId, ldapObjectGuid]) analog zu ldapDn) als Sync-Match-Key einführen; ldapDn bleibt bestehen, wird aber bei jedem Sync aus dem aktuellen AD-Zustand überschrieben (Anzeige/Debugging), nicht mehr als Identität benutzt. Ein neuer Rekonziliations-Schritt syncBoundGroupsForTenant() läuft in syncUsersForTenant() vor der bestehenden syncGroupMembershipsForTenant() (D-21), damit Letztere immer mit einem bereits aktualisierten ldapDn arbeitet.

Architectural Responsibility Map

Capability Primary Tier Secondary Tier Rationale
AD-Gruppenauswahl-UI (Checkbox-Liste, Import-Button) Browser / Client Frontend Server (Next.js 'use client'-Seite) Reiner Interaktions-State wie das bestehende admin/ldap/page.tsx-Muster; kein SSR-Datenbedarf
AD-Gruppen-Discovery (LDAP-Suche, objectGUID-Abfrage) API / Backend — LdapService spricht das LDAP-Protokoll; Directory-Zugriff darf nie vom Client aus erfolgen (Zugangsdaten liegen server-seitig)
Stabile Identität / Rename-Erkennung API / Backend Database / Storage Entscheidungslogik in LdapService, Persistenz der ldapObjectGuid-Spalte in Postgres
Cascade-Löschung (Gruppe + Mitgliedschaften + Freigaben) Database / Storage API / Backend Bereits als onDelete: Cascade in der DB verankert (15-01); der Sync-Code ruft nur group.delete() auf, baut keine eigene Lösch-Logik
Default-Gruppen-Handoff (D-06) API / Backend Database / Storage Transaktionale Entscheidungslogik im Service, abgesichert durch den bestehenden partiellen Unique-Index Group_one_default_per_tenant in der DB
Interner-Name-Anzeige (Gruppenliste, Matrix-Header, Chips) Browser / Client API / Backend Backend liefert internalName ?? name bzw. beide Felder; die Auswahl-Logik "welcher Name gewinnt" liegt bewusst im Service, nicht dreifach dupliziert im Frontend
Mandantentrennung während des Sync API / Backend Database / Storage forTenant()-Wrapper setzt app.current_tenant; RLS auf Group/GroupMembership/ModuleGrant ist das zweite Netz

Standard Stack

Core

Keine neuen Abhängigkeiten. Diese Phase nutzt ausschließlich bereits installierte Bibliotheken:

Library Version (installiert) Purpose Why Standard
ldapts 8.1.8 [VERIFIED: apps/api/package.json, node_modules/.pnpm/ldapts@8.1.8/node_modules/ldapts/package.json] LDAP-Client, bereits im gesamten LdapService im Einsatz Bereits etabliert; unterstützt binäre Attribute nativ über explicitBufferAttributes — keine zweite LDAP-Bibliothek nötig
@nestjs/schedule bereits installiert (Cron für LdapSyncScheduler) Bestehender Scheduler-Trigger Unverändert wiederverwendet
Prisma ^6.0.0 [VERIFIED: apps/api/package.json] ORM/Migrationen Unverändert

Hinweis zur Registry-Prüfung: npm view ldapts version liefert aktuell 9.0.0 als neueste Version auf der Registry [VERIFIED: npm-Registry-Abfrage in dieser Session], das Projekt ist aber gepinnt auf ^8.1.8 und tatsächlich installiert ist 8.1.8. Ein Upgrade auf 9.x ist nicht Teil dieser Phase — es besteht kein funktionaler Bedarf, und ein Versionssprung würde eine eigene Verifikation der API-Kompatibilität (insbesondere explicitBufferAttributes, das in 8.1.8 nachweislich existiert) erfordern, die außerhalb des Phasenumfangs liegt.

Alternatives Considered

Instead of Could Use Tradeoff
objectGUID als Rename-stabiler Identitäts-Schlüssel objectSid objectSid kann sich bei Domain-übergreifenden Verschiebungen ändern (SID-History-Mechanik), objectGUID gilt als über die gesamte Objektlebensdauer stabil — für dieses Projekt (ein AD, keine Cross-Domain-Migrationen im Scope) ist objectGUID die robustere Wahl [ASSUMED — allgemeines AD-Verzeichniswissen, nicht in dieser Session gegen Microsoft-Dokumentation verifiziert]
Server-seitige LDAP-Suche als Import-Basis Client-seitiges Caching der Gruppenliste Verworfen — würde eine zweite Suchmechanik neben der bestehenden listGroups() einführen, was D-01 explizit ausschließt

Installation: Keine — keine neuen Pakete.

Package Legitimacy Audit

Nicht anwendbar. Diese Phase installiert keine neuen externen Pakete. Es wird ausschließlich eine bereits vorhandene, in package.json gepinnte Bibliothek (ldapts@8.1.8) um die Nutzung einer zusätzlichen, bereits im installierten Quellcode vorhandenen Option (explicitBufferAttributes) erweitert — kein Registry-Zugriff, kein neuer Lieferant, kein Supply-Chain-Risiko.

Architecture Patterns

System Architecture Diagram

Admin (Browser)
   │
   │ 1. GET /ldap/groups  (Discovery — bestehender Endpoint, erweitert um alreadyImported-Flag)
   ▼
LdapController.listGroups()  ───────────────►  LdapService.listGroups()
   │                                                   │
   │ 2. Admin waehlt AD-Gruppen aus, klickt Import      │  LDAP-Suche gegen AD
   ▼                                                   ▼
POST /ldap/groups/import  { dns: string[] }      Active Directory
   │                                              (liefert dn, cn, objectGUID)
   ▼
LdapController.importGroups()
   │
   ▼
LdapService.importGroupsByDn()  ── legt Group-Zeilen an
   │  (name = AD-cn, ldapDn = aktueller DN, ldapObjectGuid = hex(objectGUID))
   ▼
PostgreSQL  Group-Tabelle  (RLS, forTenant-scoped)


── Turnusmaessiger/manueller Sync (unveraendert getriggert) ──────────────

LdapSyncScheduler (Cron)  oder  POST /ldap/sync (Button)
   │
   ▼
LdapService.syncUsersForTenant(config, tenantId)
   │
   ├─ 1..4  Benutzer-Sync (unveraendert)
   ├─ 5     Benutzer-Deaktivierung (unveraendert)
   ├─ 5a NEU  syncBoundGroupsForTenant(tenantId)
   │         für jede Group mit ldapObjectGuid gesetzt:
   │           AD-Suche nach objectGUID  ─┬─► Treffer: cn geaendert? → Group.name/ldapDn aktualisieren (SC-3)
   │                                      └─► kein Treffer: D-06-Handoff, dann Group.delete() → Cascade (SC-4)
   ├─ 5b     syncGroupMembershipsForTenant(tenantId)  (D-21, unveraendert,
   │         arbeitet jetzt mit dem in 5a aktualisierten ldapDn)
   └─ 6     lastSyncAt aktualisieren

Keine neuen Verzeichnisse. Erweiterungen bestehender Dateien:

apps/api/src/ldap/
├── ldap.service.ts          # + importGroupsByDn(), syncBoundGroupsForTenant(), + objectGUID-Handling in listGroups()
├── ldap.controller.ts       # + POST /ldap/groups/import
├── ldap.service.spec.ts     # + neue describe-Bloecke fuer Rename/Delete/Import
└── dto/ldap-config.dto.ts   # + ImportGroupsDto (dns: string[])

apps/api/prisma/
├── schema.prisma            # Group: + internalName, + ldapObjectGuid
└── migrations/<neu>_add_group_internal_name_and_object_guid/

apps/api/src/groups/
├── groups.service.ts        # + reassignDefaultBeforeDelete()-Helper (fuer Sync-Aufruf), Anzeige-Selects auf internalName ?? name
├── groups.controller.ts     # unveraendert (kein neuer Endpoint hier, D-01)
└── module-grants.service.ts # Zeilen 240/249/264: group.name → group internalName-aware select

apps/web/src/app/(portal)/admin/
├── ldap/page.tsx             # + neue Sektion "AD-Gruppen importieren" (Muster: Sektion 2.55 Benutzer-Import)
├── groups/components/GroupFormModal.tsx   # D-07: ldapBind-Radio-Block komplett entfernen
├── groups/page.tsx           # Namensanzeige auf internalName ?? name umstellen
├── modules/grants/page.tsx   # Zeile ~209/212: Spaltenkopf auf internalName ?? name
└── users/components/UserAccessModal.tsx  # viaGroups-Chips: Backend liefert bereits den richtigen Namen (kein Frontend-Change noetig, wenn Backend-Select korrekt ist)

Pattern 1: Additive Result-Felder auf einem gemeinsamen Sync-Report

What: LdapSyncResult wächst additiv (wie schon bei D-21) statt einen zweiten Report-Typ einzuführen. When to use: Jede neue Sync-Teilaufgabe, die im selben Lauf wie syncUsersForTenant() passiert. Example:

// Source: apps/api/src/ldap/ldap.service.ts:9-20 (bestehendes Muster, VERIFIED)
export interface LdapSyncResult {
  created: number;
  updated: number;
  deactivated: number;
  groupMembershipsAdded: number;
  groupMembershipsRemoved: number;
  // Phase 16 (additiv, gleiches Muster):
  // groupsImported: number;
  // groupsRenamed: number;
  // groupsDeleted: number;
  // defaultMarkerMoved: number;
  errors: string[];
}

Pattern 2: Binäres LDAP-Attribut abfragen (objectGUID)

What: ldapts liefert binäre Attribute nur als Buffer, wenn explicitBufferAttributes explizit gesetzt wird — sonst versucht die Bibliothek eine UTF-8-Dekodierung, die bei objectGUID (raw 16-Byte-GUID) zu Datenverlust führt. When to use: Immer, wenn objectGUID oder objectSid aus AD gelesen werden soll. Example:

// Source: node_modules/.pnpm/ldapts@8.1.8/node_modules/ldapts/src/Client.ts:139,568,628
// und src/messages/SearchEntry.ts:44-66 (VERIFIED — installierter Quellcode dieser Session gelesen)
const { searchEntries } = await client.search(baseDn, {
  filter: '(objectClass=group)',
  attributes: ['cn', 'dn', 'objectGUID'],
  explicitBufferAttributes: ['objectGUID'], // erzwingt Buffer statt (verlustbehafteter) UTF-8-Dekodierung
  scope: 'sub',
});
// entry['objectGUID'] ist jetzt ein Buffer (oder Buffer[] bei Mehrfachwerten, hier nie der Fall)
const guidHex = (entry['objectGUID'] as Buffer).toString('hex'); // stabiler, speicherbarer Identitaets-String

Pattern 3: Group-Existenz-Sweep mit binärem Filter (NEU, ungetestet gegen echtes AD)

What: Eine gespeicherte objectGUID gegen den aktuellen AD-Stand prüfen — Filterwert muss byteweise \XX-hex-escaped werden (RFC 4515), nicht wie ein String über escapeLdapFilterValue(). When to use: In syncBoundGroupsForTenant(), um pro gebundener Gruppe herauszufinden, ob sie noch existiert und wie sie aktuell heißt. Example:

// [ASSUMED — Filtersyntax nach RFC 4515-Praxis, nicht in dieser Session gegen ein
// echtes AD verifiziert; vor Produktivnahme mit einem echten Verzeichnis pruefen]
function escapeLdapFilterBuffer(buf: Buffer): string {
  return Array.from(buf)
    .map((b) => '\\' + b.toString(16).padStart(2, '0'))
    .join('');
}
const filter = `(objectGUID=${escapeLdapFilterBuffer(storedGuidBuffer)})`;
const { searchEntries } = await client.search(baseDn, {
  filter,
  attributes: ['cn', 'dn'],
  scope: 'sub',
});
// searchEntries.length === 0  →  Gruppe im AD verschwunden (D-05)
// searchEntries[0].dn !== group.ldapDn  →  umbenannt (D-03), Group.name/ldapDn aktualisieren

Anti-Patterns to Avoid

  • ldapDn als alleinigen Identitäts-/Match-Schlüssel für den Gruppen-Sync verwenden: Der DN enthält den CN — jede Umbenennung ändert ihn. Ein Sync, der ausschließlich per ldapDn matcht, kann eine Umbenennung strukturell nicht von einem Verschwinden unterscheiden (SC-3 vs. SC-4 würden identisch behandelt: beide als "gelöscht+neu angelegt", was Mitgliedschaften und Modulfreigaben unnötig zerstört).
  • Gruppen-Rekonziliation nach der Mitgliedschafts-Rekonziliation laufen lassen: Wenn syncGroupMembershipsForTenant() (D-21) vor einer Namens-/DN-Aktualisierung läuft, sucht sie mit dem VERALTETEN ldapDn — nach einer echten Umbenennung im AD liefert der memberOf=<alte-DN>-Filter keine Treffer mehr, und alle LDAP-Mitgliedschaften der umbenannten Gruppe würden fälschlich als "entfernt" behandelt.
  • prisma.group.delete() außerhalb eines forTenant()-Scopes aufrufen: Group/GroupMembership/ModuleGrant tragen FORCE ROW LEVEL SECURITY (Migration 20260804130918_groups_rls_policies); ein ungescopter Aufruf ohne gesetzten app.current_tenant-Session-Wert liefert je nach DB-Rolle entweder null betroffene Zeilen oder — schlimmer — arbeitet mit der falschen Session-Variable aus einem vorherigen, nicht bereinigten Kontext.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Mandantentrennung bei DB-Schreibzugriffen im Sync Eigene WHERE tenantId = ...-Disziplin von Hand forTenant(this.prisma, tenantId) (bereits in syncGroupMembershipsForTenant verwendet) Setzt die RLS-Session-Variable korrekt und konsistent; von Hand vergessen ist der dokumentierte Klassiker (Pitfall 2 aus der Phase-2-Research)
Cascade-Löschung von Mitgliedschaften/Freigaben beim Gruppen-Löschen Anwendungsseitige deleteMany-Aufrufe vor dem group.delete() Bestehende DB-onDelete: Cascade-Regeln (15-01) Bereits vorhanden und getestet (GroupsService.remove() verlässt sich exakt darauf, keine Doppel-Wahrheit über Aufräumverhalten schaffen)
LDAP-Filter-Escaping für String-Werte Eigene Regex LdapService.escapeLdapFilterValue() (statisch, bereits vorhanden) Bereits gegen T-02-16/T-15-07 (LDAP-Injection) geprüft und in Produktion verwendet
Default-Gruppen-Bootstrap für einen Mandanten ganz ohne Gruppen Eigene Re-Implementierung der "Alle Benutzer"-Anlage GroupsService.ensureDefaultGroup(tenantId) (Quick-Task 260805-fok) Bereits idempotent, race-sicher gegen den partiellen Unique-Index abgesichert — als Fallback nach einer Sync-Löschung des letzten verbleibenden Mandanten-Gruppe direkt wiederverwendbar

Key insight: Praktisch der gesamte "harte" Teil dieser Phase (Mandantentrennung, Cascade-Semantik, Default-Gruppen-Bootstrap, LDAP-Escaping) ist in Phase 15 bereits gebaut und getestet worden. Der einzige wirklich neue Baustein ist die Rename-vs-Delete-Unterscheidung über objectGUID.

Common Pitfalls

Pitfall 1: DN-basierte Reihenfolge-Falle im selben Sync-Lauf

What goes wrong: Eine AD-Umbenennung wird im selben Lauf, in dem sie erkannt wird, als Mitgliederverlust protokolliert. Why it happens: syncGroupMembershipsForTenant() liest group.ldapDn aus der DB und baut daraus den memberOf=<dn>-Suchfilter — wenn dieser Wert noch der alte (vor der Umbenennung gültige) DN ist, liefert AD keine Treffer mehr für die neue CN. How to avoid: syncBoundGroupsForTenant() (neu) muss die Group.ldapDn/Group.name-Aktualisierung innerhalb desselben Laufs, vor dem bestehenden D-21-Schritt abschließen (siehe Pattern 1/Diagramm oben). Warning signs: Sync-Bericht zeigt nach einer bekannten AD-Umbenennung groupMembershipsRemoved > 0 für exakt die umbenannte Gruppe, obwohl im AD niemand die Mitgliedschaft geändert hat.

Pitfall 2: objectGUID per UTF-8 statt Buffer gelesen

What goes wrong: Ohne explicitBufferAttributes: ['objectGUID'] versucht ldapts, das binäre 16-Byte-objectGUID als UTF-8-String zu interpretieren [VERIFIED: Attribute.ts:63-73 — die Bibliothek fällt zwar bei nicht-dekodierbaren Bytes auf Buffer zurück, aber ein GUID, dessen Bytes zufällig gültige UTF-8-Sequenzen ergeben, würde als kaputter String weiterverarbeitet]. Why it happens: explicitBufferAttributes ist ein Such-Optionsfeld, das explizit pro Aufruf gesetzt werden muss — es gibt keinen globalen Default für "dieses Attribut ist immer binär". How to avoid: JEDE Suche, die objectGUID anfordert (Discovery-Liste UND Existenz-Sweep), muss explicitBufferAttributes: ['objectGUID'] mitgeben. Warning signs: Gespeicherte ldapObjectGuid-Werte sind nicht konsistent 32 Hex-Zeichen lang, oder derselbe AD-Gruppe erzeugt bei zwei Syncs unterschiedliche Hex-Strings.

Pitfall 3: Sync-Bericht im Frontend verschluckt neue Felder (bestehende Lücke, wird durch Phase 16 verschärft)

What goes wrong: Das Frontend-SyncResult-Interface in apps/web/.../admin/ldap/page.tsx:54-59 kennt bereits heute NICHT die von D-21 hinzugefügten Felder groupMembershipsAdded/groupMembershipsRemoved — sie werden vom Backend geliefert, aber von der UI stillschweigend ignoriert [VERIFIED: page.tsx:54-59 deklariert nur created/updated/deactivated/errors; t('sync.result', {...}) bei Zeile 1072-1076 übergibt ebenfalls nur diese drei Werte]. Why it happens: Additive Backend-Felder brauchen einen expliziten Frontend-Schritt, der bei D-21 offenbar ausgelassen wurde. How to avoid: Phase 16 sollte diese Lücke für BEIDE Feldgruppen (D-21 UND die neuen Phase-16-Felder) in einem Aufwasch schließen — Interface erweitern, t('sync.result', {...})-Aufruf und die zugehörigen i18n-Strings (de.json/en.json) ergänzen. Warning signs: Admin klickt "Jetzt synchronisieren", sieht nur "3 erstellt, 1 aktualisiert, 0 deaktiviert" und hat keine Ahnung, ob/wie viele Gruppen importiert, umbenannt oder gelöscht wurden.

Pitfall 4: Namenskollision bei einer unattended Sync-Operation wirft statt zu protokollieren

What goes wrong: GroupsService.create()/update() werfen bei einer @@unique([tenantId, name])-Verletzung eine ConflictException — das ist für einen interaktiven Admin-Request richtig, würde aber, unverändert im Sync-Kontext wiederverwendet, den kompletten syncUsersForTenant()-Lauf für diesen Mandanten abbrechen, wenn er nicht explizit abgefangen wird. Why it happens: GroupsService-Methoden sind für HTTP-Request/Response gebaut, nicht für einen Batch-Lauf mit Sammel-Fehlerbericht. How to avoid: Der neue Gruppen-Sync-Code darf GroupsService.create()/.update() NICHT direkt aufrufen (oder muss deren Exceptions in einem eigenen try/catch pro Gruppe abfangen und in result.errors sammeln) — exakt das Muster, das syncGroupMembershipsForTenant() schon für Pro-Gruppe-Fehler nutzt (Gruppe <name>: <message>). Warning signs: Ein einzelner Namenskonflikt lässt den gesamten Sync für einen Mandanten fehlschlagen, andere unabhängige Gruppen werden nicht mehr verarbeitet.

Pitfall 5: Zero-Default-Group-Fenster nach Sync-Löschung der letzten Gruppe

What goes wrong: Wird die einzige verbliebene (und zufällig als Standard markierte) Gruppe eines Mandanten vom Sync gelöscht, bleibt der Mandant bis zum nächsten API-Neustart ohne jede Gruppe — ensureDefaultGroup() wird aktuell nur bei TenantService.create() (neuer Mandant) und AdminSeedService.ensureDefaultGroupsForAllTenants() (nur beim Container-Start, [VERIFIED: .planning-Historie, Quick-Task 260805-fok]) aufgerufen, NICHT nach einer Sync-Löschung. Why it happens: D-06 beschreibt den Regelfall ("bevorzugt auf 'Alle Benutzer', sonst eine andere vorhandene Gruppe") — deckt aber den Fall "keine andere Gruppe existiert mehr" nicht explizit ab. How to avoid: syncBoundGroupsForTenant() muss nach jeder Gruppen-Löschung prüfen (oder unbedingt am Ende des Sync-Laufs für den betroffenen Mandanten ensureDefaultGroup(tenantId) aufrufen), damit das Zero-Default-Fenster nicht bis zum nächsten Deploy offen bleibt. Warning signs: Ein Mandant mit genau einer AD-gebundenen Gruppe verliert nach deren Löschung im AD jede Standard-Zuweisung für neu angelegte Benutzer, bis die API neu gestartet wird.

Code Examples

Bestehendes Muster: Discovery + Import (Benutzer), als Vorbild für Gruppen (D-01)

// Source: apps/api/src/ldap/ldap.service.ts:356-547 (VERIFIED, gekürzt)
// searchUsers() liefert LdapUserSearchResult[] mit alreadyImported-Flag,
// berechnet ueber EINE zusammengefasste Query gegen ldapDn ODER username:
const existing = await this.prisma.user.findMany({
  where: {
    tenantId,
    OR: [{ ldapDn: { in: dns } }, { username: { in: usernames } }],
  },
  select: { ldapDn: true, username: true },
});
// → Gruppen-Aequivalent: Group.findMany({ where: { tenantId, ldapObjectGuid: { in: guids } } })
//   um alreadyImported pro Discovery-Treffer zu markieren.

// importUsersByDn() ist idempotent: ein bereits vorhandener User wird
// UEBERSPRUNGEN, nicht dupliziert oder ueberschrieben — nur ldapDn wird
// nachgezogen, falls es fehlt. Fuer Gruppen gilt dieselbe Idempotenz-Anforderung.

Bestehendes Muster: Pro-Gruppe try/catch mit gesammelten Fehlern (D-21)

// Source: apps/api/src/ldap/ldap.service.ts:860-934 (VERIFIED, gekürzt)
for (const group of boundGroups) {
  try {
    // ... AD-Suche, Reconciliation ...
  } catch (groupError: unknown) {
    const msg = groupError instanceof Error ? groupError.message : 'Unknown error syncing group';
    result.errors.push(`Gruppe ${group.name}: ${msg}`);
  }
}
// Ein fehlschlagender Gruppen-Sync stoppt niemals die uebrigen Gruppen (Pitfall 4 oben)

Bestehendes Muster: Default-Marker-Transaktion (D-13, Vorbild für D-06-Handoff)

// Source: apps/api/src/groups/groups.service.ts:99-154 (VERIFIED, gekürzt)
// GroupsService.update() setzt isDefault:true in einer Transaktion:
// zuerst updateMany auf false fuer alle anderen Gruppen des Mandanten,
// dann update der Zielgruppe auf true — abgesichert durch den partiellen
// Unique-Index Group_one_default_per_tenant.
if (data.isDefault === true) {
  const [, updated] = await this.prisma.$transaction([
    this.prisma.group.updateMany({ where: { tenantId, isDefault: true }, data: { isDefault: false } }),
    this.prisma.group.update({ where: { id }, data: { ...updateData, isDefault: true } }),
  ]);
  return updated;
}
// Phase-16-Bedarf: NEUE Methode, die statt "setze Zielgruppe X" die Zielgruppe
// selbst BESTIMMT (bevorzugt "Alle Benutzer", sonst irgendeine andere), BEVOR
// die alte Standardgruppe geloescht wird — gleiche Transaktions-Grundform.

State of the Art

Old Approach (Phase 15) Current Approach (Phase 16) When Changed Impact
ldapDn als einziger AD-Bindungs-/Identitäts-Schlüssel ldapObjectGuid als Identität, ldapDn als aus dem AD nachgezogenes Anzeige-/Debug-Feld Diese Phase Ermöglicht die Rename-vs-Delete-Unterscheidung, die SC-3/SC-4 verlangen
AD-Bindung wählbar sowohl beim Gruppen-Anlegen (GroupFormModal) als auch implizit beim Import AD-Bindung ausschließlich über den Import-Flow im LDAP-Bereich, GroupFormModal verliert die Radio-Auswahl komplett (D-07) Diese Phase Ein Weg statt zwei — verhindert die von D-07 benannte Fehlerquelle

Deprecated/outdated:

  • Die Radio-Auswahl-UI in GroupFormModal.tsx (Zeilen 147-227) für die AD-Bindung wird mit dieser Phase vollständig entfernt (D-07). Zugehörige i18n-Keys admin.groups.ldapBind.* (de.json:391-398) werden dadurch verwaist und sollten geprüft/entfernt werden — en.json wurde in dieser Session nicht gelesen und muss beim Pruning ebenfalls geprüft werden (siehe Assumptions Log).

Assumptions Log

# Claim Section Risk if Wrong
A1 objectGUID bleibt bei einer reinen CN-/Namens-Umbenennung eines AD-Gruppenobjekts unverändert (nur objectSid kann sich bei Cross-Domain-Verschiebung ändern) Summary, Standard Stack, Pattern 2/3 Falls falsch: die gesamte Rename-Erkennung (SC-3) funktioniert nicht wie geplant — jede Umbenennung würde weiterhin wie ein Verschwinden+Neuanlage behandelt. Muss vor Implementierung gegen einen echten AD-Test-Server verifiziert werden (Empfehlung: gleicher Human-Verify-Checkpoint wie beim EWS-Pfad in Phase 14/14-03)
A2 Die von escapeLdapFilterBuffer() (Pattern 3) vorgeschlagene byteweise \XX-Hex-Escaping-Syntax für einen binären (objectGUID=...)-Filterwert wird vom Ziel-AD korrekt interpretiert Pattern 3, Code Examples Falls falsch: der Existenz-Sweep pro gebundener Gruppe liefert null Treffer für JEDE Gruppe, auch für weiterhin existierende — würde SC-4 fälschlich auf alle gebundenen Gruppen anwenden. Muss gegen ein echtes AD getestet werden, bevor die Sync-Löschung produktiv scharf geschaltet wird
A3 en.json enthält dieselben admin.groups.ldapBind.*-Keys wie de.json (391-398) und muss beim D-07-Pruning parallel bereinigt werden State of the Art Gering — nur i18n-Hygiene, keine funktionale Auswirkung, aber verwaiste Keys bleiben sonst in einer Sprache liegen
A4 Empfohlenes Namenskollisions-Verhalten (Reject-with-Report statt Suffix-Anhängen) ist die richtige Wahl für diesen Sync-Kontext Q6 / Common Pitfalls Pitfall 4 Gering-Mittel — ausdrücklich als Claude's Discretion markiert; falls der Admin eine automatische Suffix-Vergabe bevorzugt, ist das ein reiner Implementierungs-Swap ohne Schema-Auswirkung

Wenn diese Tabelle leer wäre: Ist sie nicht — A1/A2 sind die zentralen technischen Risiken dieser Phase und sollten in der Planung als eigener, früh laufender Verifikations-Task (idealerweise gegen den ViCoTest-Server, 192.168.13.12, read-only) abgebildet werden, bevor die Lösch-Semantik (D-05) produktiv aktiv wird.

Open Questions

  1. Verhalten, wenn ein Admin eine automatisch importierte Gruppe in Tessera umbenennt (AD-Name, nicht internalName)

    • What we know: D-03 sperrt das Namensfeld für importierte Gruppen im UI ("In Tessera ist das Feld für importierte Gruppen gesperrt"); die Roadmap nennt genau diese Frage explizit als "offen für die Planung".
    • What's unclear: Ob es serverseitig überhaupt einen Endpoint gibt, der name für eine ldapDn-gebundene Gruppe akzeptiert (aktuell erlaubt GroupsService.update() uneingeschränkt data.name für jede Gruppe, unabhängig von ldapDn) — die Sperre existiert laut Kontext bisher nur als UI-Konvention, nicht als Backend-Invariante.
    • Recommendation: Backend-seitig UpdateGroupDto.name für Gruppen mit gesetztem ldapObjectGuid/ldapDn ablehnen (400), nicht nur im Frontend deaktivieren — sonst ist die "gesperrt"-Aussage aus D-03 nicht durchgesetzt, sondern nur kosmetisch.
  2. Verhältnis der neuen Import-Auswahloberfläche zur bestehenden groupFilterDns-Auswahl in /admin/ldap

    • What we know: groupFilterDns (Section 2.5 in admin/ldap/page.tsx) steuert, welche OUs/Gruppen beim BENUTZER-Sync als Scope/Filter dienen — ein völlig anderer Zweck als "diese AD-Gruppe soll eine Tessera-Gruppe werden".
    • What's unclear: Ob beide Auswahllisten dieselbe Discovery-Response (GET /ldap/groups) teilen sollen (technisch ja möglich, da listGroups() bereits beide OU- und Gruppen-Einträge liefert) oder als zwei unabhängige UI-Abschnitte mit eigenem Such-State nebeneinanderstehen sollen.
    • Recommendation: Zwei getrennte UI-Sektionen (wie bereits heute Section 2.5 "Import-Filter" und die neue Section "Gruppen importieren" nebeneinander existieren würden), aber EIN gemeinsamer GET /ldap/groups-Aufruf mit gemeinsamem discoverSearch-State ist eine spätere Optimierung, kein Blocker für diese Phase.

Environment Availability

Dependency Required By Available Version Fallback
Live Active-Directory-Server (für A1/A2-Verifikation) Rename-Erkennung (SC-3), Existenz-Sweep (SC-4) ✗ in dieser Sandbox — ViCoTest-Testserver (192.168.13.12) laut Projekt-Memory vorhanden — read-only Prüfung dort empfohlen, kein Docker-Deploy durch Claude (bestehende Projektregel)
Lokale PostgreSQL für Migrationstest Neue Spalten internalName/ldapObjectGuid ✓ (Docker-Container tessera-ctl-db-1, kein Host-Port) postgres:16-alpine [VERIFIED: docker-compose.yml:55] Migration via DATABASE_URL gegen die Container-IP, siehe unten
ldapts mit explicitBufferAttributes-Unterstützung objectGUID-Abfrage ✓ 8.1.8 [VERIFIED: node_modules-Quellcode] —

Migrationsbefehl (lokal, verifiziertes Muster aus einer früheren Phase-15-Vorstufe):

# [CITED: .planning/quick/260728-lih-.../260728-lih-PLAN.md:100-111, in dieser Session gelesen]
docker start tessera-ctl-db-1
DB_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tessera-ctl-db-1)
cd apps/api && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" \
  pnpm exec prisma migrate dev --name add_group_internal_name_and_object_guid

In Produktion läuft prisma migrate deploy automatisch beim Container-Start [VERIFIED: apps/api/Dockerfile:38].

Validation Architecture

Test Framework

Property Value
Framework Vitest 3.x [VERIFIED: apps/api/package.json "test": "vitest run"]
Config file apps/api/vitest.config.ts
Quick run command cd apps/api && npx vitest run src/ldap/ldap.service.spec.ts src/groups/groups.service.spec.ts
Full suite command cd apps/api && pnpm test (bzw. turbo test vom Repo-Root)

Phase Requirements → Test Map

Kriterium Behavior Test Type Automated Command File Exists?
SC-1 Ausgewählte AD-Gruppe erzeugt eine Group-Zeile mit ldapDn+ldapObjectGuid gesetzt unit vitest run src/ldap/ldap.service.spec.ts -t "importGroupsByDn" ❌ Wave 0 — neuer describe-Block
SC-2 Nicht ausgewählte AD-Gruppen erzeugen KEINE Group-Zeile unit vitest run src/ldap/ldap.service.spec.ts -t "importGroupsByDn" (negativer Fall im selben Block) ❌ Wave 0
SC-3 Umbenennung im AD aktualisiert Group.name/ldapDn, Mitgliedschaften bleiben erhalten unit vitest run src/ldap/ldap.service.spec.ts -t "syncBoundGroupsForTenant — rename" ❌ Wave 0
SC-4 Verschwinden im AD löscht Group inkl. GroupMembership/ModuleGrant (Cascade) unit + integration vitest run src/ldap/ldap.service.spec.ts -t "syncBoundGroupsForTenant — delete" + ein Cascade-Test gegen die echte Migration (migration-sql.spec.ts-Stil, reiner Textabgleich der bereits vorhandenen ON DELETE CASCADE-Klauseln — kein neuer DB-Test nötig, da bereits durch 15-01 abgedeckt) ❌ Wave 0 (neuer describe-Block), ✅ Cascade-Grundlage bereits durch bestehende Migration-SQL abgedeckt
SC-5 Manuelle Gruppen (ldapDn: null) bleiben vom Gruppen-Sync unberührt unit vitest run src/ldap/ldap.service.spec.ts -t "ignores a Tessera group with no ldapDn" (bestehendes Testmuster aus D-21, Zeile ~665, für den neuen syncBoundGroupsForTenant analog zu duplizieren) ✅ Muster existiert bereits für D-21, ❌ analoger Test für den neuen Schritt fehlt
D-04 Interner Name überlebt Sync, wird an 3 Stellen angezeigt unit + component vitest run src/groups/groups.service.spec.ts (Backend-Select) + vitest run apps/web/.../groups-page.test.tsx (Anzeige) ❌ Wave 0 für beide
D-06 Kein Mandant ohne Standardgruppe nach Sync-Löschung unit vitest run src/ldap/ldap.service.spec.ts -t "default marker handoff" ❌ Wave 0
D-07 GroupFormModal enthält keine AD-Radio-Auswahl mehr component Bestehende groups-page.test.tsx/Modal-Tests anpassen — Assertion, dass kein ldapBind-Discovery-Aufruf mehr passiert ❌ Wave 0 (bestehende Tests müssen angepasst werden, nicht nur ergänzt)

Sampling Rate

  • Per task commit: cd apps/api && npx vitest run src/ldap/ldap.service.spec.ts src/groups/groups.service.spec.ts
  • Per wave merge: pnpm test (API + Web)
  • Phase gate: Volle Suite grün, PLUS ein manueller Human-Verify-Checkpoint gegen ein echtes AD (ViCoTest) für A1/A2 (objectGUID-Stabilität, Binär-Filter-Syntax), bevor /gsd-verify-work die Löschsemantik (D-05) als produktionsreif markiert.

Wave 0 Gaps

  • apps/api/src/ldap/ldap.service.spec.ts — neue describe-Blöcke für importGroupsByDn, syncBoundGroupsForTenant (Rename/Delete/Default-Handoff/Namenskollision)
  • apps/api/src/groups/groups.service.spec.ts — Anpassung der listForTenant/getImpact-Selects auf internalName ?? name, falls diese Logik im Service statt nur im Controller liegt
  • apps/web/src/app/(portal)/admin/groups/groups-page.test.tsx — Assertion, dass GroupFormModal nach D-07 keine GET /ldap/groups-Discovery mehr für eine neue Gruppe auslöst
  • apps/api/src/groups/migration-sql.spec.ts-artiger Text-Test für die neue Migration (internalName/ldapObjectGuid-Spalten, Unique-Index-Klausel), analog zum bestehenden Muster
  • Kein neues Test-Framework nötig — Vitest ist bereits vollständig eingerichtet.

Security Domain

Applicable ASVS Categories

ASVS Category Applies Standard Control
V1 Architecture yes Mandantentrennung via forTenant() + RLS (bereits etabliert, in dieser Phase konsequent auf den neuen Sync-Code anzuwenden)
V4 Access Control yes RolesGuard + @Roles(ADMIN, SUPER_ADMIN) auf jedem neuen Endpoint (POST /ldap/groups/import), identisch zu allen bestehenden LDAP-/Gruppen-Routen
V5 Input Validation yes LDAP-Filter-Injection: jeder neue Filterbaustein (insbesondere der binäre objectGUID-Filter aus Pattern 3) muss konsequent escaped werden — kein Freitext-Interpolieren von AD-gelieferten Werten in einen Suchfilter
V6 Cryptography no Diese Phase führt keine neuen Geheimnisse/Verschlüsselung ein — bindPassword bleibt unverändert im bestehenden Klartext-Muster (dokumentierte, bekannte Design-Entscheidung aus Phase 2, nicht Gegenstand dieser Phase)

Known Threat Patterns for LDAP-Sync + Multi-Tenant-RLS

Pattern STRIDE Standard Mitigation
LDAP-Filter-Injection über einen AD-gelieferten oder admin-gewählten DN/GUID-Wert Tampering escapeLdapFilterValue() (String) bzw. der neue escapeLdapFilterBuffer() (Binär) vor jeder Filter-Interpolation — bestehendes, bereits verifiziertes Muster konsequent auf neue Codepfade anwenden
Cross-Tenant-Zugriff auf eine groupId aus einem fremden Mandanten über den Import-Endpoint Elevation of Privilege findOwned(tenantId, id)-Ownership-Check-Muster (bereits in GroupsService etabliert) auf jeden neuen Codepfad anwenden, der eine groupId entgegennimmt
Race Condition bei parallelem Default-Gruppen-Handoff (Sync + gleichzeitige manuelle Admin-Aktion) Tampering Partieller Unique-Index Group_one_default_per_tenant als DB-seitiges zweites Netz (bereits vorhanden) — die Sync-Transaktion fängt einen daraus resultierenden P2002 ab und behandelt ihn wie ein "hat sich schon jemand anderes gekümmert", statt zu werfen (Muster aus ensureDefaultGroup() übernehmen)
Unattended Sync löscht Daten (Mitgliedschaften, Modulfreigaben) ohne den D-17-Warndialog Repudiation/Information Disclosure (aus Nutzersicht: unerwarteter Zugriffsverlust) Bewusst akzeptiertes Verhalten laut D-05 — Mitigation ist ausschließlich die Protokollierung im Sync-Bericht (Pattern 1), kein technischer Schutz vorgesehen

Sources

Primary (HIGH confidence — in dieser Session gelesener Quellcode/Konfiguration)

  • apps/api/src/ldap/ldap.service.ts (VERIFIED, vollständig gelesen)
  • apps/api/src/ldap/ldap.controller.ts, apps/api/src/ldap/dto/ldap-config.dto.ts, apps/api/src/ldap/ldap-sync.scheduler.ts, apps/api/src/ldap/ldap.module.ts (VERIFIED)
  • apps/api/src/groups/groups.service.ts, groups.controller.ts, groups.module.ts, module-grants.service.ts (Auszüge), groups.service.spec.ts (VERIFIED)
  • apps/api/prisma/schema.prisma (VERIFIED, vollständig gelesen)
  • apps/api/prisma/migrations/20260804130130_add_groups_and_module_grants/migration.sql, 20260804130918_groups_rls_policies/migration.sql (VERIFIED)
  • apps/api/src/prisma/prisma-tenant.extension.ts (VERIFIED)
  • apps/web/src/app/(portal)/admin/ldap/page.tsx, apps/web/src/app/(portal)/admin/groups/components/GroupFormModal.tsx (VERIFIED)
  • apps/web/src/app/(portal)/admin/modules/grants/page.tsx, apps/web/src/app/(portal)/admin/users/components/UserAccessModal.tsx (Auszüge via grep + gezielte Zeilenprüfung, VERIFIED)
  • apps/web/src/messages/de.json Zeilen 372-417 (VERIFIED)
  • node_modules/.pnpm/ldapts@8.1.8/node_modules/ldapts/src/Client.ts, src/messages/SearchEntry.ts, src/Attribute.ts (VERIFIED — installierter Bibliotheks-Quellcode)
  • apps/api/Dockerfile:38 (VERIFIED)
  • docker-compose.yml:54-69 (VERIFIED)
  • .planning/quick/260728-lih-ldap-sync-selektiv-und-auto-sync-default/260728-lih-PLAN.md:100-111 (CITED — gelesen, dokumentiertes Migrationsverfahren)
  • .planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-CONTEXT.md, .planning/ROADMAP.md Phase 15/16, .planning/REQUIREMENTS.md, .planning/STATE.md (VERIFIED)
  • npm view ldapts version (VERIFIED — Registry-Abfrage in dieser Session)

Secondary (MEDIUM confidence)

  • Keine — alle Websearch-Provider sind in .planning/config.json deaktiviert (brave_search, exa_search, tavily_search, firecrawl, ref_search, perplexity, jina alle false); diese Recherche stützt sich vollständig auf Codebasis-Lektüre statt externer Dokumentation.

Tertiary (LOW confidence — als [ASSUMED] markiert)

  • Allgemeines Active-Directory-Verzeichniswissen zu objectGUID-Stabilität über Umbenennungen hinweg (Assumptions Log A1)
  • RFC-4515-Praxis für binäre LDAP-Filterwerte, nicht gegen ein echtes AD verifiziert (Assumptions Log A2)

Metadata

Confidence breakdown:

  • Standard Stack: HIGH — keine neuen Pakete, bestehende ldapts-Fähigkeit direkt im installierten Quellcode verifiziert
  • Architecture: HIGH — jeder betroffene Codepfad wurde gelesen, nicht aus Trainingswissen rekonstruiert
  • Pitfalls: HIGH für die codebasierten Pitfalls (1, 3, 4, 5 — alle aus gelesenem Code abgeleitet), MEDIUM-LOW für Pitfall 2/die AD-Laufzeit-Annahmen (A1/A2)

Research date: 2026-08-05 Valid until: 30 Tage (stabiler interner Code-Stack) — die AD-spezifischen Annahmen (A1/A2) sollten jedoch VOR Implementierungsbeginn, nicht erst nach 30 Tagen, gegen ein echtes Verzeichnis verifiziert werden.