feat(verzeichnis): SCIM-Endpunkt als dritte Quellenart (#029)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

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:
Florian Wilde
2026-10-10 19:50:54 +02:00
co-authored by Claude Opus 5
parent 76a61ec771
commit fcd67018b8
32 changed files with 3672 additions and 141 deletions
+98 -2
View File
@@ -35,12 +35,13 @@ als Unterpunkte mit Aufgabe und Dauer.
| #034 | M21 – Benachrichtigungen per Mail und Webhook | 3 h 30 min |
| #035 | M22 – Mitarbeiter-Selbstauskunft | 2 h 50 min |
| #028 | M15 – Quellenarten und Microsoft-Graph-Quelle | 4 h 10 min |
| | **Summe** | **60 h 35 min** |
| #029 | M16 – SCIM-Endpunkt als Alternative zum Abgleich | 4 h 05 min |
| | **Summe** | **64 h 40 min** |
| Meilenstein | Tatsächlich | Status |
|---|---|---|
| v0.1.0 | 35 h 20 min | veröffentlicht |
| v0.2.0 | 25 h 15 min | in Arbeit |
| v0.2.0 | 29 h 20 min | in Arbeit |
---
@@ -1435,3 +1436,98 @@ Punkte, darunter ein Mandant mit mehr als 999 Benutzern. Dieselbe Art offener Pu
- Vor #029 wäre ein unabhängiger Review angebracht: **sieben Meilensteine** dieser Phase liegen ohne
fremden Blick auf `main`, und diese Sitzung hat wieder einen Fehler gezeigt, den die Suite nur
deshalb gefunden hat, weil der Doppelgänger streng genug war.
---
## 2026-10-10 · Sitzung 22 · 17:40–21:45
**Issue:** #029 (M16)
**Version:** 0.2.0 (unveröffentlicht)
### Erledigt
- **SCIM 2.0 als dritte Quellenart.** Neun Endpunkte unter `/api/scim/v2`, jeder prüft sein Token
selbst — die Hülle unter `(app)` liegt dort nicht darüber, und der Proxy lässt `/api/scim`
absichtlich ohne Cookie durch.
- **Ein Token je Quelle**, argon2-gehasht, einmal angezeigt. Zwei CHECKs halten das in der
Datenbank: `…_scimtoken_gehasht` (`LIKE '$argon2%'`) und `…_scimtoken_mit_zeitpunkt`.
- **Oberfläche**: Art SCIM im Anlege-Formular, Token erzeugen und widerrufen, letzte Zustellung,
Endpunktadresse an der Quelle. Verbindungstest, Zeitplan, Abgleich-Log und Attributzuordnung
fallen dort weg — es gibt sie nicht, und ein Knopf, der nur eine Erklärung auswirft, ist kein
Knopf.
- 1432 Vitest-Tests (vorher 1373), davon 59 neue. Die Anfrageformen von authentik und Entra ID
stehen als benannte Konstanten oben in `tests/scim.test.ts`.
- **Klickprobe am gebauten Stand, zwanzig Prüfungen**, von „Art SCIM wählbar“ über eine echte
Zustellung mit genau dem Token aus der Oberfläche bis „390 px ohne horizontales Scrollen“. Die
Probe hat ihre Daten danach selbst entfernt — die Lehre aus #027.
- Anleitung Kapitel 06 um den SCIM-Abschnitt erweitert (Einrichtung in authentik und Entra ID,
Fehlerbilder), `docs/betrieb/scim.md` neu.
### Entscheidungen
- **Umgesetzt ist nicht der Standard, sondern was die Anbieter schicken.** Komplexe Filter,
Sortierung, `bulk`, `/Groups` — nichts davon. `ServiceProviderConfig` **meldet** das als
`supported: false`, und die Verneinungen sind der wichtigere Teil der Antwort: ein Anbieter, der
`bulk` für unterstützt hält, schickt Anfragen, die niemand beantwortet.
- **Ein unverstandener Filter ergibt `400`, nicht „alle Personen“.** Die wichtigste
Einzelentscheidung hier. Alles zu liefern wäre die gefährlichere Antwort: der Anbieter schlösse
daraus, die gesuchte Person gäbe es schon, und legte sie **nie** an.
- **`404` und nicht `403`** für eine Person einer fremden Quelle. Ob es sie gibt, ist für ein
fremdes Token keine Auskunft. Ebenso dieselbe `401` für „kein Token“ und „falsches Token“.
- **`DELETE` deaktiviert.** Der Anbieter bekommt `204` und hält die Person für entfernt — aus seiner
Sicht richtig. In der Anleitung steht das ausdrücklich, sonst wirkt die inaktive Zeile wie ein
Fehler.
- **SCIM-Quellen bleiben aus dem Zeitplan heraus**, an zwei Stellen gehalten. Ein Abgleich, der dort
nichts liest und `ERFOLG` meldet, hätte die ganze Belegschaft deaktiviert.
### Probleme
- **Zwei von acht Gegenproben blieben grün — und das waren die wertvollen.** Sechs schlugen sofort
an. Die beiden stillen:
- **`PATCH remove` wirkte gar nicht.** Kein Testloch, ein Fehler: `personAendern` hatte einen
Schalter `nurGesetzte`, der bei `PATCH` jedes `null` als „nicht mitgeschickt“ las. Nimmt jemand
beim Anbieter die Abteilung aus dem Profil, stand sie in Laufzettel weiter — sichtbar, gepflegt
aussehend, falsch. Der Schalter war nie nötig: `patchAnwenden` setzt auf dem vorhandenen Stand
auf und liefert immer einen vollständigen Zielzustand. Der Unterschied zwischen `PUT` und
`PATCH` liegt **vor** der Funktion, nicht in ihr.
- **Entra ID schickt `active` als `"True"`/`"False"`.** Richtig umgesetzt, aber nur halb geprüft:
beim Deaktivieren kommt mit einem reinen `value === true` **zufällig** das Richtige heraus. Nur
das Aktivieren wäre kaputt gewesen — ein wieder freigeschaltetes Konto, das in Laufzettel für
immer inaktiv bleibt. Ein Fehler, der nur in einer Richtung sichtbar ist, ist der teuerste.
- **`manager` wäre fast untergegangen.** Es stand im Issue, und umgesetzt war zunächst nur
`department`. Dazugekommen: drei Lesearten (`{value}`, `{$ref}`, nackte Zeichenkette), die
Auflösung, und `manager` im `Schemas`-Endpunkt — das Letzte ist das Entscheidende, denn ein
Anbieter schickt nur, was dort steht. Ohne den Eintrag bliebe der Vorgesetzte dauerhaft leer,
**ohne Fehlermeldung auf einer der beiden Seiten.**
- **`manager` war ein Weg über die Quellengrenze.** Eine Kennung aus einer fremden Quelle hätte über
die Vorgesetzten-Anzeige einen fremden Namen in die Oberfläche gebracht. Die Auflösung sucht nur
in der eigenen Quelle; die Gegenprobe ohne diese Einschränkung ist rot.
- **Ein Fehler aus #028 im Nachbarcode**: in `verbindungTestenAktion` stand zweimal dieselbe
Bedingung, der zweite Zweig war unerreichbar. Genau dieser Zweig war dort als Reviewbefund
eingebaut worden — „gelesen, aber nichts zuordenbar“ sollte anders klingen als „der Filter findet
niemanden“, weil die Ursachen den Administrator an verschiedene Stellen schicken. Wirkungslos
seit dem Tag, an dem er geschrieben wurde. Das ist dasselbe Muster wie die stillen Gegenproben:
**Code, der aussieht, als prüfte er etwas.**
- **Ein eigener Probenfehler**: die Klickprobe suchte „Klick Probe“ in der Mitarbeiterliste. Ohne
`displayName` baut Laufzettel „Nachname, Vorname“ — wie der Abgleich. Kein Produktfehler, aber
ein Beleg dafür, dass ein roter Befund erst gelesen und dann bewertet gehört.
- Ein Test aus #028 war **überholt**, nicht falsch: er verlangte, dass die Meldung der Weiche „#029“
nennt.
### Was offen bleibt, und das ist benannt
Gegen **echte Mandanten** ist nichts davon gelaufen. Die Anfrageformen stammen aus der
Dokumentation von authentik und Entra ID, nicht aus einem Mitschnitt — dieselbe Grenze wie bei #028,
und sie steht im Kopfkommentar von `tests/scim.test.ts`. Die Prüfbefehle für den ersten echten Lauf
stehen in `docs/betrieb/scim.md`.
### Nächster Schritt
- **Phase 2 ist damit vollständig.** Acht Meilensteine (M15–M22) liegen danach auf `main`.
- Offen bleiben **#024** (Proxmox-Einzeiler auf echtem PVE-Host) und **#027** (die Editor-Abläufe
lassen ihre Vorlagen in der Dev-Datenbank stehen).
- **Ein unabhängiger Review ist jetzt überfällig.** Acht Meilensteine dieser Phase liegen ohne
fremden Blick auf `main`, und diese Sitzung hat wieder zwei Dinge gezeigt, die eine grüne Suite
nicht sieht: einen Schalter, der eine Änderung verschluckte, und einen Zweig, der nie erreicht
wurde. Beide Befunde kamen aus Gegenproben, nicht aus Tests.