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
@@ -16,18 +16,24 @@ Zwei Dinge vorweg:
|
||||
Wer im Verzeichnis verschwindet oder dort deaktiviert wird, wird hier **inaktiv**. Gelöscht wird
|
||||
nie, sonst verlöre die Historie ihre Namen.
|
||||
|
||||
## Zwei Arten von Quelle
|
||||
## Drei Arten von Quelle
|
||||
|
||||
Laufzettel liest die Personen aus einem Verzeichnis. Dafür gibt es zwei Wege, und du wählst ihn beim
|
||||
Anlegen der Quelle:
|
||||
Laufzettel braucht die Personen aus einem Verzeichnis. Dafür gibt es drei Wege, und du wählst ihn
|
||||
beim Anlegen der Quelle:
|
||||
|
||||
| Art | Für wen |
|
||||
|---|---|
|
||||
| **LDAP** | ein lokales Active Directory (oder ein anderes LDAP-Verzeichnis) |
|
||||
| **Microsoft Graph** | Entra ID, also Microsoft 365 — für Häuser ohne lokales AD |
|
||||
| Art | Für wen | Richtung |
|
||||
|---|---|---|
|
||||
| **LDAP** | ein lokales Active Directory (oder ein anderes LDAP-Verzeichnis) | Laufzettel holt |
|
||||
| **Microsoft Graph** | Entra ID, also Microsoft 365 — für Häuser ohne lokales AD | Laufzettel holt |
|
||||
| **SCIM** | authentik, Entra ID, Okta — wenn Laufzettel nicht ins Verzeichnis sehen soll oder es nicht erreicht | der Anbieter schickt |
|
||||
|
||||
Beide verhalten sich danach gleich: anlegen, aktualisieren, deaktivieren, **nie löschen**. Was sie
|
||||
unterscheidet, sind die Zugangsdaten und zwei Platzhalter:
|
||||
Die ersten zwei sind dieselbe Arbeitsweise mit anderen Zugangsdaten: Laufzettel verbindet sich nach
|
||||
Zeitplan und liest. **SCIM ist die andere Richtung** — dort gibt es keine Zugangsdaten, keinen
|
||||
Zeitplan und keinen Verbindungstest; der Anbieter schickt jede Änderung in dem Moment, in dem sie
|
||||
dort entsteht. Für SCIM gibt es [einen eigenen Abschnitt](#scim-der-anbieter-schickt) weiter unten.
|
||||
|
||||
Alle drei verhalten sich danach gleich: anlegen, aktualisieren, deaktivieren, **nie löschen**. Was
|
||||
LDAP und Graph unterscheidet, sind die Zugangsdaten und zwei Platzhalter:
|
||||
|
||||
- **`{{ou}}` bleibt bei Graph leer.** Entra ID hat keine OU. Steht an der Abteilung eine Ziel-OU
|
||||
gepflegt, nimmt Laufzettel die; sonst bleibt der Platzhalter stehen.
|
||||
@@ -42,6 +48,109 @@ Was in Entra ID einzurichten ist — App-Registrierung, Berechtigung, Einwilligu
|
||||
[`docs/betrieb/entra-id.md`](../betrieb/entra-id.md).
|
||||
|
||||
|
||||
## SCIM: der Anbieter schickt
|
||||
|
||||
Bei den ersten zwei Arten fragt Laufzettel das Verzeichnis. Bei SCIM ist es umgekehrt: der
|
||||
Identitätsanbieter ruft Laufzettel an, sobald sich bei ihm etwas ändert. Das ist der Weg für zwei
|
||||
Lagen:
|
||||
|
||||
- Das Haus will Laufzettel **keinen Lesezugriff** auf sein Verzeichnis geben.
|
||||
- Laufzettel läuft in einem Netz, das das Verzeichnis **nicht erreicht** — ein Push von außen nach
|
||||
innen geht, ein Lesezugriff von innen nach außen nicht.
|
||||
|
||||
### Was anders ist
|
||||
|
||||
| | LDAP und Graph | SCIM |
|
||||
|---|---|---|
|
||||
| Zugangsdaten | Dienstkonto bzw. App-Registrierung | **keine** — ein Token für den Anbieter |
|
||||
| Zeitplan | alle *n* Minuten | keiner, es kommt sofort |
|
||||
| Verbindung testen | zeigt die ersten zehn Treffer | gibt es nicht — es gibt keine Verbindung |
|
||||
| Abgleich-Log | je Lauf eine Zeile | gibt es nicht; stattdessen **letzte Zustellung** |
|
||||
| Attributzuordnung | einstellbar | fest, der Anbieter bestimmt die Feldnamen (RFC 7643) |
|
||||
| `{{ou}}` | aus dem DN bzw. der Abteilung | leer — SCIM kennt keine OU |
|
||||
|
||||
Was Laufzettel übernimmt: Anmeldename (`userName`), Vor- und Nachname, Anzeigename, Mailadresse,
|
||||
Titel, Abteilung, der Vorgesetzte und ob das Konto aktiv ist. Mehr nicht — keine Gruppen, keine
|
||||
Rollen, keine Passwörter.
|
||||
|
||||
### Einrichten: Laufzettel zuerst
|
||||
|
||||
1. **Quelle anlegen.** Unten auf der Seite **Art → SCIM (wird geliefert)** wählen, einen Namen
|
||||
geben (etwa „authentik Push“), **Zustellungen annehmen** anhaken und speichern. Mehr Felder gibt
|
||||
es nicht.
|
||||
2. **Token erzeugen.** An der neuen Quelle steht **Token erzeugen**. Es erscheint **genau einmal** —
|
||||
kopier es sofort. Gespeichert wird es nur als Hash; ein zweites Ansehen gibt es nicht. Wer es
|
||||
verliert, erzeugt ein neues, und das alte gilt dann nicht mehr.
|
||||
3. **Adresse ablesen.** Unter dem Namen der Quelle steht die Adresse, die der Anbieter braucht, etwa
|
||||
`https://laufzettel.firma.de/api/scim/v2`.
|
||||
|
||||
### Einrichten in authentik
|
||||
|
||||
Unter **Applications → Providers → Create → SCIM Provider**:
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| **Name** | etwa „Laufzettel“ |
|
||||
| **URL** | die Adresse von oben, also `https://…/api/scim/v2` |
|
||||
| **Token** | das erzeugte Token, unverändert eingefügt |
|
||||
|
||||
Bei **User filtering** wählst du die Gruppe, deren Mitglieder in Laufzettel stehen sollen — nicht
|
||||
alle Konten, sondern die Menschen, für die Läufe entstehen. Unter **Attribute mapping** genügen die
|
||||
Vorgaben; authentik schickt `userName`, `name`, `emails` und die Erweiterung
|
||||
`enterprise:2.0:User` von selbst.
|
||||
|
||||
Danach **Save** und einmal **Sync** auslösen. In Laufzettel steht dann an der Quelle **Letzte
|
||||
Zustellung am …** und die Personen stehen unter **Mitarbeiter**.
|
||||
|
||||
### Einrichten in Entra ID
|
||||
|
||||
Unter **Entra ID → Enterprise applications → New application → Create your own application →
|
||||
Integrate any other application you don't find in the gallery**. Dann in der neuen Anwendung
|
||||
**Provisioning → New configuration**:
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| **Tenant URL** | die Adresse von oben, also `https://…/api/scim/v2` |
|
||||
| **Secret Token** | das erzeugte Token |
|
||||
|
||||
**Test Connection** muss grün werden — Entra ID fragt dabei `ServiceProviderConfig`, `Schemas` und
|
||||
`ResourceTypes` ab und legt probeweise niemanden an. Danach unter **Users and groups** die Gruppe
|
||||
zuweisen und das Provisioning auf **On** stellen.
|
||||
|
||||
Zwei Eigenheiten von Entra ID, die hier nicht stören, aber erklären, was in den Protokollen steht:
|
||||
|
||||
- Entra ID schickt `active` als **Zeichenkette** `"True"`/`"False"`, nicht als `true`/`false`.
|
||||
Laufzettel liest beides.
|
||||
- Der erste vollständige Durchlauf beginnt bis zu **40 Minuten** nach dem Einschalten. Das ist
|
||||
Entra ID und nichts, was sich hier beschleunigen ließe.
|
||||
|
||||
### `Löschen` beim Anbieter heißt hier `inaktiv`
|
||||
|
||||
Nimmt jemand eine Person beim Anbieter aus der zugewiesenen Gruppe oder löscht sie dort, schickt
|
||||
der Anbieter ein `DELETE`. **Laufzettel setzt die Person dann auf inaktiv und löscht nichts.** Die
|
||||
Zeile bleibt, weil Läufe und Geräte an ihr hängen: ein archivierter Lauf muss auch in zehn Jahren
|
||||
noch sagen können, für wen er lief.
|
||||
|
||||
Der Anbieter bekommt `204` und hält die Person damit für entfernt — aus seiner Sicht richtig, denn
|
||||
sie wird ihm nicht mehr zugewiesen. **In Laufzettel steht sie weiter in der Liste**, inaktiv. Das
|
||||
sieht aus wie ein Fehler und ist keiner. Wer sie aus der Ansicht haben will, filtert auf „nur
|
||||
aktive“ — das ist die Vorgabe der Mitarbeiterliste.
|
||||
|
||||
### Wenn nichts ankommt
|
||||
|
||||
| Was der Anbieter meldet | Woran es liegt |
|
||||
|---|---|
|
||||
| `401` | Das Token stimmt nicht, oder **Zustellungen annehmen** ist nicht angehakt. Beides ergibt dieselbe Antwort — absichtlich. |
|
||||
| `404` auf eine bekannte Person | Die Person gehört zu einer **anderen** Quelle. Jedes Token sieht nur die Personen seiner eigenen. |
|
||||
| `409` beim Anlegen | Die Person gibt es in dieser Quelle schon. Der Anbieter fragt normalerweise vorher mit einem Filter; schickt er trotzdem, ist `409` die richtige Antwort und kein Datenverlust. |
|
||||
| `400 invalidFilter` | Der Anbieter benutzt einen Filter, den Laufzettel nicht unterstützt — nur `userName eq "…"` geht. |
|
||||
| nichts, Zeitüberschreitung | Der Reverse Proxy lässt `/api/scim/` nicht durch, oder TLS stimmt nicht. Siehe [`docs/betrieb/scim.md`](../betrieb/scim.md). |
|
||||
|
||||
Eine Quelle **stilllegen**, ohne das Token beim Anbieter zu löschen: Häkchen **Zustellungen
|
||||
annehmen** entfernen. Der Endpunkt antwortet dann mit `401`, und beim Anbieter stehen die
|
||||
Zustellungen als Fehler — sie gehen nicht verloren, er versucht es erneut.
|
||||
|
||||
|
||||
## Eine Quelle anlegen
|
||||
|
||||

|
||||
@@ -116,6 +225,10 @@ Namensänderung der Quelle ihre Anmeldung.
|
||||
|
||||

|
||||
|
||||
Die folgenden vier Abschnitte gelten für **LDAP und Graph**. Bei einer SCIM-Quelle gibt es weder
|
||||
Verbindungstest noch Zeitplan noch Abgleich-Log — dort steht an ihrer Stelle die letzte Zustellung,
|
||||
siehe [oben](#scim-der-anbieter-schickt).
|
||||
|
||||
**Verbindung testen** liest die ersten Einträge und zeigt sie so, wie sie ankämen — **gespeichert
|
||||
wird nichts**. Mach das immer vor dem ersten Abgleich. Klappt es nicht, steht der Grund in Rot
|
||||
darunter; die häufigsten stehen in [Kapitel 08](08-fehlerbehebung.md).
|
||||
@@ -195,3 +308,7 @@ deshalb als Hinweis und nicht als Fehler da. Steigt die Zahl, prüf die Attribut
|
||||
Eine Quelle ist keine Voraussetzung. Personen lassen sich von Hand anlegen oder aus einer CSV-Datei
|
||||
einlesen — ein Verzeichnis nimmt nur die Arbeit ab. Siehe
|
||||
[Kapitel 05](05-mitarbeiter-geraete.md).
|
||||
|
||||
Von Hand angelegte Personen gehen dabei nicht verloren, wenn später eine Quelle dazukommt: taucht
|
||||
jemand mit demselben Anmeldenamen aus dem Verzeichnis oder per SCIM auf, **übernimmt** Laufzettel
|
||||
die vorhandene Zeile statt eine zweite anzulegen. Ihre Läufe und Geräte bleiben an ihr hängen.
|
||||
|
||||
@@ -378,6 +378,34 @@ was darauf steht, ist aus der Hand gegeben.
|
||||
|
||||
## 5g. Der Verzeichnis-Abgleich: vier Schichten und eine Notbremse
|
||||
|
||||
Eine Verzeichnisquelle hat seit #028 eine **Art** — `LDAP`, `GRAPH` oder `SCIM` —,
|
||||
und die erste Frage ist nicht, wie gelesen wird, sondern **in welche Richtung**:
|
||||
|
||||
| Art | Richtung | Wo der Code liegt |
|
||||
|---|---|---|
|
||||
| `LDAP` | Laufzettel holt | `src/server/ldap/*` |
|
||||
| `GRAPH` | Laufzettel holt | `src/server/graph/*` |
|
||||
| `SCIM` | der Anbieter schickt | `src/app/api/scim/*`, `src/server/scim/*`, `src/server/services/scim.ts` |
|
||||
|
||||
Die ersten zwei laufen durch **denselben** Abgleich; die eine Stelle, die die Art
|
||||
kennt, ist `eintraegeDerQuelleStroemen` in
|
||||
`src/server/verzeichnis/quellenleser.ts`. Eine zweite Verzweigung waere eine
|
||||
zweite Stelle, die sich an eine neue Quellenart erinnern muss, und die vergisst
|
||||
man.
|
||||
|
||||
**Bei `SCIM` gibt es keinen Abgleich**, und das ist an zwei Stellen gehalten:
|
||||
`faelligeQuellen` nimmt solche Quellen nicht auf, und die Weiche wirft. Der Grund
|
||||
ist nicht Ordnungsliebe — ein Abgleich, der dort nichts liest und `ERFOLG` meldet,
|
||||
haette ueber die Abschaltregel die ganze Belegschaft deaktiviert. Dasselbe Muster
|
||||
wie der Reviewbefund aus #016, nur mit einer anderen Ursache.
|
||||
|
||||
Was beim Schreiben danach gilt, gilt fuer alle drei gleich: anlegen, uebernehmen,
|
||||
aktualisieren, deaktivieren, **nie loeschen** — auch ein `DELETE` eines
|
||||
SCIM-Anbieters setzt nur `aktiv = false`. Das **Token** einer SCIM-Quelle ist der
|
||||
einzige Mandantenbeweis, den eine Zustellung mitbringt; es steht als argon2-Hash
|
||||
in der Datenbank, wird einmal angezeigt und kommt in keiner Sicht und keinem
|
||||
Protokolleintrag vor.
|
||||
|
||||
Das LDAP-Modul liegt unter `src/server/ldap/` und ist absichtlich in vier Dateien
|
||||
geteilt, von denen nur eine das Netz anfasst:
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ Sitzungen ungültig *und* die verschlüsselt gespeicherten LDAP-Bind-Passwörter
|
||||
Administratoreinwilligung, und was anders ist als bei einem lokalen AD
|
||||
- **[Benachrichtigungen](benachrichtigungen.md)** — SMTP einrichten, den Webhook prüfen, die
|
||||
Signatur nachrechnen, und wo man nachsieht, wenn keine Mail mehr ankommt
|
||||
- **[SCIM-Endpunkt](scim.md)** — die einzige Stelle, an der ein fremder Dienst von außen schreibend
|
||||
zugreift: das Token, was nicht unterstützt wird, und was der Reverse Proxy durchlassen muss
|
||||
|
||||
## Wo sonst noch etwas steht
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# 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](../anleitung/06-verzeichnis.md#scim-der-anbieter-schickt).
|
||||
|
||||
## 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 (siehe [`authentik.md`](authentik.md)), nicht aus dem Provisioning. Ein Anbieter,
|
||||
der Gruppen zuweisen will, bekommt nichts zugewiesen — `ResourceTypes` nennt nur `User`.
|
||||
- **Allgemeine Filter.** Unterstützt wird genau `userName eq "…"` — die Frage, die ein Anbieter vor
|
||||
jedem Anlegen stellt. Alles andere ergibt `400` mit `scimType: 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 /Users` ohne 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-store` auf 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 als `401` zurück.
|
||||
- **Eine Zustellung ist klein**, einige Kilobyte. Die Grenzen für Anhänge
|
||||
(`client_max_body_size`, siehe [`reverse-proxy.md`](reverse-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:
|
||||
|
||||
```bash
|
||||
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“.
|
||||
+98
-2
@@ -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.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# #029 M16 – SCIM-Endpunkt als Alternative zum Abgleich
|
||||
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Im Tracker:** [#49](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/49)
|
||||
**Meilenstein:** v0.2.0
|
||||
**Labels:** phase-2, backend
|
||||
**Geschätzt:** 12 h **Tatsächlich:** –
|
||||
**Geschätzt:** 12 h **Tatsächlich:** 4 h 05 min
|
||||
|
||||
## Ziel
|
||||
|
||||
@@ -16,41 +16,137 @@ Setzt [#028](028-graph-quelle.md) voraus: die Quellenart `SCIM` braucht die Weic
|
||||
|
||||
## Zu entscheiden
|
||||
|
||||
- [ ] **Ein Token je Quelle, als Hash gespeichert** — Empfehlung, dasselbe Verfahren wie beim
|
||||
- [x] **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
|
||||
- [x] **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, bekommt `204` — 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 Filter `userName eq`, POST),
|
||||
- [x] Route Handler unter `src/app/api/scim/v2/`: `Users` (GET mit Filter `userName eq`, POST),
|
||||
`Users/{id}` (GET, PUT, PATCH, DELETE), dazu `ServiceProviderConfig`, `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
|
||||
- [x] **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`,
|
||||
- [x] Zuordnung: `externalId` → `externeId`, `userName` → `upn`, `name.givenName`/`familyName`,
|
||||
`emails[primary]` → `mail`, `active` → `aktiv`, `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User`
|
||||
für `department` und `manager`
|
||||
- [ ] Antwortform nach RFC 7644: `schemas`, `id`, `meta.resourceType`, `meta.location`, bei Listen
|
||||
- [x] Antwortform nach RFC 7644: `schemas`, `id`, `meta.resourceType`, `meta.location`, bei Listen
|
||||
`totalResults`/`itemsPerPage`/`startIndex`; Fehler als `urn:ietf:params:scim:api:messages:2.0:Error`
|
||||
- [ ] `PATCH` mit `op: add|replace|remove` auf den Pfaden, die die Anbieter wirklich schicken —
|
||||
- [x] `PATCH` mit `op: add|replace|remove` auf den Pfaden, die die Anbieter wirklich schicken —
|
||||
nicht der ganze Standard
|
||||
- [ ] Idempotenz: zweimal dasselbe `POST` ergibt `409`, 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
|
||||
- [x] Idempotenz: zweimal dasselbe `POST` ergibt `409`, nicht eine zweite Person
|
||||
- [x] Protokolleintrag je Vorgang, mit der Quelle als Urheber (Systemlauf, kein Benutzer)
|
||||
- [x] Admin → Verzeichnis: Token erzeugen, einmal anzeigen, widerrufen; letzte Zustellung sichtbar
|
||||
- [x] Anleitung Kapitel 06: Einrichtung in authentik und in Entra ID
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] Test gegen **aufgezeichnete Anfragen** von authentik und Entra ID (Form, nicht erfunden):
|
||||
- [x] 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: `DELETE` setzt `aktiv = false` und löscht nichts
|
||||
- [ ] Test: zweimal dasselbe `POST` ergibt `409` und keine zweite Zeile
|
||||
- [ ] Test: das Token steht nur als Hash in der Datenbank und in keiner Antwort
|
||||
- [ ] Test: eine Quelle der Art `SCIM` wird vom Zeitplan **nicht** abgeglichen — sie bekommt
|
||||
- [x] Test: ohne Token `401`, mit fremdem Token kein Zugriff auf die Personen einer anderen Quelle
|
||||
- [x] Test: `DELETE` setzt `aktiv = false` und löscht nichts
|
||||
- [x] Test: zweimal dasselbe `POST` ergibt `409` und keine zweite Zeile
|
||||
- [x] Test: das Token steht nur als Hash in der Datenbank und in keiner Antwort
|
||||
- [x] Test: eine Quelle der Art `SCIM` wird 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 remove` wirkte gar nicht** — ein echter Fehler, kein Testloch. `personAendern` hatte
|
||||
einen Schalter `nurGesetzte`, der bei `PATCH` jedes `null` als „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:
|
||||
`patchAnwenden` setzt auf dem vorhandenen Stand auf und liefert deshalb immer einen
|
||||
**vollständigen** Zielzustand. Der Unterschied zwischen `PUT` und `PATCH` liegt vor der
|
||||
Funktion, nicht in ihr. Schalter entfernt, Test ergänzt, Gegenprobe schlägt jetzt an.
|
||||
- **Entra ID schickt `active` als Zeichenkette `"True"`/`"False"`.** Das war richtig umgesetzt,
|
||||
aber nur halb geprüft: der Deaktivierungs-Test lief gegen `"False"`, und mit einem reinen
|
||||
`value === true` kommt 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 steht `ENTRA_PATCH_AN` mit
|
||||
`"True"` als eigene aufgezeichnete Form daneben.
|
||||
|
||||
- **`manager` war im Issue genannt und wäre fast untergegangen.** Die Zuordnung nennt die
|
||||
Enterprise-Erweiterung für `department` **und** `manager`; umgesetzt war zunächst nur die
|
||||
Abteilung. Dazugekommen sind: `vorgesetzterAus` liest 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, und `manager` im `Schemas`-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**.
|
||||
|
||||
- **`manager` wä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 `400` und 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 genau `userName eq "…"`, die Frage, die jeder Anbieter vor dem Anlegen stellt.
|
||||
|
||||
- **Ein Fehler im Nachbarcode aus #028**: in `verbindungTestenAktion` stand 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 `ERFOLG` meldet, hätte über die Abschaltregel die **ganze
|
||||
Belegschaft deaktiviert** — derselbe Fall, der in #016 einen Reviewbefund gekostet hat. Zwei
|
||||
Stellen halten das: `faelligeQuellen` nimmt 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 `401` fü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`, `filter` und `etag` stehen ausdrücklich auf `false`. Ein
|
||||
Anbieter, der `bulk` fü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“, `DELETE` deaktiviert, 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.ts`
|
||||
stammen 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 in
|
||||
[`docs/betrieb/scim.md`](../betrieb/scim.md) unter „Prüfen von Hand“.
|
||||
- **Keine Paginierung über 200 Einträge** bei `GET /Users` ohne 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. `ResourceTypes` nennt deshalb nur `User`.
|
||||
|
||||
@@ -33,11 +33,11 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
||||
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
||||
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – |
|
||||
| #028 | [M15 – Quellenarten und Microsoft-Graph-Quelle](028-graph-quelle.md) | erledigt | v0.2.0 | 10 h | 4 h 10 min |
|
||||
| #029 | [M16 – SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | offen | v0.2.0 | 12 h | – |
|
||||
| #029 | [M16 – SCIM-Endpunkt als Alternative zum Abgleich](029-scim-endpunkt.md) | erledigt | v0.2.0 | 12 h | 4 h 05 min |
|
||||
| #030 | [M17 – Wiederverwendbare Bausteine](030-bausteine.md) | erledigt | v0.2.0 | 12 h | 2 h 25 min |
|
||||
| #031 | [M18 – Abteilungsspezifische Zusatzabschnitte](031-abteilungsabschnitte.md) | erledigt | v0.2.0 | 8 h | 1 h 35 min |
|
||||
| #032 | [M19 – Anhänge und Fotos an Schritten](032-anhaenge.md) | erledigt | v0.2.0 | 10 h | 3 h 05 min |
|
||||
| #033 | [M20 – Live-Aktualisierung per SSE statt Polling](033-sse-live.md) | erledigt | v0.2.0 | 6 h | 1 h 40 min |
|
||||
| #034 | [M21 – Benachrichtigungen per Mail und Webhook](034-benachrichtigungen.md) | erledigt | v0.2.0 | 10 h | 3 h 30 min |
|
||||
| #035 | [M22 – Mitarbeiter-Selbstauskunft](035-selbstauskunft.md) | erledigt | v0.2.0 | 8 h | 2 h 50 min |
|
||||
| | **Summe** | | | **229 h** | **60 h 35 min** |
|
||||
| | **Summe** | | | **229 h** | **64 h 40 min** |
|
||||
|
||||
Reference in New Issue
Block a user