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>
10 KiB
#029 M16 – SCIM-Endpunkt als Alternative zum Abgleich
Status: erledigt Im Tracker: #49 Meilenstein: v0.2.0 Labels: phase-2, backend Geschätzt: 12 h Tatsächlich: 4 h 05 min
Ziel
Der Abgleich holt heute. Ein Identitätsanbieter kann auch schicken — authentik, Entra ID und Okta sprechen dafür SCIM 2.0. Das ist der Weg für Häuser, die Laufzettel keinen Lesezugriff auf ihr Verzeichnis geben wollen oder aus einem Netz heraus betreiben, das das Verzeichnis nicht erreicht.
Setzt #028 voraus: die Quellenart SCIM braucht die Weiche.
Zu entscheiden
- Ein Token je Quelle, als Hash gespeichert — Empfehlung, dasselbe Verfahren wie beim lokalen Passwort (argon2). Es wird einmal angezeigt und ist danach nicht mehr lesbar. Das Token identifiziert die Quelle und damit den Kunden; es ist der einzige Mandantenbeweis, den ein Push mitbringt.
- Was bei
DELETE /Users/{id}geschieht:aktiv = false, nicht löschen. Das ist die Regel des ganzen Projekts, und ein Identitätsanbieter, der löschen will, bekommt204— aber die Zeile bleibt, weil Läufe an ihr hängen. Das muss in der Anleitung stehen, sonst wirkt es wie ein Fehler.
Aufgaben
- Route Handler unter
src/app/api/scim/v2/:Users(GET mit FilteruserName eq, POST),Users/{id}(GET, PUT, PATCH, DELETE), dazuServiceProviderConfig,Schemas,ResourceTypes— ohne die drei weigern sich manche Anbieter, überhaupt anzufangen - Jeder dieser Handler prüft selbst: das Token, und daraus die Quelle und den Kunden. Die
Hülle unter
(app)liegt hier nicht darüber (CLAUDE.md) - Zuordnung:
externalId→externeId,userName→upn,name.givenName/familyName,emails[primary]→mail,active→aktiv,urn:ietf:params:scim:schemas:extension:enterprise:2.0:Userfürdepartmentundmanager - Antwortform nach RFC 7644:
schemas,id,meta.resourceType,meta.location, bei ListentotalResults/itemsPerPage/startIndex; Fehler alsurn:ietf:params:scim:api:messages:2.0:Error PATCHmitop: add|replace|removeauf den Pfaden, die die Anbieter wirklich schicken — nicht der ganze Standard- Idempotenz: zweimal dasselbe
POSTergibt409, nicht eine zweite Person - Protokolleintrag je Vorgang, mit der Quelle als Urheber (Systemlauf, kein Benutzer)
- Admin → Verzeichnis: Token erzeugen, einmal anzeigen, widerrufen; letzte Zustellung sichtbar
- Anleitung Kapitel 06: Einrichtung in authentik und in Entra ID
Akzeptanzkriterien
- Test gegen aufgezeichnete Anfragen von authentik und Entra ID (Form, nicht erfunden): anlegen, aktualisieren, deaktivieren, wieder aktivieren
- Test: ohne Token
401, mit fremdem Token kein Zugriff auf die Personen einer anderen Quelle - Test:
DELETEsetztaktiv = falseund löscht nichts - Test: zweimal dasselbe
POSTergibt409und keine zweite Zeile - Test: das Token steht nur als Hash in der Datenbank und in keiner Antwort
- Test: eine Quelle der Art
SCIMwird vom Zeitplan nicht abgeglichen — sie bekommt geschickt
Beim Umsetzen dazugekommen
-
Zwei Gegenproben blieben grün, und beide deckten etwas auf. Von acht Gegenproben (Regel brechen, Test muss rot werden) schlugen sechs sofort an. Die zwei stillen waren die wertvollen:
PATCH removewirkte gar nicht — ein echter Fehler, kein Testloch.personAendernhatte einen SchalternurGesetzte, der beiPATCHjedesnullals „nicht mitgeschickt“ las und den alten Wert stehen ließ. Nimmt jemand beim Anbieter die Abteilung aus dem Profil, stand sie in Laufzettel weiter — sichtbar, gepflegt aussehend, falsch. Nötig war der Schalter nie:patchAnwendensetzt auf dem vorhandenen Stand auf und liefert deshalb immer einen vollständigen Zielzustand. Der Unterschied zwischenPUTundPATCHliegt vor der Funktion, nicht in ihr. Schalter entfernt, Test ergänzt, Gegenprobe schlägt jetzt an.- Entra ID schickt
activeals Zeichenkette"True"/"False". Das war richtig umgesetzt, aber nur halb geprüft: der Deaktivierungs-Test lief gegen"False", und mit einem reinenvalue === truekommt dort zufällig das Richtige heraus. Nur das Aktivieren wäre kaputt gewesen — ein Konto, das wieder freigeschaltet wird und in Laufzettel für immer inaktiv bleibt. Der Fehler wäre also nur in einer Richtung sichtbar gewesen. Jetzt stehtENTRA_PATCH_ANmit"True"als eigene aufgezeichnete Form daneben.
-
managerwar im Issue genannt und wäre fast untergegangen. Die Zuordnung nennt die Enterprise-Erweiterung fürdepartmentundmanager; umgesetzt war zunächst nur die Abteilung. Dazugekommen sind:vorgesetzterAusliest alle drei Formen, die die Anbieter schicken ({value},{$ref}und die nackte Zeichenkette — letztere nicht standardkonform, aber authentik schickt sie), die Auflösung über die Laufzettel-Kennung, undmanagerimSchemas-Endpunkt. Das Letzte ist das Entscheidende: ein Anbieter liest dieses Schema und schickt nur, was darin steht. Fehlte es, bliebe der Vorgesetzte dauerhaft leer — ohne Fehlermeldung auf einer der beiden Seiten. -
managerwäre ein Weg über die Quellengrenze gewesen. Eine Kennung aus einer fremden Quelle zeigt auf jemanden, den diese hier nicht sehen darf, und über die Vorgesetzten-Anzeige stünde dessen Name in der Oberfläche. Die Auflösung sucht deshalb nur in der eigenen Quelle; eine Gegenprobe ohne diese Einschränkung ist rot. Dasselbe für den Fall „sein eigener Vorgesetzter“ — die Datenbank weist das ohnehin ab, aber dann wäre die ganze Zuweisung daran gescheitert. -
Ein unbekannter Vorgesetzter lässt die Zuweisung nicht scheitern. Der Anbieter schickt die Leute in beliebiger Reihenfolge. Wer an einer noch unbekannten Kennung scheiterte, ließe die halbe Belegschaft draußen; nachgezogen wird beim nächsten
PATCH. -
Ein unverstandener Filter ergibt
400und nicht „alle Personen“. Das ist die wichtigste Einzelentscheidung dieses Meilensteins. Ihn zu ignorieren hieße, alle zu liefern — und der Anbieter schlösse daraus, die gesuchte Person gäbe es schon, und legte sie nie an. Unterstützt wird genauuserName eq "…", die Frage, die jeder Anbieter vor dem Anlegen stellt. -
Ein Fehler im Nachbarcode aus #028: in
verbindungTestenAktionstand zweimal dieselbe Bedingung (ergebnis.treffer.length === 0), der zweite Zweig war damit unerreichbar. Genau der Zweig war in #028 als Reviewbefund eingebaut worden — „gelesen, aber nichts zuordenbar“ sollte anders klingen als „der Filter findet niemanden“, weil die Ursachen verschiedene sind und den Administrator an verschiedene Stellen schicken. Wirkungslos, seit er geschrieben wurde. Behoben. -
„Noch nie abgeglichen“ ist an einer SCIM-Quelle eine Falschaussage, und „kein Zeitplan“ liest sich wie ein Mangel. Beides ersetzt: die Pille sagt „nimmt Zustellungen an“, und an der Stelle, an der bei LDAP der Verbindungstest sitzt, steht die letzte Zustellung. Verbindungstest und „Jetzt abgleichen“ gibt es dort nicht — ein Knopf, der nur eine Erklärung auswirft, ist kein Knopf. Wer sie trotzdem über den Dienst ruft, bekommt eine Erklärung statt „unbekannter Fehler“.
-
Der Zeitplan lässt SCIM-Quellen aus, und das ist keine Kosmetik. Ein Abgleich, der an einer SCIM-Quelle nichts liest und
ERFOLGmeldet, hätte über die Abschaltregel die ganze Belegschaft deaktiviert — derselbe Fall, der in #016 einen Reviewbefund gekostet hat. Zwei Stellen halten das:faelligeQuellennimmt sie nicht auf, und die Weiche im Quellenleser wirft. -
Das Token in keiner Sicht — geprüft nicht an einem Feld, sondern an der ganzen serialisierten Sicht aus Liste und Detail. Ein später ergänztes Feld fällt damit auch auf. Dazu: nicht im Protokolleintrag, nicht im HTML der Seite nach dem Neuladen (in der Klickprobe am gebauten Stand nachgesehen).
-
Dieselbe
401für „kein Token“ und „falsches Token“. Die Unterscheidung wäre eine Auskunft darüber, ob ein geratenes Token die richtige Form hat. Und vor der Hash-Prüfung steht eine Vorab-Prüfung auf das Präfix: ohne sie würde ein Token mit falschem Präfix gegen jeden Hash geprüft, eines mit richtigem nur bis zum Treffer — ein Zeitorakel. -
Die Beschreibungs-Endpunkte verlangen ebenfalls ein Token, und ihre Verneinungen sind der wichtigere Teil:
bulk,sort,filterundetagstehen ausdrücklich auffalse. Ein Anbieter, derbulkfür unterstützt hält, schickt Anfragen, die niemand beantwortet, und meldet das als Fehler der Gegenseite. -
Die Klickprobe am gebauten Stand fand keinen Bedienfehler — zwanzig Prüfungen von „Art SCIM wählbar“ über „Token angezeigt“, eine echte Zustellung mit genau diesem Token, „Person steht in der Liste“,
DELETEdeaktiviert, Widerruf sperrt, bis „390 px ohne horizontales Scrollen“. Die Probe hat ihre Daten danach selbst wieder entfernt; das ist die Lehre aus #027. -
Ein Test aus #028 war überholt, nicht falsch: er prüfte, dass die Meldung der Weiche „#029“ enthält. Jetzt ist #029 da, und die Antwort bleibt dieselbe — nur ohne Issue-Nummer, denn die Meldung steht auch in der Oberfläche.
Was offen bleibt
- Gegen echte Mandanten ist das nicht gelaufen. Die Anfrageformen in
tests/scim.test.tsstammen aus der Dokumentation von authentik und Entra ID, nicht aus einem Mitschnitt — dieselbe Grenze wie bei #028, und sie ist dort im Kopfkommentar der Testdatei benannt. Stimmt eine Form nicht, stimmen Test und Wirklichkeit nicht. Die Prüflisten für den ersten echten Lauf stehen indocs/betrieb/scim.mdunter „Prüfen von Hand“. - Keine Paginierung über 200 Einträge bei
GET /Usersohne Filter. Die Anbieter fragen gefiltert; ein voller Abzug ist keine Betriebsart dieses Endpunkts. - Kein
/Groups. Rollen entstehen beim Anmelden aus den OIDC-Gruppen, nicht aus dem Provisioning.ResourceTypesnennt deshalb nurUser.