feat(selbstauskunft): Mitarbeiter sehen ihre eigenen Geraete (#55)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

„Was ist eigentlich mit meinem Notebook passiert?“ musste bisher ein
Techniker nachsehen. Ein Mitarbeiter kann die Arbeiten an **seinen**
Geraeten jetzt selbst ansehen — mit der fuenften Rolle MITARBEITER,
zugeordnet beim Anmelden ueber den UPN.

Was er sieht, sind Titel, Zeitraum und Status. Keine Schritte, keine
Notizen, keine Kommentare, keine Anhaenge: in einem Lauf stehen
Arbeitsnotizen ueber Menschen und Befehle mit Platzhaltern fuer
Zugangsdaten. Eine Selbstauskunft ist keine Akteneinsicht, und das haelt
nicht die Seite fest, sondern das Lesemodell — eine Seite, die filtert,
ist eine Seite, die es vergessen kann.

`selbstauskunft` ist ein **Ersatz** fuer `lesen` und kein Zusatz; auch
ein ADMIN hat es nicht. Ein zweiter Weg zu denselben Daten waere eine
zweite Stelle, an der jemand die Einschraenkung auf die eigene Person
vergisst. Die Datenbank verlangt das sogar: ein CHECK haelt fest, dass
Rolle und Zuordnung zusammengehoeren, in beide Richtungen.

Ein mehrdeutiger UPN wird abgewiesen und protokolliert. Zwei Kunden
koennen dieselbe Person fuehren; ein findFirst entschiede nach
Sortierung darueber, wessen Geraete jemand sieht.

**Verhaltensaenderung:** wer sich anmeldet und in keiner Rollengruppe
steht, bekommt nicht mehr automatisch LESER — also nicht mehr
Lesezugriff auf alle Laeufe des Hauses. Das greift nur mit
konfigurierten OIDC_GROUP_*; ohne sie bleibt alles wie vorher, denn ohne
abgebildete Gruppen laesst sich Techniker nicht von Mitarbeiter
unterscheiden.

Nebenbei behoben: `staerksteRolle` benutzte LESER als Startwert und
damit als Untergrenze — mit einer Rolle darunter lieferte sie mehr
Rechte als uebergeben. Gefunden von einem Test, der seit #007
unveraendert dasteht.

Closes #55

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-10-06 07:32:54 +02:00
co-authored by Claude Opus 5
parent a417426f43
commit 0158d0292f
28 changed files with 1806 additions and 76 deletions
+64 -7
View File
@@ -20,8 +20,24 @@ import { ROLLEN_RANG, ROLLEN_TEXT } from "@/lib/rollen-text";
* des Cookies.
*/
export const RECHTE = {
/** Vorlagen, Laeufe, Geraete und Verzeichnis ansehen. Jede Rolle darf das. */
/**
* Vorlagen, Laeufe, Geraete und Verzeichnis ansehen.
*
* **Nicht mehr „jede Rolle“** (#035): `MITARBEITER` fehlt hier, und das ist
* der ganze Punkt dieser Rolle. Wer sich selbst auskunftsberechtigt anmeldet,
* darf nicht die Laeufe des Hauses lesen — dafuer gibt es `selbstauskunft`.
*/
lesen: ["ADMIN", "REDAKTEUR", "TECHNIKER", "LESER"],
/**
* Die eigenen Geraete und die Laeufe dazu ansehen (#035).
*
* **Nur `MITARBEITER`.** Die anderen Rollen sehen ueber `lesen` ohnehin alles;
* sie hier mit aufzunehmen waere ein zweiter Weg zu denselben Daten und damit
* eine zweite Stelle, an der jemand die Einschraenkung auf die eigene Person
* vergessen kann. Ein Techniker, der seine eigenen Geraete sehen will, nimmt
* die gewoehnliche Geraeteliste.
*/
selbstauskunft: ["MITARBEITER"],
/** Laeufe starten, abarbeiten, abschliessen und archivieren. */
laeufeBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"],
/** Geraete und deren Stammdaten pflegen, Tags registrieren. */
@@ -57,18 +73,59 @@ export function darf(rolle: Rolle, recht: Recht): boolean {
* Die staerkste der uebergebenen Rollen.
*
* Der OIDC-Login braucht das: ist jemand in mehreren abgebildeten Gruppen, gewinnt die
* staerkere Rolle. Eine leere Liste ergibt `LESER` -- nie ein Fehler und nie `ADMIN`. Wer
* sich anmelden darf, aber keiner abgebildeten Gruppe angehoert, soll hineinsehen koennen
* und nichts aendern; ein Fehler an dieser Stelle wuerde stattdessen den Login blockieren.
* staerkere Rolle. Eine leere Liste ergibt `LESER` -- nie ein Fehler und nie `ADMIN`.
*
* **Das gilt weiter fuer eine eingerichtete Zuordnung ohne Treffer in dieser
* Funktion.** Ob aus „keine Gruppe getroffen“ eine Selbstauskunft wird, entscheidet
* seit #035 der Login ueber `rolleAusGruppenGenau` — hier bleibt der Standard
* `LESER`, damit ein Aufrufer ohne diese Unterscheidung nie versehentlich mehr
* bekommt als vorher.
*/
export function staerksteRolle(rollen: readonly Rolle[]): Rolle {
let staerkste: Rolle = Rolle.LESER;
/*
* `undefined` als Startwert, nicht `LESER`.
*
* Mit `LESER` als Startwert war der Wert zugleich **Untergrenze**: eine
* uebergebene Rolle unterhalb davon kam nie heraus. Solange alle Rollen
* mindestens `LESER` waren, fiel das nicht auf — mit `MITARBEITER` (#035) gab
* `staerksteRolle(["MITARBEITER"])` ploetzlich `LESER`, also **mehr** Rechte
* als uebergeben. Der Test „gibt bei nur <Rolle> diese Rolle zurueck“ stand
* seit #007 da und hat es sofort gezeigt.
*
* Der Rueckfall auf `LESER` gilt jetzt nur noch fuer die **leere** Liste, und
* genau so steht es auch im Kommentar darueber.
*/
let staerkste: Rolle | undefined;
for (const rolle of rollen) {
if (ROLLEN_RANG[rolle] > ROLLEN_RANG[staerkste]) {
if (staerkste === undefined || ROLLEN_RANG[rolle] > ROLLEN_RANG[staerkste]) {
staerkste = rolle;
}
}
return staerkste;
return staerkste ?? Rolle.LESER;
}
/**
* Wo diese Rolle nach der Anmeldung landet (#035).
*
* **Gefunden beim Durchklicken:** ein `MITARBEITER` meldete sich an und sah
* „Dafuer fehlt dir die Berechtigung“ — das Standardziel ist `/`, und die
* Uebersicht verlangt `lesen`. Genau die leere Seite, die wie ein Fehler
* aussieht und die #035 vermeiden will.
*
* Eine reine Funktion und nicht ein `if` in der Seite: dieselbe Antwort braucht
* die Uebersicht (zum Umleiten), `/kein-zugriff` (fuer seinen Knopf — der auf
* `/` zeigte und damit im Kreis) und spaeter jede weitere Rolle mit eigenem
* Einstieg.
*/
export function startseite(rolle: Rolle): string {
if (darf(rolle, "lesen")) return "/";
if (darf(rolle, "selbstauskunft")) return "/meine-geraete";
/*
* Kein Recht fuer irgendeinen Einstieg. Gibt es heute nicht — jede Rolle hat
* entweder `lesen` oder `selbstauskunft` —, und `/` ist hier die ehrlichere
* Antwort als ein Wurf: dort erklaert die Seite, was los ist.
*/
return "/";
}
// Beschriftung und Rang liegen in src/lib, damit Client-Komponenten sie ohne