feat(auth): Anmeldung, Rollen und Benutzerverwaltung (#21)
OIDC mit Discovery, PKCE, state und nonce; lokaler Login mit argon2 als Notfallzugang; Rechte in einer Tabelle, Prüfung ausschließlich serverseitig, Rolle und Aktiv-Status pro Anfrage aus der Datenbank. Ein unabhängiger Review fand eine offene Weiterleitung: die Präfix-Prüfung des Rücksprungziels ließ /<TAB>/fremd.example durch, woraus der Browser //fremd.example macht. Das Ziel wird jetzt aufgelöst und die Herkunft verglichen. Dazu acht weitere Befunde, alle behoben. 274 Tests. Closes #7 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #21.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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. Jede Rolle darf das. */
|
||||
lesen: ["ADMIN", "REDAKTEUR", "TECHNIKER", "LESER"],
|
||||
/** Laeufe starten, abarbeiten, abschliessen und archivieren. */
|
||||
laeufeBearbeiten: ["ADMIN", "REDAKTEUR", "TECHNIKER"],
|
||||
/** Geraete und deren Stammdaten pflegen, Tags registrieren. */
|
||||
geraeteBearbeiten: ["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`. 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.
|
||||
*/
|
||||
export function staerksteRolle(rollen: readonly Rolle[]): Rolle {
|
||||
let staerkste: Rolle = Rolle.LESER;
|
||||
for (const rolle of rollen) {
|
||||
if (ROLLEN_RANG[rolle] > ROLLEN_RANG[staerkste]) {
|
||||
staerkste = rolle;
|
||||
}
|
||||
}
|
||||
return staerkste;
|
||||
}
|
||||
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user