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
@@ -0,0 +1,97 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
|
||||
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
|
||||
-- (Archivsuche), die partiellen eindeutigen Indizes und alle Trigger: es kennt
|
||||
-- sie nicht, weil sie sich im Schema nicht ausdruecken lassen. Diese
|
||||
-- Loeschbefehle sind bewusst gestrichen.
|
||||
--
|
||||
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
|
||||
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Quellenarten und Microsoft-Graph-Quelle (#028).
|
||||
|
||||
CREATE TYPE "QuellenArt" AS ENUM ('LDAP', 'GRAPH', 'SCIM');
|
||||
|
||||
/*
|
||||
* **Die Reihenfolge der drei Schritte ist nicht beliebig.**
|
||||
*
|
||||
* Erst die Spalte mit Vorgabe `LDAP` anlegen, dann die bisherigen
|
||||
* Pflichtfelder lockern, und erst danach den CHECK setzen. Umgekehrt waere der
|
||||
* CHECK fuer die Dauer einer Anweisung gegen Zeilen gerichtet, die ihn noch
|
||||
* nicht erfuellen koennen.
|
||||
*
|
||||
* Die Vorgabe `LDAP` ist zugleich die Nachfuehrung fuer alle bestehenden Zeilen:
|
||||
* vor #028 war jede Quelle implizit LDAP, und `ADD COLUMN ... DEFAULT` schreibt
|
||||
* den Wert in jede vorhandene Zeile. Ein eigenes `UPDATE` braucht es deshalb
|
||||
* nicht — aber die Pruefung unten steht trotzdem da.
|
||||
*/
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD COLUMN "art" "QuellenArt" NOT NULL DEFAULT 'LDAP';
|
||||
|
||||
-- Die LDAP-Felder gelten nur noch fuer LDAP-Quellen.
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ALTER COLUMN "url" DROP NOT NULL,
|
||||
ALTER COLUMN "bindDn" DROP NOT NULL,
|
||||
ALTER COLUMN "bindPasswortVerschluesselt" DROP NOT NULL,
|
||||
ALTER COLUMN "basisDn" DROP NOT NULL,
|
||||
ALTER COLUMN "filter" DROP NOT NULL;
|
||||
|
||||
-- Und die Felder einer Graph-Quelle.
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD COLUMN "tenantId" TEXT,
|
||||
ADD COLUMN "clientId" TEXT,
|
||||
ADD COLUMN "clientSecretVerschluesselt" TEXT;
|
||||
|
||||
/*
|
||||
* **Welche Felder eine Art braucht, haelt die Datenbank fest.**
|
||||
*
|
||||
* Ohne das waere eine Graph-Quelle mit `basisDn` moeglich — eine Zeile, die zwei
|
||||
* Dinge gleichzeitig behauptet, und bei der niemand mehr sagen kann, was gilt.
|
||||
* Der Dienst prueft dasselbe noch einmal, nur mit besserer Meldung.
|
||||
*
|
||||
* `SCIM` fehlt hier mit Absicht: die Art steht im Enum, weil #029 dieselbe
|
||||
* Weiche braucht, hat aber noch keine Felder. Eine Bedingung auf Felder, die es
|
||||
* nicht gibt, waere eine Zeile, die beim naechsten Meilenstein umgeschrieben
|
||||
* werden muss — und der Dienst weist `SCIM` bis dahin ohnehin ab.
|
||||
*/
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD CONSTRAINT "Verzeichnisquelle_felder_je_art"
|
||||
CHECK (
|
||||
("art" = 'LDAP' AND "url" IS NOT NULL AND "bindDn" IS NOT NULL
|
||||
AND "bindPasswortVerschluesselt" IS NOT NULL AND "basisDn" IS NOT NULL
|
||||
AND "filter" IS NOT NULL
|
||||
AND "tenantId" IS NULL AND "clientId" IS NULL AND "clientSecretVerschluesselt" IS NULL)
|
||||
OR
|
||||
("art" = 'GRAPH' AND "tenantId" IS NOT NULL AND "clientId" IS NOT NULL
|
||||
AND "clientSecretVerschluesselt" IS NOT NULL
|
||||
AND "url" IS NULL AND "bindDn" IS NULL AND "bindPasswortVerschluesselt" IS NULL
|
||||
AND "basisDn" IS NULL AND "filter" IS NULL)
|
||||
OR
|
||||
("art" = 'SCIM')
|
||||
);
|
||||
|
||||
-- Das Client-Secret verschluesselt, dieselbe Pruefung wie beim Bind-Passwort.
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD CONSTRAINT "Verzeichnisquelle_clientsecret_verschluesselt"
|
||||
CHECK ("clientSecretVerschluesselt" ~ '^v[0-9]+\.');
|
||||
|
||||
-- Der Zeitplan fragt nach faelligen **aktiven** Quellen; die Art kommt jetzt in
|
||||
-- dieselbe Abfrage, weil das Holen der Eintraege je Art ein anderes ist.
|
||||
CREATE INDEX "Verzeichnisquelle_art_idx" ON "Verzeichnisquelle"("art");
|
||||
|
||||
/*
|
||||
* Gegenprobe zur Nachfuehrung: nach dem `ADD COLUMN ... DEFAULT` darf keine
|
||||
* Zeile ohne Art dastehen. `art` ist NOT NULL, das kann also nicht schiefgehen —
|
||||
* aber eine Migration, die ihre eigene Nachfuehrung behauptet, ohne sie zu
|
||||
* pruefen, ist genau die Art Zusage, die dieses Projekt teuer bezahlt hat.
|
||||
*/
|
||||
DO $$
|
||||
DECLARE
|
||||
ohne_art INTEGER;
|
||||
BEGIN
|
||||
SELECT count(*) INTO ohne_art FROM "Verzeichnisquelle" WHERE "art" IS NULL;
|
||||
IF ohne_art > 0 THEN
|
||||
RAISE EXCEPTION 'Nachfuehrung fehlgeschlagen: % Quellen ohne Art.', ohne_art;
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user