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
+42
-2
@@ -60,6 +60,40 @@ export const AD_ZUORDNUNG: AttributZuordnung = {
|
||||
externeId: "objectGUID",
|
||||
};
|
||||
|
||||
/**
|
||||
* Dieselben Felder mit den Namen, die **Microsoft Graph** benutzt (#028).
|
||||
*
|
||||
* Vier Unterschiede zum AD, und jeder hat einen Grund:
|
||||
*
|
||||
* - `surname` statt `sn` — Graph schreibt die Namen aus.
|
||||
* - `jobTitle` statt `title` — `title` gibt es bei Graph auch, es ist aber die
|
||||
* Anrede („Dr.“), nicht die Funktion.
|
||||
* - `onPremisesSamAccountName` statt `sAMAccountName` — der Wert kommt aus dem
|
||||
* lokalen AD und ist **leer**, wenn der Mandant kein AD synchronisiert. Eine
|
||||
* reine Cloud-Umgebung hat keinen Anmeldenamen in diesem Sinn; `{{sam}}`
|
||||
* bleibt dort leer, statt dass etwas hineingedeutet wird.
|
||||
* - `id` statt `objectGUID` — dieselbe GUID, nur schon als Text. Das Umsortieren
|
||||
* der Bytes (`src/lib/ldap-guid.ts`) braucht es hier nicht.
|
||||
*/
|
||||
export const GRAPH_ZUORDNUNG: AttributZuordnung = {
|
||||
vorname: "givenName",
|
||||
nachname: "surname",
|
||||
anzeigename: "displayName",
|
||||
mail: "mail",
|
||||
upn: "userPrincipalName",
|
||||
sam: "onPremisesSamAccountName",
|
||||
abteilung: "department",
|
||||
titel: "jobTitle",
|
||||
vorgesetzter: "manager",
|
||||
externeId: "id",
|
||||
};
|
||||
|
||||
/** Die Vorgaben je Quellenart. `SCIM` hat noch keine (#029). */
|
||||
export const ZUORDNUNGS_VORGABE: Record<"LDAP" | "GRAPH", AttributZuordnung> = {
|
||||
LDAP: AD_ZUORDNUNG,
|
||||
GRAPH: GRAPH_ZUORDNUNG,
|
||||
};
|
||||
|
||||
/** Die Felder in der Reihenfolge, in der das Formular sie zeigt. */
|
||||
export const ZUORDNUNGS_FELDER = Object.keys(AD_ZUORDNUNG) as (keyof AttributZuordnung)[];
|
||||
|
||||
@@ -92,9 +126,15 @@ export const ATTRIBUT_MUSTER = /^[A-Za-z][A-Za-z0-9-]*$/u;
|
||||
* mitschreiben muessen, und eine von Hand geleerte Zeile darf kein Attribut mit
|
||||
* leerem Namen anfragen.
|
||||
*/
|
||||
export function zuordnungLesen(roh: unknown): AttributZuordnung {
|
||||
export function zuordnungLesen(roh: unknown, art: "LDAP" | "GRAPH" = "LDAP"): AttributZuordnung {
|
||||
const felder = (typeof roh === "object" && roh !== null ? roh : {}) as Record<string, unknown>;
|
||||
const gelesen = { ...AD_ZUORDNUNG };
|
||||
/*
|
||||
* Die Vorgabe haengt an der **Art** (#028). Ohne diesen Parameter bekaeme eine
|
||||
* Graph-Quelle mit leerer Zuordnung die AD-Namen — und fragte dann bei Graph
|
||||
* nach `sn` und `objectGUID`, die es dort nicht gibt. Der Standardwert `LDAP`
|
||||
* haelt jeden Aufrufer von vor #028 bei seinem bisherigen Verhalten.
|
||||
*/
|
||||
const gelesen = { ...ZUORDNUNGS_VORGABE[art] };
|
||||
for (const schluessel of ZUORDNUNGS_FELDER) {
|
||||
const wert = felder[schluessel];
|
||||
if (typeof wert === "string" && wert.trim()) gelesen[schluessel] = wert.trim();
|
||||
|
||||
Reference in New Issue
Block a user