37 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | estimate | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 16-ad-gruppen-synchronisation | 01 | execute | 1 |
|
false |
|
|
|
Zusaetzlich legt der Plan die beiden neuen Spalten aus D-04 an: internalName (vom Sync nie beruehrt) und ldapObjectGuid (rename-stabiler Identitaetsschluessel). Beide sind eine Migration und eine one-way-Entscheidung — deshalb sitzt vor der Tracer-Task ein checkpoint:decision.
Purpose: Die Architektur der Phase wird an einer einzigen echten Faehigkeit bewiesen, bevor Rekonziliation (16-03), Anzeige-Fallback (16-02/16-05) und Dialog-Umbau (16-04) darauf aufbauen. Insbesondere wird hier entschieden und verifiziert, dass objectGUID ueberhaupt als Buffer lesbar und speicherbar ist — die Voraussetzung fuer SC-3 und SC-4.
Output: Neue Spalten + Migration, listGroups() mit alreadyImported, importGroupsByDn(), POST /ldap/groups/import, neue Import-Sektion in /admin/ldap, i18n-Keys, Unit-Tests.
<artifacts_this_phase_produces> Neu entstehende Symbole in diesem Plan (fuer die Source-Grounding-Pruefung: diese Namen existieren vor diesem Plan NICHT im Repo):
| Art | Symbol |
|---|---|
| Prisma-Spalte | Group.internalName |
| Prisma-Spalte | Group.ldapObjectGuid |
| Prisma-Index | Group_tenantId_ldapObjectGuid_key |
| Migrationsverzeichnis | <timestamp>_add_group_internal_name_and_object_guid |
| TS-Interface | LdapGroupImportResult ({ imported, skipped, nameCollisions, errors }) |
| Service-Methode | LdapService.importGroupsByDn(config, tenantId, dns) |
| Service-Helper | LdapService.escapeLdapFilterBuffer(buf) (static) |
| DTO | ImportGroupsDto |
| Route | POST /ldap/groups/import |
| Feld auf bestehendem Interface | LdapDirectoryEntry.alreadyImported |
| i18n-Namespace | admin.ldap.groupImport.* (title, description, discover, searchPlaceholder, noneFound, noMatches, alreadyImported, importSelected, resultSummary, membershipHint, nameCollisionError, discoverError, importError) |
| Frontend-State | groupImportCandidates, groupImportSearch, discoveringGroupImport, selectedImportDns, importingGroups, groupImportResult, groupImportError |
| Frontend-Handler | handleDiscoverGroupsToImport, handleImportGroups, toggleImportDn |
| </artifacts_this_phase_produces> |
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/16-ad-gruppen-synchronisation/16-CONTEXT.md @.planning/phases/16-ad-gruppen-synchronisation/16-RESEARCH.md @.planning/phases/16-ad-gruppen-synchronisation/16-PATTERNS.md @.planning/phases/16-ad-gruppen-synchronisation/16-UI-SPEC.md<decisions_recorded> Entscheidungen aus dem Discretion-Bereich von D-04..D-07, die dieser Plan festlegt:
- Spaltennamen:
internalNameundldapObjectGuid, beideString?, Stil identisch zuldapDn(RESEARCH.md/PATTERNS.md).internalNametraegt bewusst keinen Unique-Index — zwei Gruppen duerfen denselben internen Namen tragen. - Kein zweiter Discovery-Endpoint: D-01 verlangt Wiederverwendung der bestehenden AD-Gruppensuche. Statt eines zweiten
GET-Endpoints wirdGET /ldap/groupsumalreadyImportederweitert; beide Sektionen der Seite rufen denselben Endpoint mit eigenem State auf (loest RESEARCH Open Question 2). - Import loest keinen Sofort-Sync aus (UI-SPEC Copywriting Contract): Mitgliedschaften fuellt der naechste turnusmaessige oder manuelle Lauf; die Oberflaeche sagt das per
membershipHintexplizit. - Namenskollision: Reject-with-Report je Gruppe. Das Ergebnisobjekt traegt ein eigenes
nameCollisions: string[](Gruppennamen) nebenerrors: string[], damit die Kollisionsmeldung im Frontend uebersetzbar bleibt (admin.ldap.groupImport.nameCollisionError) statt als deutscher Backend-String durchgereicht zu werden. Der Fehler-Zaehler inresultSummaryisterrors.length + nameCollisions.length. - Bereits importierte Gruppen werden gekennzeichnet, nicht ausgeblendet (UI-SPEC E1/populated).
- Identitaetsmodell (Assumption-Delta-Notiz):
internalNamewird add-alongside eingefuehrt, nicht promote —Group.namebleibt der AD-gelieferte Name und das persistierte Sortierkriterium,internalNameist ein optionaler Anzeige-Override. Akzeptierte Schuld: jeder Anzeige-Konsument muss den FallbackinternalName ?? nameselbst kennen. Was spaeter einen Promote erzwingen wuerde: sobald Sortierung, Suche oder ein Export ueber den Anzeigenamen laufen muss (heute sortiertGroupsService.listForTenantuebername) — dann wird eine berechnete/persistiertedisplayName-Spalte faellig. Dieser Punkt ist beratend und blockiert nichts. </decisions_recorded>
Ohne ldapObjectGuid bliebe nur ldapDn als Schluessel — der DN enthaelt den CN, jede AD-Umbenennung aendert ihn, und der Sync koennte Umbenennung strukturell nicht von Verschwinden unterscheiden. Er wuerde die Gruppe samt Mitgliedschaften und Modulfreigaben loeschen und neu anlegen.
Beide Spalten in einer Migration (empfohlen)
Erfuellt D-04 woertlich; macht SC-3/SC-4 ueberhaupt erst unterscheidbar; eine Migration statt zwei; Unique-Index folgt exakt dem bestehenden (tenantId, ldapDn)-Muster (NULL ist in Postgres je Zeile distinct).
One-way: Rueckbau nach Vergabe echter interner Namen ist Datenverlust. Erhoeht die Group-Tabelle um zwei Spalten.
Nur ldapObjectGuid, internalName spaeter
Kleinere Migration, D-04 bleibt rueckbaubar.
Verfehlt die tragende Idee der Phase (CONTEXT.md <specifics>: der interne Name kam vom User selbst); erzwingt spaeter eine zweite Migration mit identischem one-way-Risiko.
Zurueckstellen und Phase neu zuschneiden
Keine Schemaaenderung.
Ohne beide Spalten ist keines der fuenf Success Criteria erreichbar; die Phase haette keinen Inhalt.
<acceptance_criteria>
- Die Antwort ist eine der drei Options-IDs (approve-both, approve-guid-only, defer); jede andere Eingabe wird als Rueckfrage behandelt, nicht als Freigabe
- Bei approve-both faehrt Task 2 unveraendert fort
- Bei approve-guid-only oder defer wird die Ausfuehrung angehalten und die Phase zur Neuplanung zurueckgemeldet — die uebrigen vier Plaene der Phase setzen beide Spalten voraus
</acceptance_criteria>
Antworte mit: approve-both, approve-guid-only oder defer
Schicht 2 — LdapService. In apps/api/src/ldap/ldap.service.ts:
- Erweitere
LdapDirectoryEntryumalreadyImported?: boolean. - Neues exportiertes Interface
LdapGroupImportResultmitimported: number,skipped: number,nameCollisions: string[],errors: string[]. - Neuer
static escapeLdapFilterBuffer(buf: Buffer): string— byteweises Backslash-plus-zweistelliges-Hex je Byte (RFC-4515-Praxis, siehe RESEARCH.md Pattern 3). Wird in diesem Plan noch nicht aufgerufen; Plan 16-03 nutzt ihn fuer den Existenz-Sweep. Er entsteht hier, damit beide Escaping-Bausteine (String und binaer) an einer Stelle stehen. listGroups(...)bekommt einen zweiten ParametertenantId: string. Die LDAP-Suche fordert zusaetzlichobjectGUIDan und setztexplicitBufferAttributes: ['objectGUID']— dieser Punkt ist nicht optional: ohne diese Option versuchtldaptseine UTF-8-Dekodierung des 16-Byte-Werts (RESEARCH.md Pitfall 2). Fuer jeden Eintrag vom Typgroupwird der Buffer per.toString('hex')in Kleinbuchstaben-Hex ueberfuehrt. Nach dem Aufbau der Ergebnisliste eine einzige Queryprisma.group.findMany({ where: { tenantId, ldapObjectGuid: { in: <alle Hex-Werte> } }, select: { ldapObjectGuid: true } })— Vorbild ist die Existenz-Query insearchUsers(). Aus dem Ergebnis ein Set bauen undalreadyImportedje Eintrag setzen; OU-Eintraege bekommenalreadyImported: false. Der Hex-Wert selbst wird NICHT an den Client geliefert (er hat dort keinen Verwendungszweck). Sortiere die Rueckgabe abschliessend nachnameperlocaleCompare, bei Gleichstand nachdn.- Neue Methode
async importGroupsByDn(config: LdapConfigData, tenantId: string, dns: string[]): Promise<LdapGroupImportResult>. Geruest exakt wieimportUsersByDn(): einClientausbuildClientOptions,bind(),try/finallymitunbind()im finally. Pro DN eine base-scoped Suche (scope: 'base',filter: '(objectClass=group)',attributes: ['cn', 'dn', 'objectGUID'],explicitBufferAttributes: ['objectGUID']) in einem eigenen try/catch, das den Lauf niemals abbricht (Vorbild: die Pro-Gruppe-Schleife insyncGroupMembershipsForTenant, Zeilen 860-934). Kein Treffer oder kein Buffer unterobjectGUID→ Zeile inerrorsmit dem DN,continue. Sonstcnalsname(Array-Wert wie inmapEntryaufloesen) und Hex-GUID bilden. Vorab-Pruefung perldapObjectGuid: existiert die Gruppe im Mandanten schon,skipped++undcontinue. Sonst anlegen ueberforTenant(this.prisma, tenantId)mitgroup.create({ data: { tenantId, name, ldapDn, ldapObjectGuid } })—isDefaultwird nicht gesetzt,internalNamewird nicht gesetzt. Prisma-FehlerP2002auswerten: betrifft das Zielname, den Gruppennamen annameCollisionshaengen; betrifft esldapObjectGuid, wieskippedbehandeln; alles andere als Fehlerzeile sammeln.GroupsService.create()wird bewusst NICHT aufgerufen — dessenConflictExceptionist fuer HTTP gebaut und wuerde den Batch abbrechen (RESEARCH.md Pitfall 4).
Schicht 3 — DTO + Controller. In apps/api/src/ldap/dto/ldap-config.dto.ts ImportGroupsDto mit @IsArray() @ArrayNotEmpty() @IsString({ each: true }) dns!: string[] — strukturgleich zu ImportUsersDto. In apps/api/src/ldap/ldap.controller.ts: listGroups() reicht req.tenantId als zweiten Parameter an den Service durch. Neue Route @Post('groups/import') mit @Roles(Role.ADMIN, Role.SUPER_ADMIN), identischem Guard-Praefix wie importUsers() (tenantId-Pruefung, getConfig-Pruefung), Aufruf ldapService.importGroupsByDn(<config-Objekt wie bei importUsers>, tenantId, dto.dns). Platziere die Route unmittelbar nach GET 'groups' und vor allen dynamischen Segmenten — statische Routen stehen in diesem Projekt immer vor :id-Routen.
Schicht 4 — Frontend. In apps/web/src/app/(portal)/admin/ldap/page.tsx: LdapDirectoryEntry um alreadyImported?: boolean erweitern, neues Interface GroupImportResult mit imported, skipped, nameCollisions: string[], errors: string[]. Sieben neue State-Variablen: groupImportCandidates, groupImportSearch, discoveringGroupImport, selectedImportDns, importingGroups, groupImportResult, groupImportError. Handler handleDiscoverGroupsToImport ruft GET /ldap/groups mit credentials: 'include' und filtert die Antwort auf type === 'group' — OUs sind nicht importierbar. Handler handleImportGroups postet { dns: selectedImportDns } an /ldap/groups/import, setzt danach selectedImportDns zurueck und ruft handleDiscoverGroupsToImport() erneut auf, damit die Markierung sofort nachzieht. Beide Handler setzen bei !res.ok und im catch-Zweig groupImportError auf den passenden uebersetzten Text und rendern ihn sichtbar; das in dieser Datei sonst uebliche stille Verschlucken von Fehlern wird fuer diese beiden Handler ausdruecklich nicht uebernommen (Owner-Entscheidung 2026-08-06). Bei Erfolg wird groupImportError auf null zurueckgesetzt.
Neue Sektion zwischen Sektion 2.5 (endet Zeile 806) und Sektion 2.55 (beginnt Zeile 808), im gleichen rounded-lg border border-border p-6-Card-Muster: h2 mit admin.ldap.groupImport.title, Beschreibungsabsatz description, Discover-Button (Markup wie Zeilen 694-703, Label discover, waehrend discoveringGroupImport stattdessen tCommon('loading') und disabled), Suchfeld ueber der Liste (Markup wie Zeilen 707-713, Placeholder searchPlaceholder, filtert groupImportCandidates case-insensitive ueber name und dn), Ergebnisliste max-h-64 overflow-y-auto rounded-md border border-border divide-y divide-border mit Zeilen-Markup wie Zeilen 841-876 (Checkbox, Name in font-medium, DN in font-mono text-xs text-muted-foreground truncate, rechts das alreadyImported-Badge in bg-muted text-muted-foreground, Checkbox dann disabled und Zeile opacity-60), Import-Button mit Auswahlzahl im Label und disabled bei leerer Auswahl oder laufendem Import, Ergebnisabsatz mt-3 text-sm text-muted-foreground mit resultSummary (Fehlerzahl = errors.length + nameCollisions.length), darunter je Eintrag aus nameCollisions eine text-xs text-destructive-Zeile mit nameCollisionError und je Eintrag aus errors eine text-xs text-destructive-Zeile mit dem Rohtext, darunter der statische Hinweis membershipHint in text-xs text-muted-foreground. Leere geladene Liste → noneFound; nicht-leere Liste, aber leeres Filterergebnis → noMatches. Fehlerzustand groupImportError als text-sm text-destructive unterhalb des jeweils ausloesenden Buttons.
Schicht 5 — i18n. Beide Dateien apps/web/src/messages/de.json und apps/web/src/messages/en.json bekommen unter admin.ldap einen Block groupImport mit allen im Abschnitt "Artifacts this phase produces" gelisteten Keys. Deutsche Texte woertlich aus dem Copywriting Contract der UI-SPEC; englische Texte sinngleich. Keine firmen- oder kundenspezifischen Beispielwerte in Platzhaltern.
Schicht 6 — Tests. Neuer describe-Block LdapService — AD group import (SC-1/SC-2, D-01/D-02) in apps/api/src/ldap/ldap.service.spec.ts, Mock-Aufbau wie im bestehenden Block ab Zeile 283. Testfaelle exakt nach der <behavior>-Liste oben, plus je ein Fall fuer die Sortierung der Discovery-Liste und fuer alreadyImported.
cd apps/api && npx vitest run src/ldap/ldap.service.spec.ts && npx tsc --noEmit
cd apps/web && npx tsc --noEmit
node -e "const de=require('./apps/web/src/messages/de.json'),en=require('./apps/web/src/messages/en.json');const k=Object.keys(de.admin.ldap.groupImport).sort().join(',');const l=Object.keys(en.admin.ldap.groupImport).sort().join(',');if(k!==l)throw new Error('groupImport keys differ: '+k+' vs '+l);if(!k.includes('membershipHint'))throw new Error('membershipHint missing')"
<acceptance_criteria>
- grep -c 'explicitBufferAttributes' apps/api/src/ldap/ldap.service.ts ist >= 2 (Discovery-Suche und Import-Suche fordern den Buffer beide explizit an)
- grep -q 'ldapObjectGuid' apps/api/prisma/schema.prisma und grep -q 'internalName' apps/api/prisma/schema.prisma treffen beide
- grep -q '@@unique(\[tenantId, ldapObjectGuid\])' apps/api/prisma/schema.prisma trifft
- grep -q "Post('groups/import')" apps/api/src/ldap/ldap.controller.ts trifft, und in derselben Datei steht in den drei Zeilen darunter @Roles(Role.ADMIN, Role.SUPER_ADMIN)
- grep -c 'forTenant' apps/api/src/ldap/ldap.service.ts ist gegenueber dem Stand vor diesem Task um mindestens 1 gestiegen (der Schreibpfad in importGroupsByDn ist mandantengescopt)
- cd apps/api && npx vitest run src/ldap/ldap.service.spec.ts ist gruen und der neue describe-Block enthaelt mindestens 8 it(-Faelle
- cd apps/api && npx tsc --noEmit und cd apps/web && npx tsc --noEmit sind beide fehlerfrei
- de.json und en.json tragen unter admin.ldap.groupImport denselben Schluesselsatz (Node-Check oben)
- In apps/web/src/app/(portal)/admin/ldap/page.tsx enthaelt keiner der beiden neuen Handler einen leeren catch-Block: awk '/handleDiscoverGroupsToImport|handleImportGroups/,/^ };/' 'apps/web/src/app/(portal)/admin/ldap/page.tsx' | grep -c 'setGroupImportError' ist >= 4
- Die neue Sektion rendert ausschliesslich Eintraege mit type === 'group' — grep -q "type === 'group'" 'apps/web/src/app/(portal)/admin/ldap/page.tsx' trifft
</acceptance_criteria>
Ein Admin kann in /admin/ldap AD-Gruppen entdecken, einzelne anhaken und importieren; je angehakter Gruppe existiert danach eine Group-Zeile mit ldapDn und ldapObjectGuid, bereits importierte Gruppen sind gekennzeichnet und nicht erneut anwaehlbar, und ein Fehler im Discovery- oder Import-Request ist auf der Seite sichtbar. Die Scheibe ist committed.
Verfahren wortgleich zum dokumentierten Muster: docker start tessera-ctl-db-1, dann die Container-IP per docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tessera-ctl-db-1 ermitteln und 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 ausfuehren. Erwartetes Ergebnis: ein neues Verzeichnis mit dem Suffix _add_group_internal_name_and_object_guid unter apps/api/prisma/migrations/, dessen migration.sql zwei ADD COLUMN-Anweisungen fuer internalName und ldapObjectGuid sowie einen CREATE UNIQUE INDEX auf ("tenantId", "ldapObjectGuid") enthaelt.
Erzeugt Prisma stattdessen ein Reset-Angebot oder eine Drift-Warnung, ist das ein Abbruchgrund — nicht bestaetigen, sondern den Zustand melden. Ein Reset wuerde die lokale Entwicklungsdatenbank leeren.
Keine RLS-Anweisung in dieser Migration: die Policy aus 20260804130918_groups_rls_policies gilt tabellenweit und greift auf neue Spalten automatisch mit. Falls Prisma die Migration mit zusaetzlichen, nicht angeforderten Anweisungen erzeugt (z.B. Aenderungen an fremden Tabellen), diese entfernen und den Grund im SUMMARY festhalten.
Danach apps/api/src/groups/migration-sql.spec.ts um einen describe-Block fuer die neue Migration erweitern, der ueber den vorhandenen readMigrationSql-Helper mit dem Suffix _add_group_internal_name_and_object_guid liest und die drei Bestandteile per toContain festnagelt: die Spalte internalName, die Spalte ldapObjectGuid und den Unique-Index auf ("tenantId", "ldapObjectGuid").
In Produktion laeuft prisma migrate deploy beim Container-Start (apps/api/Dockerfile:38) — es ist kein zusaetzlicher Deploy-Schritt zu planen, und der Testserver wird von diesem Task nicht angefasst.
cd apps/api && npx vitest run src/groups/migration-sql.spec.ts
ls -d apps/api/prisma/migrations/*_add_group_internal_name_and_object_guid | wc -l | grep -qx 1
cd apps/api && npx vitest run
<acceptance_criteria>
- Genau ein Verzeichnis unter apps/api/prisma/migrations/ endet auf _add_group_internal_name_and_object_guid
- Dessen migration.sql enthaelt die Literale internalName, ldapObjectGuid und "tenantId", "ldapObjectGuid"
- Die neue Migration enthaelt keine ROW LEVEL SECURITY-Anweisung: grep -ci 'row level security' apps/api/prisma/migrations/*_add_group_internal_name_and_object_guid/migration.sql ergibt 0
- cd apps/api && npx vitest run src/groups/migration-sql.spec.ts ist gruen
- cd apps/api && npx vitest run ist vollstaendig gruen (keine Regression in den 387+ bestehenden Faellen)
- psql-Gegenprobe gegen die lokale Datenbank zeigt beide Spalten auf "Group": docker exec tessera-ctl-db-1 psql -U tessera -d tessera -c '\d "Group"' listet internalName und ldapObjectGuid
</acceptance_criteria>
Die Migration existiert versioniert, ist auf der lokalen Datenbank angewendet, per SQL-Textest festgenagelt, und die vollstaendige API-Testsuite ist gruen.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
Browser → API (POST /ldap/groups/import) |
Admin-gelieferte DN-Liste ueberquert hier die Grenze; Mandantenkontext kommt ausschliesslich aus req.tenantId, nie aus dem Body |
| API → Active Directory | Admin-gewaehlte DNs werden Teil einer LDAP-Suche; AD-gelieferte Werte (cn, objectGUID) kommen ungeprueft zurueck |
| API → PostgreSQL | Schreibpfad auf Group unter RLS (FORCE ROW LEVEL SECURITY, Migration 20260804130918_groups_rls_policies) |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-16-01 | Tampering | LdapService.importGroupsByDn LDAP-Suche |
high | mitigate | Suche laeuft scope: 'base' direkt auf dem uebergebenen DN mit konstantem Filter (objectClass=group) — der admin-gelieferte Wert wird als Such-Base uebergeben, nicht in einen Filterausdruck interpoliert. Jede kuenftige Filter-Interpolation eines String-Werts nutzt escapeLdapFilterValue(), jede eines Binaerwerts den in diesem Plan angelegten escapeLdapFilterBuffer() |
| T-16-02 | Elevation of Privilege | Schreibpfad group.create |
high | mitigate | forTenant(this.prisma, tenantId) setzt app.current_tenant; tenantId stammt aus req.tenantId, nie aus dem Request-Body. RLS ist das zweite Netz |
| T-16-03 | Spoofing | Route POST /ldap/groups/import |
high | mitigate | @Roles(Role.ADMIN, Role.SUPER_ADMIN) + bestehender RolesGuard, identisch zu allen uebrigen LDAP-Routen (ASVS V4) |
| T-16-04 | Information Disclosure | Discovery-Antwort GET /ldap/groups |
low | mitigate | Der Hex-objectGUID wird bewusst NICHT an den Client geliefert; die Antwort traegt nur dn, name, type, alreadyImported |
| T-16-05 | Tampering | cn aus dem AD als Group.name |
medium | mitigate | Der Wert wird ausschliesslich als Prisma-Parameter geschrieben (keine Raw-SQL-Interpolation) und im Frontend als React-Text gerendert (kein dangerouslySetInnerHTML) |
| T-16-SC | Tampering | Paketinstallation | low | accept | Diese Phase installiert kein einziges neues Paket — es wird nur eine Option der bereits gepinnten ldapts@8.1.8 genutzt. Kein Registry-Zugriff, damit kein Supply-Chain-Vektor und keine Legitimacy-Pruefung erforderlich |
| </threat_model> |
Flagged assumption (nicht in diesem Plan aufloesbar): RESEARCH.md A1 (objectGUID bleibt ueber eine AD-Umbenennung stabil) und A2 (Binaer-Filter-Syntax) sind gegen kein echtes Verzeichnis geprueft. Dieser Plan schreibt die GUID nur; er liest sie nicht filternd zurueck. Die Aufloesung beider Annahmen ist Bestandteil von Plan 16-03 und laeuft dort als Live-Pruefung gegen ViCoTest.
<success_criteria>
- Admin waehlt in
/admin/ldapeinzelne AD-Gruppen und importiert sie; je Auswahl entsteht genau eine Tessera-Gruppe mitldapDnundldapObjectGuid(SC-1) - Nicht ausgewaehlte AD-Gruppen entstehen nicht (SC-2)
Group.internalNameundGroup.ldapObjectGuidexistieren in Schema, Migration und laufender Datenbank (D-04)- Doppelter Import derselben Gruppe erzeugt keine Doppelzeile
- Namenskollision meldet die betroffene Gruppe und bricht den restlichen Import nicht ab
- Discovery- und Import-Fehler sind auf der Seite sichtbar, nicht stumm </success_criteria>