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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0158d0292f
commit
76a61ec771
@@ -16,6 +16,32 @@ Zwei Dinge vorweg:
|
||||
Wer im Verzeichnis verschwindet oder dort deaktiviert wird, wird hier **inaktiv**. Gelöscht wird
|
||||
nie, sonst verlöre die Historie ihre Namen.
|
||||
|
||||
## Zwei Arten von Quelle
|
||||
|
||||
Laufzettel liest die Personen aus einem Verzeichnis. Dafür gibt es zwei Wege, und du wählst ihn beim
|
||||
Anlegen der Quelle:
|
||||
|
||||
| Art | Für wen |
|
||||
|---|---|
|
||||
| **LDAP** | ein lokales Active Directory (oder ein anderes LDAP-Verzeichnis) |
|
||||
| **Microsoft Graph** | Entra ID, also Microsoft 365 — für Häuser ohne lokales AD |
|
||||
|
||||
Beide verhalten sich danach gleich: anlegen, aktualisieren, deaktivieren, **nie löschen**. Was sie
|
||||
unterscheidet, sind die Zugangsdaten und zwei Platzhalter:
|
||||
|
||||
- **`{{ou}}` bleibt bei Graph leer.** Entra ID hat keine OU. Steht an der Abteilung eine Ziel-OU
|
||||
gepflegt, nimmt Laufzettel die; sonst bleibt der Platzhalter stehen.
|
||||
- **`{{sam}}` ist bei Graph leer**, wenn der Mandant kein lokales AD synchronisiert — dort gibt es
|
||||
keinen Anmeldenamen in diesem Sinn.
|
||||
|
||||
**Die Art lässt sich nachträglich nicht ändern.** An ihr hängt, was die dauerhafte Kennung einer
|
||||
Person bedeutet; nach einem Wechsel würde niemand mehr wiedererkannt, und der nächste Abgleich legte
|
||||
alle neu an. Wer wechseln muss, legt eine zweite Quelle an und lässt die erste stehen.
|
||||
|
||||
Was in Entra ID einzurichten ist — App-Registrierung, Berechtigung, Einwilligung — steht in
|
||||
[`docs/betrieb/entra-id.md`](../betrieb/entra-id.md).
|
||||
|
||||
|
||||
## Eine Quelle anlegen
|
||||
|
||||

|
||||
|
||||
@@ -56,6 +56,8 @@ Sitzungen ungültig *und* die verschlüsselt gespeicherten LDAP-Bind-Passwörter
|
||||
- **[Anmeldung über authentik](authentik.md)** — OIDC-Provider, Gruppen auf Rollen, Notfallzugang
|
||||
- **[Sichern und Zurückspielen](sicherung.md)** — auf beiden Wegen, was in die Sicherung gehört und
|
||||
was nicht, und wie du prüfst, dass das Zurückspielen wirklich funktioniert
|
||||
- **[Entra ID als Verzeichnisquelle](entra-id.md)** — App-Registrierung, `User.Read.All`,
|
||||
Administratoreinwilligung, und was anders ist als bei einem lokalen AD
|
||||
- **[Benachrichtigungen](benachrichtigungen.md)** — SMTP einrichten, den Webhook prüfen, die
|
||||
Signatur nachrechnen, und wo man nachsieht, wenn keine Mail mehr ankommt
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# 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
|
||||
+67
-2
@@ -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.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #028 M15 – Quellenarten und Microsoft-Graph-Quelle
|
||||
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Im Tracker:** [#48](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/48)
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** phase-2, backend, frontend
|
||||
**Geschätzt:** 10 h **Tatsächlich:** –
|
||||
**Geschätzt:** 10 h **Tatsächlich:** 4 h 10 min
|
||||
|
||||
## Ziel
|
||||
|
||||
@@ -17,39 +17,94 @@ Dieser Meilenstein legt dazu die Weiche, die auch [#029](029-scim-endpunkt.md) b
|
||||
|
||||
## Zu entscheiden
|
||||
|
||||
- [ ] **Ein Enum `QuellenArt {LDAP GRAPH SCIM}` auf `Verzeichnisquelle`** — Empfehlung. Die Felder
|
||||
- [x] **Ein Enum `QuellenArt {LDAP GRAPH SCIM}` auf `Verzeichnisquelle`** — Empfehlung. Die Felder
|
||||
`url`, `bindDn` und `basisDn` sind dann je Art unterschiedlich belegt; was eine Art braucht,
|
||||
prüft der Service, nicht die Datenbank. Die Alternative wäre eine Tabelle je Art mit
|
||||
gemeinsamer Oberklasse — mehr Struktur, als drei Arten rechtfertigen.
|
||||
- [ ] **Wie `{{ou}}` in Entra ID aussieht.** Es gibt keinen DN und keine OU. Ehrlich ist, den
|
||||
- [x] **Wie `{{ou}}` in Entra ID aussieht.** Es gibt keinen DN und keine OU. Ehrlich ist, den
|
||||
Platzhalter bei einer Graph-Quelle **leer** zu lassen, statt etwas hineinzudeuten — und das
|
||||
in der Anleitung zu sagen. Wer OU-abhängige Befehle fährt, braucht ein lokales AD.
|
||||
|
||||
## Aufgaben
|
||||
|
||||
- [ ] `QuellenArt` als Enum, Migration mit `LDAP` für alle bestehenden Zeilen
|
||||
- [ ] Graph-Zugang: `tenantId`, `clientId`, `clientSecret` (verschlüsselt wie das Bind-Passwort, nie
|
||||
- [x] `QuellenArt` als Enum, Migration mit `LDAP` für alle bestehenden Zeilen
|
||||
- [x] Graph-Zugang: `tenantId`, `clientId`, `clientSecret` (verschlüsselt wie das Bind-Passwort, nie
|
||||
in einer Antwort), Token über Client-Credentials-Flow, Zwischenspeicher bis kurz vor Ablauf
|
||||
- [ ] Lesen über `GET /v1.0/users` mit `$select`, Seitenabruf über `@odata.nextLink`; der
|
||||
- [x] Lesen über `GET /v1.0/users` mit `$select`, Seitenabruf über `@odata.nextLink`; der
|
||||
Vorgesetzte über `$expand=manager($select=id)`
|
||||
- [ ] Attributzuordnung mit Graph-Vorgaben (`givenName`, `surname`, `displayName`, `mail`,
|
||||
- [x] Attributzuordnung mit Graph-Vorgaben (`givenName`, `surname`, `displayName`, `mail`,
|
||||
`userPrincipalName`, `onPremisesSamAccountName`, `department`, `jobTitle`, `accountEnabled`)
|
||||
- [ ] `externeId` ist die Entra-`id` (GUID). Sie ist stabil und übersteht eine Umbenennung
|
||||
- [ ] „Verbindung testen“ wie bei LDAP: die ersten zehn zugeordneten Treffer zeigen
|
||||
- [ ] Der Abgleich läuft durch **denselben** Dienst wie LDAP — nur das Holen der Einträge
|
||||
- [x] `externeId` ist die Entra-`id` (GUID). Sie ist stabil und übersteht eine Umbenennung
|
||||
- [x] „Verbindung testen“ wie bei LDAP: die ersten zehn zugeordneten Treffer zeigen
|
||||
- [x] Der Abgleich läuft durch **denselben** Dienst wie LDAP — nur das Holen der Einträge
|
||||
unterscheidet sich. Sonst driften die Regeln auseinander
|
||||
- [ ] Admin → Verzeichnis zeigt die Art je Quelle und bietet beim Anlegen die Wahl
|
||||
- [ ] Anleitung Kapitel 06 und `docs/betrieb/`: was in Entra ID einzurichten ist
|
||||
- [x] Admin → Verzeichnis zeigt die Art je Quelle und bietet beim Anlegen die Wahl
|
||||
- [x] Anleitung Kapitel 06 und `docs/betrieb/`: was in Entra ID einzurichten ist
|
||||
(App-Registrierung, `User.Read.All`, `Directory.Read.All`, Administratoreinwilligung)
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Test gegen einen **abspielenden HTTP-Doppelgänger** mit aufgezeichneten Graph-Antworten: erster
|
||||
- [x] Test gegen einen **abspielenden HTTP-Doppelgänger** mit aufgezeichneten Graph-Antworten: erster
|
||||
Abgleich legt an, zweiter aktualisiert ohne Dubletten, ein `accountEnabled: false` setzt
|
||||
`aktiv = false`
|
||||
- [ ] Test: der Seitenabruf folgt `@odata.nextLink` über mindestens drei Seiten
|
||||
- [ ] Test: `clientSecret` erscheint in keiner Antwort und in keinem Protokolleintrag
|
||||
- [ ] Test: eine Graph-Quelle lässt `{{ou}}` leer, statt einen Wert zu erfinden
|
||||
- [ ] Der LDAP-Weg verhält sich unverändert — alle Tests aus #016 bleiben grün
|
||||
- [ ] **Offen und benannt:** gegen einen echten Entra-Mandanten ist das nicht gelaufen; die
|
||||
- [x] Test: der Seitenabruf folgt `@odata.nextLink` über mindestens drei Seiten
|
||||
- [x] Test: `clientSecret` erscheint in keiner Antwort und in keinem Protokolleintrag
|
||||
- [x] Test: eine Graph-Quelle lässt `{{ou}}` leer, statt einen Wert zu erfinden
|
||||
- [x] Der LDAP-Weg verhält sich unverändert — alle Tests aus #016 bleiben grün
|
||||
- [x] **Offen und benannt:** gegen einen echten Entra-Mandanten ist das nicht gelaufen; die
|
||||
Gegenprobe gehört in eine Prüfliste wie bei [#024](024-proxmox-echtlauf.md)
|
||||
|
||||
## Beim Umsetzen dazugekommen
|
||||
|
||||
- **„Deaktiviert in Entra ID“ als *übersprungen* zu melden war falsch — und hätte Leute aktiv
|
||||
gelassen.** „Übersprungen“ bedeutet im Abgleich „nicht zuordenbar“, und solche Einträge werden
|
||||
seit #016 absichtlich **nicht** abgeschaltet (ein Konto ohne GUID soll nicht die Person
|
||||
deaktivieren). Ein gesperrtes Entra-Konto wäre damit genau umgekehrt behandelt worden als im AD.
|
||||
Gefiltert wird jetzt serverseitig mit `$filter=accountEnabled eq true`, als Gegenstück zum
|
||||
LDAP-Standardfilter. Gefunden hat es der Test „setzt aktiv = false“ — und nur, weil der
|
||||
Doppelgänger den Filter wirklich auswertet statt ihn bloß zu empfangen.
|
||||
- **`URLSearchParams` kodiert `$` zu `%24`.** Die Abfrage hätte `%24select` und `%28` in
|
||||
`manager($select=id)` geschickt. Graph nimmt das teils an und antwortet teils mit „Invalid expand
|
||||
clause“ — und eine Adresse, die in einem Mitschnitt anders aussieht als in der Dokumentation,
|
||||
kostet bei der Fehlersuche eine Stunde. Die Parameter werden jetzt von Hand zusammengesetzt.
|
||||
- **Die Art einer Quelle ist unveränderlich**, und das steht nicht im Issue. An ihr hängt, was
|
||||
`externeId` bedeutet: im AD eine `objectGUID`, in Entra ID eine andere GUID. Nach einem Wechsel
|
||||
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. Der Dienst weist einen Wechsel ab, die Oberfläche bietet ihn nicht an, und die Anleitung
|
||||
sagt, was stattdessen zu tun ist.
|
||||
- **`zuordnungLesen` brauchte die Art als Parameter.** Ohne sie bekäme eine Graph-Quelle mit leerer
|
||||
Zuordnung die AD-Namen und fragte bei Graph nach `sn` und `objectGUID`, die es dort nicht gibt.
|
||||
Der Standardwert `LDAP` hält jeden Aufrufer von vor #028 bei seinem Verhalten.
|
||||
- **Die Zugangs-Helfer mussten in ein eigenes Modul.** `quellenleser.ts` (die Weiche) braucht
|
||||
`zugangAus`, und `services/verzeichnis.ts` braucht die Weiche für „Verbindung testen“ — zusammen
|
||||
ein Import-Zyklus. Der läuft in ESM oft, aber nicht zuverlässig, und ein Modul, das beim Laden
|
||||
halb leer ist, ergibt Fehler, die niemand liest. Jetzt liegt beides in
|
||||
`src/server/verzeichnis/zugang.ts`.
|
||||
- **`SCIM` steht im Enum, wird aber abgewiesen.** Die Weiche braucht #029, und ein Wert, der
|
||||
stillschweigend nichts tut, wäre hier besonders teuer: ein Abgleich, der null Einträge liest und
|
||||
`ERFOLG` meldet, hätte über die Abschaltregel die ganze Belegschaft deaktiviert — genau der Fall,
|
||||
der in #016 einen Reviewbefund gekostet hat. Mit eigenem Test.
|
||||
- **Die Felder je Art hält die Datenbank fest.** Ein CHECK verlangt bei `LDAP` die LDAP-Felder
|
||||
**und** verbietet die Graph-Felder, bei `GRAPH` umgekehrt. Ohne den zweiten Teil wäre eine
|
||||
Graph-Quelle mit `basisDn` möglich — eine Zeile, die zwei Dinge gleichzeitig behauptet.
|
||||
- **Der Vorgesetzte kommt bei Graph direkt**, nicht über eine DN-Auflösung. `RoherEintrag` hat dafür
|
||||
ein zweites Feld (`vorgesetzterDirekt`) und nicht ein Feld mit zwei Bedeutungen: ein DN und eine
|
||||
GUID sehen verschieden aus, aber ein Feld, das mal das eine und mal das andere trägt, verwechselt
|
||||
irgendwann jemand.
|
||||
- **Der Token-Zwischenspeicher hat 60 Sekunden Sicherheitsabstand.** Ein Token, das in zehn Sekunden
|
||||
abläuft, ist für eine Seite, die zwei Sekunden braucht, formal noch gültig — und in der Mitte der
|
||||
nächsten nicht mehr. Der Fehler wäre ein Abgleich, der nach der dritten Seite mit 401 abbricht.
|
||||
- **Eine Reißleine bei 1000 Seiten.** Ein `@odata.nextLink`, der auf sich selbst zeigt, wäre sonst
|
||||
eine Schleife bis zum Speicherende — still, weil jeder einzelne Abruf gelingt.
|
||||
- **Zwei eigene Testfehler**, keine Produktfehler: die Token-Grenze liegt bei +3540 s und nicht bei
|
||||
„so ungefähr einer Stunde“, und die Klickprobe zählte die Felder der Bearbeiten-Formulare der
|
||||
bestehenden Quellen mit.
|
||||
|
||||
## Noch offen, und benannt
|
||||
|
||||
Gegen einen **echten Entra-Mandanten** ist das nicht gelaufen. Geprüft ist es gegen einen
|
||||
HTTP-Doppelgänger mit aufgezeichneten Antworten, und deren Form stammt aus der Dokumentation, nicht
|
||||
aus einem Mitschnitt. Die Prüfliste steht am Ende von
|
||||
[`docs/betrieb/entra-id.md`](../betrieb/entra-id.md) — acht Punkte, darunter ein Mandant mit mehr als
|
||||
999 Benutzern für den Seitenabruf. Dieselbe Art offener Punkt wie [#024](024-proxmox-echtlauf.md).
|
||||
|
||||
@@ -32,7 +32,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
||||
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
|
||||
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
||||
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – |
|
||||
| #028 | [M15 – Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | offen | v0.2.0 | 10 h | – |
|
||||
| #028 | [M15 – Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | erledigt | v0.2.0 | 10 h | 4 h 10 min |
|
||||
| #029 | [M16 – SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | offen | v0.2.0 | 12 h | – |
|
||||
| #030 | [M17 – Wiederverwendbare Bausteine](030-bausteine.md) | erledigt | v0.2.0 | 12 h | 2 h 25 min |
|
||||
| #031 | [M18 – Abteilungsspezifische Zusatzabschnitte](031-abteilungsabschnitte.md) | erledigt | v0.2.0 | 8 h | 1 h 35 min |
|
||||
@@ -40,4 +40,4 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
||||
| #033 | [M20 – Live-Aktualisierung per SSE statt Polling](033-sse-live.md) | erledigt | v0.2.0 | 6 h | 1 h 40 min |
|
||||
| #034 | [M21 – Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | erledigt | v0.2.0 | 10 h | 3 h 30 min |
|
||||
| #035 | [M22 – Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | erledigt | v0.2.0 | 8 h | 2 h 50 min |
|
||||
| | **Summe** | | | **229 h** | **56 h 25 min** |
|
||||
| | **Summe** | | | **229 h** | **60 h 35 min** |
|
||||
|
||||
Reference in New Issue
Block a user