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>
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.Allals 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.nextLinkläuft durch - Nach einem abgelaufenen Geheimnis steht der Grund an der Quelle und nicht nur im Log