Files
Laufzettel/docs/betrieb/entra-id.md
T
Florian WildeandClaude Opus 5 76a61ec771
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(verzeichnis): Entra ID als Quelle ueber Microsoft Graph (#48)
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>
2026-10-06 23:36:36 +02:00

6.6 KiB

Entra ID als Verzeichnisquelle

Ein Haus ohne lokales Active Directory hat kein LDAP, auf das sich zeigen ließe. Für reine Microsoft-365-Umgebungen liest Laufzettel über Microsoft Graph — mit denselben Regeln wie der LDAP-Abgleich: anlegen, aktualisieren, deaktivieren, nie löschen.

Dieser Weg ist nicht gegen einen echten Mandanten gelaufen. Geprüft ist er gegen einen HTTP-Doppelgänger mit aufgezeichneten Graph-Antworten (tests/graph.test.ts), und die Form dieser Antworten stammt aus der Dokumentation, nicht aus einem Mitschnitt. Die Prüfliste am Ende dieser Seite gehört einmal abgearbeitet, so wie bei Proxmox (#024).

Was in Entra ID einzurichten ist

1. App-Registrierung anlegen

Im Entra-Portal unter Identität → Anwendungen → App-Registrierungen → Neue Registrierung:

Feld Wert
Name Laufzettel Verzeichnisabgleich
Unterstützte Kontotypen Nur Konten in diesem Organisationsverzeichnis
Umleitungs-URI keiner — Laufzettel meldet sich als Anwendung an, nicht im Namen eines Menschen

Notiere von der Übersichtsseite die Anwendungs-ID (Client) und die Verzeichnis-ID (Mandant).

2. Berechtigung erteilen

Unter API-Berechtigungen → Berechtigung hinzufügen → Microsoft Graph → Anwendungsberechtigungen:

Berechtigung Wofür
User.Read.All die Benutzer lesen

Dann Administratoreinwilligung erteilen. Ohne diesen Klick antwortet Graph mit 403 und Authorization_RequestDenied — Laufzettel nennt den Grund in der Fehlermeldung, weil ihn sonst niemand errät.

Anwendungsberechtigung, nicht delegierte. Der Abgleich läuft nachts, wenn niemand angemeldet ist; eine delegierte Berechtigung braucht einen angemeldeten Menschen und funktioniert dort nicht.

Directory.Read.All wird nicht gebraucht. User.Read.All deckt die Benutzer und über $expand=manager auch den Vorgesetzten ab — die weitergehende Berechtigung wäre mehr Zugriff, als die Aufgabe verlangt.

3. Geheimnis erzeugen

Unter Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel. Die Laufzeit ist eine Entscheidung mit Folgen: läuft das Geheimnis ab, hört der Abgleich auf, und niemand merkt es, weil jeder Lauf weiter funktioniert. Setz dir eine Erinnerung, und sieh unter Verwaltung → Verzeichnis nach dem Status, wenn Personen nicht mehr aktualisiert werden.

Der Wert ist nur beim Anlegen sichtbar. Kopier ihn sofort.

In Laufzettel einrichten

Unter Verwaltung → Verzeichnis → Quelle anlegen, Art Microsoft Graph:

Feld Wert
Mandanten-ID die Verzeichnis-ID oder der Domänenname (contoso.onmicrosoft.com)
Anwendungs-ID die Anwendungs-ID (Client) aus Schritt 1
Client-Secret der Wert aus Schritt 3 — wird verschlüsselt gespeichert und nie wieder angezeigt

Dann Verbindung testen: es erscheinen die ersten zehn zugeordneten Personen. Steht dort nichts, hilft die Tabelle unter „Wenn es klemmt“.

Was anders ist als bei einem lokalen AD

LDAP (AD) Microsoft Graph (Entra ID)
Dauerhafte Kennung objectGUID (binär, umsortiert) id (GUID als Text)
Nachname sn surname
Funktion title jobTitle
Anmeldename sAMAccountName onPremisesSamAccountName
Vorgesetzter manager als DN, danach aufgelöst manager direkt als Kennung
Deaktivierte Konten über den Filter ausgeschlossen über $filter=accountEnabled eq true
{{ou}} aus dem DN leer

{{ou}} bleibt leer, und das ist Absicht

Entra ID hat keinen DN und keine OU. Etwas hineinzudeuten — den Abteilungsnamen, eine erfundene Struktur — wäre ein Wert, den kein PowerShell-Befehl brauchen kann, und er sähe richtig aus.

Steht an der Abteilung eine Ziel-OU gepflegt, nimmt Laufzettel die ({{ou}} hat zwei Quellen, siehe docs/architektur.md). Wer OU-abhängige Befehle fährt, braucht ein lokales AD — oder pflegt die Ziel-OU je Abteilung von Hand.

{{sam}} ist leer ohne AD-Synchronisierung

onPremisesSamAccountName kommt aus einem lokalen AD. Ein Mandant, der nie eines hatte, liefert dort nichts — eine reine Cloud-Umgebung hat keinen Anmeldenamen in diesem Sinn. Vorlagen, die {{sam}} benutzen, zeigen dann einen leeren Wert; gemeint ist meist ohnehin der UPN.

Die Art einer Quelle lässt sich nicht ändern

An ihr hängt, was die dauerhafte Kennung bedeutet. Nach einem Wechsel von LDAP auf Graph würde kein einziger Mitarbeiter wiedererkannt: der nächste Abgleich legte die ganze Belegschaft neu an und setzte die alten Zeilen auf aktiv = false — mit allen Läufen und Geräten daran.

Wer wechselt, legt eine zweite Quelle an und lässt die erste stehen, bis die Personen übernommen sind. Laufzettel übernimmt dabei von Hand angelegte und bereits vorhandene Personen über Name und Mailadresse, statt Dubletten anzulegen (#016).

Wenn es klemmt

Meldung Ursache
401 beim Anmelden Mandant, Anwendungs-ID oder Geheimnis stimmen nicht — oder das Geheimnis ist abgelaufen. Die ausführliche Antwort von Entra ID steht im Protokoll des Dienstes, nicht in der Oberfläche: sie nennt Mandant und Anwendung.
403 Authorization_RequestDenied Die Administratoreinwilligung für User.Read.All fehlt. Erteilen und erneut testen.
Invalid expand clause Sollte nicht vorkommen — Laufzettel baut $expand=manager($select=id) mit unkodierten Klammern. Tritt es auf, steht ein Proxy dazwischen, der die Adresse umschreibt.
Verbindung testen zeigt niemanden Der Mandant hat keine Benutzer mit accountEnabled = true, oder alle wurden übersprungen. Die Liste der Übersprungenen steht unter dem Ergebnis.
Personen werden nicht mehr aktualisiert Zuerst den Status der Quelle ansehen. Fast immer ist das Client-Secret abgelaufen.

Prüfliste für den ersten echten Lauf

Diese Punkte sind nicht automatisiert geprüft — sie brauchen einen echten Mandanten:

  • App-Registrierung angelegt, User.Read.All als Anwendungsberechtigung, Einwilligung erteilt
  • „Verbindung testen“ zeigt die ersten zehn Personen mit richtigen Namen und Abteilungen
  • Der Vorgesetzte steht bei jemandem, der einen hat (manager über $expand)
  • Erster Abgleich legt an; die Zahl passt zur Zahl der aktiven Konten im Mandanten
  • Zweiter Abgleich legt nichts an und meldet nur Änderungen
  • Ein Konto in Entra ID sperren → nächster Abgleich setzt aktiv = false, löscht aber nichts
  • Ein Mandant mit mehr als 999 Benutzern: der Seitenabruf über @odata.nextLink läuft durch
  • Nach einem abgelaufenen Geheimnis steht der Grund an der Quelle und nicht nur im Log