„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>
178 lines
7.8 KiB
TypeScript
178 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { Rolle } from "@/generated/prisma/enums";
|
||
import { RECHTE, ROLLEN_RANG, ROLLEN_TEXT, darf, staerksteRolle, type Recht } from "@/server/auth/rollen";
|
||
|
||
const ALLE_ROLLEN = Object.values(Rolle);
|
||
const ALLE_RECHTE = Object.keys(RECHTE) as Recht[];
|
||
|
||
/**
|
||
* Die vollstaendige Rechtematrix, absichtlich ausgeschrieben statt aus `RECHTE` berechnet:
|
||
* nur so faellt auf, wenn jemand einem `LESER` versehentlich Schreibrechte gibt (#007). Ein
|
||
* Test, der die Tabelle nur gegen sich selbst haelt, wuerde jede Aenderung mitmachen.
|
||
*/
|
||
const MATRIX: Record<Recht, Record<Rolle, boolean>> = {
|
||
lesen: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: true, MITARBEITER: false },
|
||
selbstauskunft: { ADMIN: false, REDAKTEUR: false, TECHNIKER: false, LESER: false, MITARBEITER: true },
|
||
laeufeBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false, MITARBEITER: false },
|
||
geraeteBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: true, LESER: false, MITARBEITER: false },
|
||
mitarbeiterBearbeiten: {
|
||
ADMIN: true,
|
||
REDAKTEUR: true,
|
||
TECHNIKER: true,
|
||
LESER: false,
|
||
MITARBEITER: false,
|
||
},
|
||
vorlagenBearbeiten: { ADMIN: true, REDAKTEUR: true, TECHNIKER: false, LESER: false, MITARBEITER: false },
|
||
archivVerwalten: { ADMIN: true, REDAKTEUR: false, TECHNIKER: false, LESER: false, MITARBEITER: false },
|
||
verwaltung: { ADMIN: true, REDAKTEUR: false, TECHNIKER: false, LESER: false, MITARBEITER: false },
|
||
};
|
||
|
||
describe("Rechtematrix", () => {
|
||
const faelle = ALLE_RECHTE.flatMap((recht) =>
|
||
ALLE_ROLLEN.map((rolle) => [recht, rolle, MATRIX[recht][rolle]] as const),
|
||
);
|
||
|
||
it("prueft jede Rolle gegen jedes Recht", () => {
|
||
// Haelt fest, dass die Tabelle oben wirklich vollstaendig ist: fuenf Rollen,
|
||
// acht Rechte. Die Zahlen stehen hier ausgeschrieben, damit eine neue Rolle
|
||
// oder ein neues Recht **hier** auffaellt und nicht erst im Betrieb (#035).
|
||
expect(ALLE_ROLLEN).toHaveLength(5);
|
||
expect(ALLE_RECHTE).toHaveLength(8);
|
||
expect(faelle).toHaveLength(40);
|
||
});
|
||
|
||
it.each(faelle)("%s: %s -> %s", (recht, rolle, erwartet) => {
|
||
expect(darf(rolle, recht)).toBe(erwartet);
|
||
});
|
||
});
|
||
|
||
describe("Rollen im Einzelnen", () => {
|
||
it("laesst LESER nur lesen", () => {
|
||
expect(darf("LESER", "lesen")).toBe(true);
|
||
for (const recht of ALLE_RECHTE.filter((r) => r !== "lesen")) {
|
||
expect(darf("LESER", recht), `LESER darf ${recht} nicht`).toBe(false);
|
||
}
|
||
});
|
||
|
||
it("gibt TECHNIKER Laeufe und Geraete, aber keine Vorlagen und keine Verwaltung", () => {
|
||
expect(darf("TECHNIKER", "laeufeBearbeiten")).toBe(true);
|
||
expect(darf("TECHNIKER", "geraeteBearbeiten")).toBe(true);
|
||
expect(darf("TECHNIKER", "vorlagenBearbeiten")).toBe(false);
|
||
expect(darf("TECHNIKER", "verwaltung")).toBe(false);
|
||
expect(darf("TECHNIKER", "archivVerwalten")).toBe(false);
|
||
});
|
||
|
||
it("gibt REDAKTEUR alles von TECHNIKER plus Vorlagen, aber keine Verwaltung", () => {
|
||
for (const recht of ALLE_RECHTE.filter((r) => darf("TECHNIKER", r))) {
|
||
expect(darf("REDAKTEUR", recht), `REDAKTEUR darf ${recht} wie ein TECHNIKER`).toBe(true);
|
||
}
|
||
expect(darf("REDAKTEUR", "vorlagenBearbeiten")).toBe(true);
|
||
expect(darf("REDAKTEUR", "verwaltung")).toBe(false);
|
||
expect(darf("REDAKTEUR", "archivVerwalten")).toBe(false);
|
||
});
|
||
|
||
it("gibt ADMIN alles – ausser der Selbstauskunft", () => {
|
||
for (const recht of ALLE_RECHTE.filter((r) => r !== "selbstauskunft")) {
|
||
expect(darf("ADMIN", recht), `ADMIN darf ${recht}`).toBe(true);
|
||
}
|
||
/*
|
||
* Die eine Ausnahme, und sie ist Absicht (#035): `selbstauskunft` ist kein
|
||
* zusaetzliches Recht, sondern ein **Ersatz** fuer `lesen`. Ein ADMIN sieht
|
||
* ueber `lesen` ohnehin alles; ihm die Selbstauskunft zu geben waere ein
|
||
* zweiter Weg zu denselben Daten — und damit eine zweite Stelle, an der
|
||
* jemand die Einschraenkung auf die eigene Person vergessen kann.
|
||
*/
|
||
expect(darf("ADMIN", "selbstauskunft")).toBe(false);
|
||
});
|
||
|
||
it("laesst MITARBEITER genau eines und sonst nichts", () => {
|
||
expect(darf("MITARBEITER", "selbstauskunft")).toBe(true);
|
||
for (const recht of ALLE_RECHTE.filter((r) => r !== "selbstauskunft")) {
|
||
expect(darf("MITARBEITER", recht), `MITARBEITER darf ${recht} nicht`).toBe(false);
|
||
}
|
||
// Vor allem nicht `lesen`: das waere die Akteneinsicht, die es nicht sein soll.
|
||
expect(darf("MITARBEITER", "lesen")).toBe(false);
|
||
});
|
||
|
||
it("behaelt das Zurueckholen aus dem Archiv beim ADMIN", () => {
|
||
// Steht als eigener Test da, weil die Zusage "ein archivierter Lauf aendert sich nicht"
|
||
// daran haengt.
|
||
expect(RECHTE.archivVerwalten).toStrictEqual(["ADMIN"]);
|
||
});
|
||
});
|
||
|
||
describe("Aufbau der Tabelle", () => {
|
||
it("listet fuer jedes Recht mindestens eine Rolle", () => {
|
||
for (const recht of ALLE_RECHTE) {
|
||
expect(RECHTE[recht].length, `${recht} ohne Rolle waere fuer niemanden erreichbar`).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
it("nennt in jedem Recht nur bekannte Rollen, jede hoechstens einmal", () => {
|
||
for (const recht of ALLE_RECHTE) {
|
||
const rollen = RECHTE[recht] as readonly Rolle[];
|
||
expect(new Set(rollen).size, `${recht} nennt eine Rolle doppelt`).toBe(rollen.length);
|
||
for (const rolle of rollen) {
|
||
expect(ALLE_ROLLEN).toContain(rolle);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("deckt mit ROLLEN_TEXT genau die Rollen des Enums ab", () => {
|
||
expect(Object.keys(ROLLEN_TEXT).sort()).toStrictEqual([...ALLE_ROLLEN].sort());
|
||
for (const rolle of ALLE_ROLLEN) {
|
||
expect(ROLLEN_TEXT[rolle].length).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
it("deckt mit ROLLEN_RANG genau die Rollen des Enums ab", () => {
|
||
expect(Object.keys(ROLLEN_RANG).sort()).toStrictEqual([...ALLE_ROLLEN].sort());
|
||
});
|
||
|
||
it("gibt jeder Rolle einen eigenen Rang, ADMIN oben und MITARBEITER unten", () => {
|
||
const raenge = ALLE_ROLLEN.map((rolle) => ROLLEN_RANG[rolle]);
|
||
expect(new Set(raenge).size).toBe(ALLE_ROLLEN.length);
|
||
expect(Math.max(...raenge)).toBe(ROLLEN_RANG.ADMIN);
|
||
expect(Math.min(...raenge)).toBe(ROLLEN_RANG.MITARBEITER);
|
||
expect(ROLLEN_RANG.ADMIN).toBeGreaterThan(ROLLEN_RANG.REDAKTEUR);
|
||
expect(ROLLEN_RANG.REDAKTEUR).toBeGreaterThan(ROLLEN_RANG.TECHNIKER);
|
||
expect(ROLLEN_RANG.TECHNIKER).toBeGreaterThan(ROLLEN_RANG.LESER);
|
||
/*
|
||
* **Unter LESER.** Der Rang entscheidet in `staerksteRolle`, wer gewinnt,
|
||
* wenn jemand in mehreren abgebildeten Gruppen steht. Stuende MITARBEITER
|
||
* darueber, verdraengte eine Selbstauskunft-Gruppe die echte Rolle — und ein
|
||
* Techniker verlöre seine Rechte, weil er auch Mitarbeiter des Hauses ist.
|
||
*/
|
||
expect(ROLLEN_RANG.LESER).toBeGreaterThan(ROLLEN_RANG.MITARBEITER);
|
||
});
|
||
});
|
||
|
||
describe("staerksteRolle", () => {
|
||
it("gibt bei leerer Liste LESER zurueck", () => {
|
||
// Wer in keiner abgebildeten Gruppe ist, soll hineinsehen duerfen -- nicht scheitern
|
||
// und erst recht nicht ADMIN werden.
|
||
expect(staerksteRolle([])).toBe("LESER");
|
||
});
|
||
|
||
it.each(ALLE_ROLLEN)("gibt bei nur %s diese Rolle zurueck", (rolle) => {
|
||
expect(staerksteRolle([rolle])).toBe(rolle);
|
||
});
|
||
|
||
it.each([
|
||
["LESER und TECHNIKER", ["LESER", "TECHNIKER"], "TECHNIKER"],
|
||
["TECHNIKER und REDAKTEUR", ["TECHNIKER", "REDAKTEUR"], "REDAKTEUR"],
|
||
["REDAKTEUR und ADMIN", ["REDAKTEUR", "ADMIN"], "ADMIN"],
|
||
["alle vier", ["LESER", "TECHNIKER", "REDAKTEUR", "ADMIN"], "ADMIN"],
|
||
["ADMIN zuerst genannt", ["ADMIN", "LESER"], "ADMIN"],
|
||
["LESER zuerst genannt", ["LESER", "ADMIN"], "ADMIN"],
|
||
["dieselbe Rolle mehrfach", ["TECHNIKER", "TECHNIKER"], "TECHNIKER"],
|
||
] as const)("waehlt aus %s die Rolle %s", (_fall, rollen, erwartet) => {
|
||
expect(staerksteRolle(rollen)).toBe(erwartet);
|
||
});
|
||
|
||
it("ist von der Reihenfolge unabhaengig", () => {
|
||
const rollen: Rolle[] = ["TECHNIKER", "ADMIN", "LESER", "REDAKTEUR"];
|
||
expect(staerksteRolle(rollen)).toBe(staerksteRolle([...rollen].reverse()));
|
||
});
|
||
});
|