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>
127 lines
6.6 KiB
Markdown
127 lines
6.6 KiB
Markdown
# 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
|