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>
8.3 KiB
SCIM-Endpunkt betreiben
Laufzettel kann Personen aus einem Verzeichnis holen (LDAP, Microsoft Graph) oder sich beliefern lassen. Der zweite Weg ist SCIM 2.0, und er ist die einzige Stelle, an der ein fremder Dienst von außen schreibend auf Laufzettel zugreift. Diese Seite beschreibt, was der Betreiber dafür wissen muss. Wie eine Quelle eingerichtet wird, steht in Kapitel 06 der Anleitung.
Was der Endpunkt ist
| Adresse | https://<APP_URL>/api/scim/v2 |
| Anmeldung | Authorization: Bearer <Token>, ein Token je Quelle |
| Medientyp | application/scim+json |
| Normen | RFC 7643 (Schema), RFC 7644 (Protokoll) |
Vollständig:
GET /api/scim/v2/ServiceProviderConfig
GET /api/scim/v2/Schemas
GET /api/scim/v2/ResourceTypes
GET /api/scim/v2/Users?filter=userName eq "…"
POST /api/scim/v2/Users
GET /api/scim/v2/Users/{id}
PUT /api/scim/v2/Users/{id}
PATCH /api/scim/v2/Users/{id}
DELETE /api/scim/v2/Users/{id}
Die drei Beschreibungs-Endpunkte sind nicht Zierde: manche Anbieter, Entra ID darunter, fragen sie beim „Test Connection“ ab und weigern sich sonst anzufangen. Sie verlangen ebenfalls ein Token.
Was nicht unterstützt wird — und warum das so gemeldet wird
ServiceProviderConfig sagt ausdrücklich supported: false für bulk, sort, filter und
etag. Die Verneinungen sind der wichtigere Teil der Antwort: ein Anbieter, der bulk für
unterstützt hält, schickt Anfragen, die niemand beantwortet, und meldet das als Fehler der
Gegenseite.
Nicht umgesetzt sind außerdem:
/Groups. Laufzettel führt keine Gruppen. Rollen entstehen beim Anmelden aus den OIDC-Gruppen (sieheauthentik.md), nicht aus dem Provisioning. Ein Anbieter, der Gruppen zuweisen will, bekommt nichts zugewiesen —ResourceTypesnennt nurUser.- Allgemeine Filter. Unterstützt wird genau
userName eq "…"— die Frage, die ein Anbieter vor jedem Anlegen stellt. Alles andere ergibt400mitscimType: invalidFilter. Das ist Absicht: einen unverstandenen Filter zu ignorieren und alle Personen zu liefern wäre die gefährlichere Antwort, denn der Anbieter schlösse daraus, die gesuchte Person gäbe es schon, und legte sie nie an. - Paginierung über 200 Einträge. Ein
GET /Usersohne Filter liefert höchstens 200 Zeilen. Die Anbieter fragen gefiltert; ein voller Abzug ist keine Betriebsart dieses Endpunkts.
Das Token
Ein Token je Quelle, erzeugt unter Admin → Verzeichnis, Form lzscim_ + 43 Zeichen aus
randomBytes(32).
- Es steht in der Datenbank nur als argon2-Hash. Ein CHECK hält das fest
(
Verzeichnisquelle_scimtoken_gehasht): ein roh gespeichertes Token weist die Datenbank ab, auch wenn jemand am Dienst vorbei schreibt. - Es wird einmal angezeigt, direkt nach dem Erzeugen. Danach gibt es keinen Weg, es zu lesen — auch nicht für einen Administrator, auch nicht aus einem Backup der Anwendung.
- Es erscheint in keinem Protokolleintrag. Vermerkt wird „SCIM-Token erzeugt“ mit dem Namen der Quelle, nicht das Token; das Protokoll ist in der Anwendung ansehbar.
- Ein neues ersetzt das alte. Es gibt genau eines je Quelle — zwei gültige wären zwei Dinge, die man widerrufen muss, und eines davon vergisst man.
Das Token ist der einzige Mandantenbeweis, den eine Zustellung mitbringt. Aus ihm folgt die
Quelle, aus der Quelle der Kunde, und daran hängt, welche Personen die Anfrage sehen und ändern
darf. Ein Token einer anderen Quelle bekommt auf deren Personen 404 und nicht 403: ob es eine
Person gibt, ist für eine fremde Quelle keine Auskunft.
Gleiche Antwort für kein Token und falsches Token: 401 mit
WWW-Authenticate: Bearer realm="Laufzettel SCIM". Die Unterscheidung wäre eine Auskunft darüber,
ob ein geratenes Token die richtige Form hat.
Widerrufen geht jederzeit über Token widerrufen. Eine Quelle stilllegen, ohne das Token beim
Anbieter zu entfernen, geht über das Häkchen Zustellungen annehmen — der Endpunkt antwortet dann
ebenfalls mit 401.
Reverse Proxy
Der Pfad /api/scim/ braucht keine Sonderbehandlung, aber er muss durchgelassen werden. Wer vor
Laufzettel eine Regel hat, die /api/ einschränkt oder nur bekannte Pfade weiterleitet, trägt ihn
ein. Drei Punkte:
- Kein Cache. Der Endpunkt setzt
Cache-Control: no-storeauf jede Antwort; ein Proxy, der das überschreibt, liefert veraltete Personenlisten. - Der
Authorization-Kopf muss ankommen. Manche Konfigurationen entfernen ihn, wenn davor eine eigene Authentifizierung sitzt. Dann kommt jede Zustellung als401zurück. - Eine Zustellung ist klein, einige Kilobyte. Die Grenzen für Anhänge
(
client_max_body_size, siehereverse-proxy.md) reichen weit aus.
Eine IP-Einschränkung auf die Adressen des Anbieters ist möglich und sinnvoll, wenn er feste hat. authentik im eigenen Haus hat welche; Entra ID nicht zuverlässig — Microsoft veröffentlicht die Bereiche, aber sie ändern sich, und eine veraltete Liste sperrt das Provisioning aus.
Was im Betrieb auffällt
Es wird nicht abgeglichen. Eine SCIM-Quelle taucht im Zeitplan nicht auf. Das ist wichtig und nicht nur Kosmetik: ein Abgleich, der an einer SCIM-Quelle nichts liest und „Erfolg“ meldet, würde über die Abschaltregel die ganze Belegschaft deaktivieren. Der Zeitplan lässt sie deshalb aus, und „Jetzt abgleichen“ bricht mit einer Erklärung ab.
DELETE deaktiviert. Der Anbieter bekommt 204, die Zeile bleibt mit aktiv = false. An einer
Person hängen Läufe und Geräte; ein archivierter Lauf muss auch in zehn Jahren noch sagen können,
für wen er lief. Das steht auch in der Anleitung, weil es sonst wie ein Fehler wirkt.
Protokolleinträge haben keinen Benutzer. Jede Zustellung schreibt einen Protokoll-Eintrag mit
der Quelle als Urheber („Person angelegt (SCIM: authentik)“) und ohne benutzerId. Das ist ein
Systemlauf wie der Zeitplan; ein erfundener Administrator wäre eine Lüge im Protokoll.
„Letzte Zustellung“ ist die Lebenszeichenanzeige. Sie steht an der Quelle und wird bei jeder Anfrage gesetzt, die etwas geschrieben hat. Steht dort nichts, ist die Einrichtung beim Anbieter nicht fertig — nicht die Verbindung kaputt, denn es gibt keine.
Prüfen von Hand
Mit dem Token aus der Oberfläche, von einem Rechner aus, der die öffentliche Adresse erreicht:
TOKEN='lzscim_…'
BASIS='https://laufzettel.firma.de/api/scim/v2'
# 1. Antwortet der Endpunkt überhaupt, und gilt das Token?
curl -s -H "Authorization: Bearer $TOKEN" "$BASIS/ServiceProviderConfig" | head -c 400
# 2. Ohne Token muss 401 kommen.
curl -s -o /dev/null -w '%{http_code}\n' "$BASIS/Users"
# 3. Die Frage, die jeder Anbieter vor dem Anlegen stellt.
curl -s -H "Authorization: Bearer $TOKEN" \
--get --data-urlencode 'filter=userName eq "mhuber@firma.example"' "$BASIS/Users"
Die zweite Zeile muss 401 ausgeben. Kommt dort 200, läuft etwas vor Laufzettel, das den
Authorization-Kopf selbst setzt — dann ist der Endpunkt offen, und das gehört sofort behoben.
Anlegen von Hand ist zum Prüfen nicht nötig und auch nicht empfehlenswert: die Person stünde danach in der Quelle, und der Anbieter kennt sie nicht. Löschen lässt sie sich nicht, nur deaktivieren.
Fehlerbilder
| Antwort | Ursache |
|---|---|
401 |
Token falsch, widerrufen, oder Zustellungen annehmen nicht angehakt. |
404 auf eine bekannte Person |
Sie gehört zu einer anderen Quelle. Jedes Token sieht nur seine eigene. |
409 uniqueness |
Die Person gibt es in dieser Quelle schon — richtige Antwort auf ein wiederholtes POST, kein Datenverlust. |
400 invalidFilter |
Ein Filter außer userName eq "…". |
400 invalidSyntax |
Kein gültiges JSON, oder ein PATCH ohne Operations. |
500 |
Ein Fehler in Laufzettel. Die Einzelheiten stehen im Protokoll des Dienstes, nicht in der Antwort: eine Datenbankmeldung nennt Host, Port und Benutzernamen. |
docker compose logs -f web beziehungsweise journalctl -u laufzettel -f zeigt die Zeilen, die
hinter einem 500 stehen, und auch die Pfade, die ein PATCH mitgeschickt und Laufzettel übergangen
hat (SCIM-PATCH: übergangene Pfade: …) — die sind kein Fehler, aber die Antwort auf die Frage
„warum steht das Feld nicht drin“.