# 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 **Anwendungs**berechtigung, 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