„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>
134 lines
6.0 KiB
TypeScript
134 lines
6.0 KiB
TypeScript
import { Rolle } from "@/generated/prisma/enums";
|
|
import { ROLLEN_RANG, ROLLEN_TEXT } from "@/lib/rollen-text";
|
|
|
|
/**
|
|
* Wer darf was (#007).
|
|
*
|
|
* Die Rechte stehen bewusst als eine Tabelle an einer Stelle -- nicht als
|
|
* `if (rolle === "ADMIN")` verstreut ueber Seiten, Aktionen und Services. Drei Gruende:
|
|
*
|
|
* 1. Eine Rechteaenderung hat genau einen Ort. Kommt spaeter eine Rolle hinzu oder darf ein
|
|
* `REDAKTEUR` etwas mehr, wird eine Zeile hier geaendert und nicht ein Dutzend Abfragen
|
|
* gesucht, von denen eine uebersehen wird.
|
|
* 2. Die Tabelle ist pruefbar. `tests/rollen.test.ts` schreibt jede Rolle gegen jedes Recht aus;
|
|
* eine versehentlich erweiterte Liste faellt sofort auf, statt erst im Betrieb.
|
|
* 3. Wer eine neue Aktion baut, sucht sich hier das passende Recht und prueft es mit `darf()`.
|
|
* Ein Recht kann so nie doppelt und unterschiedlich ausgelegt werden.
|
|
*
|
|
* Geprueft wird ausschliesslich serverseitig. Die Rolle kommt dabei pro Anfrage aus der
|
|
* Datenbank, nicht aus dem Sitzungstoken -- ein Rechteentzug wirkt sonst erst nach Ablauf
|
|
* des Cookies.
|
|
*/
|
|
export const RECHTE = {
|
|
/**
|
|
* 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. */
|
|
geraeteBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"],
|
|
/**
|
|
* Mitarbeiter von Hand anlegen, bearbeiten und deaktivieren (#011).
|
|
*
|
|
* Dieselben Rollen wie bei den Geraeten, und aus demselben Grund: beim
|
|
* Onboarding steht der Techniker vor der Person, bevor der naechste
|
|
* Verzeichnisabgleich gelaufen ist. Die **Abteilungen** bleiben dagegen bei
|
|
* `verwaltung` — an ihnen haengen Ziel-OU und Standardgruppen, also das, was
|
|
* die spaeteren PowerShell-Befehle in ein Verzeichnis schreiben.
|
|
*/
|
|
mitarbeiterBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"],
|
|
/** Vorlagen anlegen, bearbeiten und veroeffentlichen. */
|
|
vorlagenBearbeiten: ["ADMIN", "REDAKTEUR"],
|
|
/**
|
|
* Einen archivierten Lauf zurueckholen. Nur `ADMIN`: das Archiv ist die Zusage, dass ein
|
|
* abgeschlossener Lauf sich nicht mehr aendert -- diese Zusage darf nur eine Stelle aufheben.
|
|
*/
|
|
archivVerwalten: ["ADMIN"],
|
|
/** App-Benutzer, Verzeichnis, Abteilungen, Einstellungen, Kunden. */
|
|
verwaltung: ["ADMIN"],
|
|
} as const satisfies Record<string, readonly Rolle[]>;
|
|
|
|
export type Recht = keyof typeof RECHTE;
|
|
|
|
export function darf(rolle: Rolle, recht: Recht): boolean {
|
|
return (RECHTE[recht] as readonly Rolle[]).includes(rolle);
|
|
}
|
|
|
|
/**
|
|
* 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`.
|
|
*
|
|
* **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 {
|
|
/*
|
|
* `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 (staerkste === undefined || ROLLEN_RANG[rolle] > ROLLEN_RANG[staerkste]) {
|
|
staerkste = rolle;
|
|
}
|
|
}
|
|
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
|
|
// die Rechtetabelle bekommen. Hier weitergereicht fuer serverseitige Aufrufer.
|
|
export { ROLLEN_RANG, ROLLEN_TEXT };
|