Ein Haus ohne lokales Active Directory hat kein LDAP, auf das sich zeigen liesse. Eine Verzeichnisquelle hat jetzt eine **Art** — LDAP oder GRAPH —, und beide laufen durch **denselben** Abgleich: anlegen, aktualisieren, deaktivieren, nie loeschen. Nur das Holen der Eintraege unterscheidet sich; zwei Abgleiche nebeneinander wuerden auseinanderdriften, und die Zusage „es wird nichts geloescht“ gaelte dann fuer eine Art und fuer die andere nicht mehr ganz. Gelesen wird mit dem Client-Credentials-Flow, seitenweise ueber @odata.nextLink, der Vorgesetzte ueber $expand=manager($select=id). Das Client-Secret wird verschluesselt gespeichert wie das Bind-Passwort und erreicht keine Antwort und keinen Protokolleintrag. Die Art ist **unveraenderlich**, und das steht nicht im Issue: an ihr haengt, was `externeId` bedeutet. Im AD eine objectGUID, in Entra ID eine andere GUID — nach einem Wechsel wuerde kein einziger Mitarbeiter wiedererkannt, der naechste Abgleich legte die ganze Belegschaft neu an und setzte die alten Zeilen auf aktiv = false, mit allen Laeufen und Geraeten daran. Der teuerste Fund beim Bauen: „in Entra ID deaktiviert“ als *uebersprungen* zu melden haette die Leute **aktiv** gelassen. Uebersprungen heisst im Abgleich „nicht zuordenbar“, und solche Eintraege werden seit #016 absichtlich nicht abgeschaltet. Gefiltert wird deshalb serverseitig mit `$filter=accountEnabled eq true`, als Gegenstueck zum LDAP-Standardfilter. Gefunden hat es der Test — aber nur, weil der Doppelgaenger den Filter wirklich auswertet statt ihn bloss zu empfangen. {{ou}} bleibt bei einer Graph-Quelle leer: Entra ID hat keine OU, und etwas hineinzudeuten waere ein Wert, den kein PowerShell-Befehl brauchen kann — er saehe nur richtig aus. Steht an der Abteilung eine Ziel-OU gepflegt, greift die. Gegen einen echten Entra-Mandanten ist das **nicht** gelaufen. Geprueft ist es gegen einen HTTP-Doppelgaenger mit aufgezeichneten Antworten; die Pruefliste fuer den ersten echten Lauf steht am Ende von docs/betrieb/entra-id.md. Closes #48 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.7 KiB
#028 M15 – Quellenarten und Microsoft-Graph-Quelle
Status: erledigt Im Tracker: #48 Meilenstein: v0.2.0 Labels: phase-2, backend, frontend Geschätzt: 10 h Tatsächlich: 4 h 10 min
Ziel
Ein Haus ohne lokales AD hat kein LDAP, auf das sich zeigen ließe. Für reine Entra-ID-Umgebungen braucht es eine zweite Art von Verzeichnisquelle, die über Microsoft Graph liest — mit denselben Regeln wie der LDAP-Abgleich: anlegen, aktualisieren, deaktivieren, nie löschen.
Dieser Meilenstein legt dazu die Weiche, die auch #029 braucht:
Verzeichnisquelle bekommt eine Art. Heute ist jede Quelle implizit LDAP.
Zu entscheiden
- Ein Enum
QuellenArt {LDAP GRAPH SCIM}aufVerzeichnisquelle— Empfehlung. Die Felderurl,bindDnundbasisDnsind dann je Art unterschiedlich belegt; was eine Art braucht, prüft der Service, nicht die Datenbank. Die Alternative wäre eine Tabelle je Art mit gemeinsamer Oberklasse — mehr Struktur, als drei Arten rechtfertigen. - Wie
{{ou}}in Entra ID aussieht. Es gibt keinen DN und keine OU. Ehrlich ist, den Platzhalter bei einer Graph-Quelle leer zu lassen, statt etwas hineinzudeuten — und das in der Anleitung zu sagen. Wer OU-abhängige Befehle fährt, braucht ein lokales AD.
Aufgaben
QuellenArtals Enum, Migration mitLDAPfür alle bestehenden Zeilen- Graph-Zugang:
tenantId,clientId,clientSecret(verschlüsselt wie das Bind-Passwort, nie in einer Antwort), Token über Client-Credentials-Flow, Zwischenspeicher bis kurz vor Ablauf - Lesen über
GET /v1.0/usersmit$select, Seitenabruf über@odata.nextLink; der Vorgesetzte über$expand=manager($select=id) - Attributzuordnung mit Graph-Vorgaben (
givenName,surname,displayName,mail,userPrincipalName,onPremisesSamAccountName,department,jobTitle,accountEnabled) externeIdist die Entra-id(GUID). Sie ist stabil und übersteht eine Umbenennung- „Verbindung testen“ wie bei LDAP: die ersten zehn zugeordneten Treffer zeigen
- Der Abgleich läuft durch denselben Dienst wie LDAP — nur das Holen der Einträge unterscheidet sich. Sonst driften die Regeln auseinander
- Admin → Verzeichnis zeigt die Art je Quelle und bietet beim Anlegen die Wahl
- Anleitung Kapitel 06 und
docs/betrieb/: was in Entra ID einzurichten ist (App-Registrierung,User.Read.All,Directory.Read.All, Administratoreinwilligung)
Akzeptanzkriterien
- Test gegen einen abspielenden HTTP-Doppelgänger mit aufgezeichneten Graph-Antworten: erster
Abgleich legt an, zweiter aktualisiert ohne Dubletten, ein
accountEnabled: falsesetztaktiv = false - Test: der Seitenabruf folgt
@odata.nextLinküber mindestens drei Seiten - Test:
clientSecreterscheint in keiner Antwort und in keinem Protokolleintrag - Test: eine Graph-Quelle lässt
{{ou}}leer, statt einen Wert zu erfinden - Der LDAP-Weg verhält sich unverändert — alle Tests aus #016 bleiben grün
- Offen und benannt: gegen einen echten Entra-Mandanten ist das nicht gelaufen; die Gegenprobe gehört in eine Prüfliste wie bei #024
Beim Umsetzen dazugekommen
- „Deaktiviert in Entra ID“ als übersprungen zu melden war falsch — und hätte Leute aktiv
gelassen. „Übersprungen“ bedeutet im Abgleich „nicht zuordenbar“, und solche Einträge werden
seit #016 absichtlich nicht abgeschaltet (ein Konto ohne GUID soll nicht die Person
deaktivieren). Ein gesperrtes Entra-Konto wäre damit genau umgekehrt behandelt worden als im AD.
Gefiltert wird jetzt serverseitig mit
$filter=accountEnabled eq true, als Gegenstück zum LDAP-Standardfilter. Gefunden hat es der Test „setzt aktiv = false“ — und nur, weil der Doppelgänger den Filter wirklich auswertet statt ihn bloß zu empfangen. URLSearchParamskodiert$zu%24. Die Abfrage hätte%24selectund%28inmanager($select=id)geschickt. Graph nimmt das teils an und antwortet teils mit „Invalid expand clause“ — und eine Adresse, die in einem Mitschnitt anders aussieht als in der Dokumentation, kostet bei der Fehlersuche eine Stunde. Die Parameter werden jetzt von Hand zusammengesetzt.- Die Art einer Quelle ist unveränderlich, und das steht nicht im Issue. An ihr hängt, was
externeIdbedeutet: im AD eineobjectGUID, in Entra ID eine andere GUID. Nach einem Wechsel würde kein einziger Mitarbeiter wiedererkannt — der nächste Abgleich legte die ganze Belegschaft neu an und setzte die alten Zeilen aufaktiv = false, mit allen Läufen und Geräten daran. Der Dienst weist einen Wechsel ab, die Oberfläche bietet ihn nicht an, und die Anleitung sagt, was stattdessen zu tun ist. zuordnungLesenbrauchte die Art als Parameter. Ohne sie bekäme eine Graph-Quelle mit leerer Zuordnung die AD-Namen und fragte bei Graph nachsnundobjectGUID, die es dort nicht gibt. Der StandardwertLDAPhält jeden Aufrufer von vor #028 bei seinem Verhalten.- Die Zugangs-Helfer mussten in ein eigenes Modul.
quellenleser.ts(die Weiche) brauchtzugangAus, undservices/verzeichnis.tsbraucht die Weiche für „Verbindung testen“ — zusammen ein Import-Zyklus. Der läuft in ESM oft, aber nicht zuverlässig, und ein Modul, das beim Laden halb leer ist, ergibt Fehler, die niemand liest. Jetzt liegt beides insrc/server/verzeichnis/zugang.ts. SCIMsteht im Enum, wird aber abgewiesen. Die Weiche braucht #029, und ein Wert, der stillschweigend nichts tut, wäre hier besonders teuer: ein Abgleich, der null Einträge liest undERFOLGmeldet, hätte über die Abschaltregel die ganze Belegschaft deaktiviert — genau der Fall, der in #016 einen Reviewbefund gekostet hat. Mit eigenem Test.- Die Felder je Art hält die Datenbank fest. Ein CHECK verlangt bei
LDAPdie LDAP-Felder und verbietet die Graph-Felder, beiGRAPHumgekehrt. Ohne den zweiten Teil wäre eine Graph-Quelle mitbasisDnmöglich — eine Zeile, die zwei Dinge gleichzeitig behauptet. - Der Vorgesetzte kommt bei Graph direkt, nicht über eine DN-Auflösung.
RoherEintraghat dafür ein zweites Feld (vorgesetzterDirekt) und nicht ein Feld mit zwei Bedeutungen: ein DN und eine GUID sehen verschieden aus, aber ein Feld, das mal das eine und mal das andere trägt, verwechselt irgendwann jemand. - Der Token-Zwischenspeicher hat 60 Sekunden Sicherheitsabstand. Ein Token, das in zehn Sekunden abläuft, ist für eine Seite, die zwei Sekunden braucht, formal noch gültig — und in der Mitte der nächsten nicht mehr. Der Fehler wäre ein Abgleich, der nach der dritten Seite mit 401 abbricht.
- Eine Reißleine bei 1000 Seiten. Ein
@odata.nextLink, der auf sich selbst zeigt, wäre sonst eine Schleife bis zum Speicherende — still, weil jeder einzelne Abruf gelingt. - Zwei eigene Testfehler, keine Produktfehler: die Token-Grenze liegt bei +3540 s und nicht bei „so ungefähr einer Stunde“, und die Klickprobe zählte die Felder der Bearbeiten-Formulare der bestehenden Quellen mit.
Noch offen, und benannt
Gegen einen echten Entra-Mandanten ist das nicht gelaufen. Geprüft ist es gegen einen
HTTP-Doppelgänger mit aufgezeichneten Antworten, und deren Form stammt aus der Dokumentation, nicht
aus einem Mitschnitt. Die Prüfliste steht am Ende von
docs/betrieb/entra-id.md — acht Punkte, darunter ein Mandant mit mehr als
999 Benutzern für den Seitenabruf. Dieselbe Art offener Punkt wie #024.