feat(verzeichnis): Entra ID als Quelle ueber Microsoft Graph (#48)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

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>
This commit is contained in:
Florian Wilde
2026-10-06 23:36:36 +02:00
co-authored by Claude Opus 5
parent 0158d0292f
commit 76a61ec771
25 changed files with 2446 additions and 382 deletions
+67 -2
View File
@@ -34,12 +34,13 @@ als Unterpunkte mit Aufgabe und Dauer.
| #033 | M20 – Live-Aktualisierung per SSE statt Polling | 1 h 40 min |
| #034 | M21 – Benachrichtigungen per Mail und Webhook | 3 h 30 min |
| #035 | M22 – Mitarbeiter-Selbstauskunft | 2 h 50 min |
| | **Summe** | **56 h 25 min** |
| #028 | M15 – Quellenarten und Microsoft-Graph-Quelle | 4 h 10 min |
| | **Summe** | **60 h 35 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 35 h 20 min | veröffentlicht |
| v0.2.0 | 21 h 05 min | in Arbeit |
| v0.2.0 | 25 h 15 min | in Arbeit |
---
@@ -1370,3 +1371,67 @@ und nicht nach jeder einzelnen — das ist der Grund, warum er alles auf einmal
- Vor beiden wäre ein unabhängiger Review über die sechs Meilensteine dieser Phase angebracht —
M17 bis M22 liegen ohne fremden Blick auf `main`.
- Weiter offen und nicht von hier aus machbar: **#024** Proxmox-Einzeiler auf einem echten PVE-Host.
---
## 2026-10-06 · Sitzung 21 · 09:00–13:10
**Issue:** #028 (M15)
**Version:** 0.2.0 (unveröffentlicht)
### Erledigt
- **Eine Verzeichnisquelle hat eine Art**: `LDAP`, `GRAPH` oder `SCIM`. Bestehende Zeilen sind
`LDAP` — das war vor #028 die einzige.
- **Entra ID über Microsoft Graph**: Client-Credentials-Flow mit Token-Zwischenspeicher, Seitenabruf
über `@odata.nextLink`, Vorgesetzter über `$expand`.
- **Derselbe Abgleich für beide Arten.** Die Weiche sitzt in
`src/server/verzeichnis/quellenleser.ts` und ist die einzige Stelle, die die Art kennt.
- 1373 Vitest-Tests (vorher 1342), davon 31 neue gegen einen HTTP-Doppelgänger mit aufgezeichneten
Graph-Antworten.
- Im gebauten Stand eine Graph-Quelle wirklich angelegt: Art gespeichert, Geheimnis verschlüsselt,
nicht im Quelltext der Seite.
### Entscheidungen
- **Ein Enum auf der Quelle, keine Tabelle je Art.** Drei Arten rechtfertigen keine Vererbung; was
eine Art braucht, hält ein CHECK fest — in **beide** Richtungen, sonst wäre eine Graph-Quelle mit
`basisDn` möglich.
- **Die Art ist unveränderlich.** An ihr hängt, was `externeId` bedeutet; nach einem Wechsel würde
kein Mitarbeiter wiedererkannt. Steht so nicht im Issue und ist die wichtigste Zusage dieses
Meilensteins.
- **`{{ou}}` bleibt bei Graph leer.** Etwas hineinzudeuten wäre ein Wert, den kein PowerShell-Befehl
brauchen kann — und er sähe richtig aus.
- **`SCIM` steht im Enum, wird aber abgewiesen.** Ein Wert, der stillschweigend nichts tut, hätte
hier die ganze Belegschaft deaktiviert.
### Probleme
- **Der teuerste Fund: „deaktiviert“ als „übersprungen“ zu melden hätte Leute aktiv gelassen.**
„Übersprungen“ heißt im Abgleich „nicht zuordenbar“, und solche Einträge werden seit #016
absichtlich nicht abgeschaltet. Ein gesperrtes Entra-Konto wäre damit genau umgekehrt behandelt
worden als ein gesperrtes AD-Konto. Jetzt filtert die Abfrage serverseitig, als Gegenstück zum
LDAP-Standardfilter.
**Gefunden hat es der Test — aber nur, weil der Doppelgänger den Filter wirklich auswertet.** Hätte
er den Filter bloß empfangen und ignoriert, wäre der Test grün gewesen und der Fehler im Betrieb
aufgefallen. Ein Doppelgänger, der nur antwortet, prüft die Anfrage nicht.
- **`URLSearchParams` kodiert `$` zu `%24`** und die Klammern in `manager($select=id)`. Graph nimmt
das teils an und antwortet teils mit „Invalid expand clause“. Jetzt von Hand zusammengesetzt.
- **Ein Import-Zyklus drohte**: die Weiche braucht `zugangAus`, und der Dienst braucht die Weiche.
Beides aufgelöst durch ein eigenes Modul. In ESM läuft so ein Zyklus oft — aber nicht zuverlässig.
- **Zwei eigene Testfehler**: die Token-Grenze liegt bei +3540 s und nicht „ungefähr eine Stunde“,
und die Klickprobe zählte die Felder der Bearbeiten-Formulare mit.
### Was offen bleibt, und das ist benannt
Gegen einen **echten Entra-Mandanten** ist nichts davon gelaufen. Die Form der Antworten im
Doppelgänger stammt aus der Dokumentation, nicht aus einem Mitschnitt — stimmt sie nicht, stimmen
Test und Wirklichkeit nicht. Die Prüfliste steht am Ende von `docs/betrieb/entra-id.md`, acht
Punkte, darunter ein Mandant mit mehr als 999 Benutzern. Dieselbe Art offener Punkt wie #024.
### Nächster Schritt
- **M16 (#029)** SCIM-Endpunkt ist der letzte Meilenstein von Phase 2. Die Weiche dafür steht.
- Vor #029 wäre ein unabhängiger Review angebracht: **sieben Meilensteine** dieser Phase liegen ohne
fremden Blick auf `main`, und diese Sitzung hat wieder einen Fehler gezeigt, den die Suite nur
deshalb gefunden hat, weil der Doppelgänger streng genug war.