feat(verzeichnis): SCIM-Endpunkt als dritte Quellenart (#029)
Bisher holt Laufzettel Personen aus dem Verzeichnis. Das setzt Lesezugriff und einen Netzweg dorthin voraus, und beides ist nicht ueberall gegeben. Mit der Art SCIM ruft stattdessen der Anbieter an — sofort, nicht beim naechsten Intervall. Umgesetzt ist, was authentik, Entra ID und Okta wirklich schicken: Users mit dem Filter userName eq, POST/GET/PUT/PATCH/DELETE, dazu ServiceProviderConfig, Schemas und ResourceTypes. Jeder Handler prueft sein Token selbst. Ein Token je Quelle, argon2-gehasht, einmal angezeigt — es ist der einzige Mandantenbeweis, den eine Zustellung mitbringt. DELETE deaktiviert und loescht nichts. Eine SCIM-Quelle wird nicht abgeglichen, an zwei Stellen gehalten: ein Abgleich, der dort nichts liest und ERFOLG meldet, haette ueber die Abschaltregel die ganze Belegschaft deaktiviert. Zwei Befunde kamen aus Gegenproben, nicht aus Tests: - PATCH remove wirkte gar nicht. Der Schalter nurGesetzte las bei PATCH jedes null als "nicht mitgeschickt"; eine beim Anbieter entfernte Abteilung stand hier weiter. Noetig war er nie — patchAnwenden liefert immer den vollstaendigen Zielzustand. - Entra ID schickt active als "True"/"False". Beim Deaktivieren kommt mit einem reinen value === true zufaellig das Richtige heraus, beim Aktivieren nicht: ein wieder freigeschaltetes Konto waere fuer immer inaktiv geblieben. Dazu ein Fehler aus #028: in verbindungTestenAktion stand zweimal dieselbe Bedingung, der zweite Zweig war seit dem ersten Tag unerreichbar. 1432 Tests (vorher 1373). Klickprobe am gebauten Stand mit einer echten Zustellung. Gegen echte Mandanten ist nichts gelaufen — die Anfrageformen stammen aus der Dokumentation, und das steht so in der Testdatei. Closes #49 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
76a61ec771
commit
fcd67018b8
@@ -0,0 +1,95 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- SCIM-Endpunkt als Alternative zum Abgleich (#029).
|
||||
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD COLUMN "scimTokenHash" TEXT,
|
||||
ADD COLUMN "scimTokenGesetztAm" TIMESTAMP(3),
|
||||
ADD COLUMN "letzteScimZustellungAm" TIMESTAMP(3);
|
||||
|
||||
/*
|
||||
* **Der CHECK je Art wird ersetzt, nicht ergaenzt.**
|
||||
*
|
||||
* Bis #028 liess er `SCIM` ohne jede Bedingung durch — die Art hatte noch keine
|
||||
* Felder. Jetzt hat sie welche, und dieselbe Regel gilt in beide Richtungen: was
|
||||
* eine Art braucht, und was sie **nicht** haben darf. Ohne den zweiten Teil
|
||||
* waere eine SCIM-Quelle mit `basisDn` moeglich, und bei der koennte niemand
|
||||
* mehr sagen, was gilt.
|
||||
*
|
||||
* **Das Token steht nicht in der Bedingung.** Eine SCIM-Quelle entsteht ohne
|
||||
* Token und bekommt es danach — der Administrator legt die Quelle an, erzeugt
|
||||
* das Token und traegt es beim Anbieter ein. Eine Quelle ohne Token nimmt
|
||||
* nichts an: der Handler findet keinen Hash zum Vergleichen und antwortet mit
|
||||
* 401. Das ist der richtige Zustand, nicht ein verbotener.
|
||||
*/
|
||||
ALTER TABLE "Verzeichnisquelle" DROP CONSTRAINT "Verzeichnisquelle_felder_je_art";
|
||||
|
||||
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
|
||||
AND "scimTokenHash" 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
|
||||
AND "scimTokenHash" IS NULL)
|
||||
OR
|
||||
("art" = 'SCIM'
|
||||
AND "url" IS NULL AND "bindDn" IS NULL AND "bindPasswortVerschluesselt" IS NULL
|
||||
AND "basisDn" IS NULL AND "filter" IS NULL
|
||||
AND "tenantId" IS NULL AND "clientId" IS NULL AND "clientSecretVerschluesselt" IS NULL)
|
||||
);
|
||||
|
||||
-- Das Token als argon2-Hash, nicht im Klartext. `$argon2` ist das Praefix der
|
||||
-- Bibliothek; ein Token, das versehentlich roh gespeichert wird, faellt hier auf.
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD CONSTRAINT "Verzeichnisquelle_scimtoken_gehasht"
|
||||
CHECK ("scimTokenHash" IS NULL OR "scimTokenHash" LIKE '$argon2%');
|
||||
|
||||
-- Ein Token gehoert zu einem Zeitpunkt, in beide Richtungen: ohne ihn liesse
|
||||
-- sich nicht sagen, seit wann es gilt, und ein Zeitpunkt ohne Token behauptete
|
||||
-- ein Token, das es nicht gibt.
|
||||
ALTER TABLE "Verzeichnisquelle"
|
||||
ADD CONSTRAINT "Verzeichnisquelle_scimtoken_mit_zeitpunkt"
|
||||
CHECK (("scimTokenHash" IS NULL) = ("scimTokenGesetztAm" IS NULL));
|
||||
|
||||
/*
|
||||
* Der Index, auf dem der SCIM-Handler arbeitet.
|
||||
*
|
||||
* Jede Anfrage bringt ein Token mit und muss daraus die Quelle finden. Ein Hash
|
||||
* laesst sich nicht nachschlagen — argon2 ist je Aufruf anders —, also werden
|
||||
* die SCIM-Quellen gelesen und einzeln geprueft. Bei zwei Quellen ist das
|
||||
* gleichgueltig; der Index haelt es auch bei zwanzig schnell und sagt zugleich,
|
||||
* dass hier nach `art` gesucht wird.
|
||||
*/
|
||||
CREATE INDEX "Verzeichnisquelle_art_aktiv_idx" ON "Verzeichnisquelle"("art", "aktiv");
|
||||
|
||||
/*
|
||||
* Gegenprobe: der ersetzte CHECK muss die bestehenden Zeilen noch zulassen.
|
||||
* `ALTER TABLE ... ADD CONSTRAINT` prueft das von sich aus — aber eine
|
||||
* Migration, die ihre eigene Annahme nicht nachrechnet, ist genau die Art
|
||||
* Zusage, die dieses Projekt teuer bezahlt hat.
|
||||
*/
|
||||
DO $$
|
||||
DECLARE
|
||||
anzahl INTEGER;
|
||||
BEGIN
|
||||
SELECT count(*) INTO anzahl FROM "Verzeichnisquelle" WHERE "art" NOT IN ('LDAP', 'GRAPH', 'SCIM');
|
||||
IF anzahl > 0 THEN
|
||||
RAISE EXCEPTION 'Unerwartete Quellenart in % Zeilen.', anzahl;
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user