288 lines
9.7 KiB
TypeScript
288 lines
9.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { RolleSchluessel } from "@prisma/client";
|
|
import {
|
|
ALLE_RECHTE,
|
|
KeinZugriffFehler,
|
|
ROLLEN,
|
|
ROLLEN_BEZEICHNUNG,
|
|
ROLLEN_RECHTE,
|
|
alsRolle,
|
|
darfPersonSehen,
|
|
gehoertZurOrganisation,
|
|
hatAlleRechte,
|
|
hatEinesDerRechte,
|
|
hatRecht,
|
|
rechteVon,
|
|
verlangeRecht,
|
|
type Sitzung,
|
|
} from "@/lib/authz";
|
|
|
|
/**
|
|
* Beispieltest für Phase 0.
|
|
*
|
|
* Geprüft wird das, was am teuersten schiefgeht: dass der Gerätewart nicht in
|
|
* die Kasse kommt, dass der Administrator wirklich alles darf, und dass das
|
|
* Terminal nur das Nötigste kann.
|
|
*/
|
|
|
|
function sitzung(
|
|
rollen: Sitzung["rollen"],
|
|
organisationId = "org-1",
|
|
): Sitzung & { personId: string } {
|
|
return {
|
|
personId: "person-1",
|
|
organisationId,
|
|
name: "Testperson",
|
|
rollen,
|
|
anmeldeArt: "LOKAL",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Eine Sitzung der Kreisaufsicht: kein Konto in dieser Wehr, keine Akte.
|
|
*
|
|
* Seit Phase 10 gibt es diesen Zustand, und er ist der einzige, in dem
|
|
* `personId` fehlt.
|
|
*/
|
|
function aufsicht(organisationId = "org-2"): Sitzung {
|
|
return {
|
|
personId: null,
|
|
organisationId,
|
|
name: "Kreisbrandinspektion",
|
|
rollen: ["AUFSICHT"],
|
|
anmeldeArt: "LOKAL",
|
|
nurLesend: true,
|
|
};
|
|
}
|
|
|
|
describe("Rollenkatalog", () => {
|
|
it("hat für jede Rolle eine deutsche Bezeichnung", () => {
|
|
for (const rolle of ROLLEN) {
|
|
expect(ROLLEN_BEZEICHNUNG[rolle]).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
it("vergibt in ROLLEN_RECHTE nur Rechte, die es im Katalog gibt", () => {
|
|
for (const [rolle, rechte] of Object.entries(ROLLEN_RECHTE)) {
|
|
for (const recht of rechte) {
|
|
expect(ALLE_RECHTE, `${rolle} verweist auf unbekanntes Recht ${recht}`).toContain(
|
|
recht,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("hatRecht", () => {
|
|
it("gibt dem Administrator ausnahmslos alles", () => {
|
|
const admin = sitzung(["SUPERADMIN"]);
|
|
for (const recht of ALLE_RECHTE) {
|
|
expect(hatRecht(admin, recht)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("lässt den Gerätewart Geräte bearbeiten", () => {
|
|
expect(hatRecht(sitzung(["GERAETEWART"]), "geraet.bearbeiten")).toBe(true);
|
|
expect(hatRecht(sitzung(["GERAETEWART"]), "pruefung.durchfuehren")).toBe(true);
|
|
});
|
|
|
|
it("lässt den Gerätewart NICHT an die Kasse", () => {
|
|
expect(hatRecht(sitzung(["GERAETEWART"]), "kasse.bearbeiten")).toBe(false);
|
|
expect(hatRecht(sitzung(["GERAETEWART"]), "kasse.abschluss")).toBe(false);
|
|
});
|
|
|
|
it("lässt den Kassier nicht an Geräte und den Atemschutz", () => {
|
|
expect(hatRecht(sitzung(["KASSIER"]), "geraet.bearbeiten")).toBe(false);
|
|
expect(hatRecht(sitzung(["KASSIER"]), "atemschutz.ueberwachung")).toBe(false);
|
|
});
|
|
|
|
it("gibt dem einfachen Mitglied nur Lese- und eigene Rechte", () => {
|
|
const mitglied = sitzung(["MITGLIED"]);
|
|
expect(hatRecht(mitglied, "person.eigene")).toBe(true);
|
|
expect(hatRecht(mitglied, "geraet.lesen")).toBe(true);
|
|
expect(hatRecht(mitglied, "person.bearbeiten")).toBe(false);
|
|
expect(hatRecht(mitglied, "geraet.bearbeiten")).toBe(false);
|
|
});
|
|
|
|
it("beschränkt das Terminal auf Anmelden und Erfassen", () => {
|
|
const kiosk = sitzung(["KIOSK"]);
|
|
expect(hatRecht(kiosk, "kiosk.anmelden")).toBe(true);
|
|
expect(hatRecht(kiosk, "kiosk.erfassen")).toBe(true);
|
|
expect(hatRecht(kiosk, "person.bearbeiten")).toBe(false);
|
|
expect(hatRecht(kiosk, "system.einstellungen")).toBe(false);
|
|
expect(hatRecht(kiosk, "kasse.lesen")).toBe(false);
|
|
});
|
|
|
|
it("verweigert ohne Sitzung alles", () => {
|
|
for (const recht of ALLE_RECHTE) {
|
|
expect(hatRecht(null, recht)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("addiert die Rechte mehrerer Rollen", () => {
|
|
const doppelrolle = sitzung(["GERAETEWART", "KASSIER"]);
|
|
expect(hatRecht(doppelrolle, "geraet.bearbeiten")).toBe(true);
|
|
expect(hatRecht(doppelrolle, "kasse.bearbeiten")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("rechteVon", () => {
|
|
it("liefert für den Administrator den vollständigen Katalog", () => {
|
|
expect(rechteVon(["SUPERADMIN"]).size).toBe(ALLE_RECHTE.length);
|
|
});
|
|
|
|
it("liefert für das Terminal deutlich weniger als für den Kommandanten", () => {
|
|
expect(rechteVon(["KIOSK"]).size).toBeLessThan(rechteVon(["KOMMANDANT"]).size);
|
|
});
|
|
});
|
|
|
|
describe("hatEinesDerRechte / hatAlleRechte", () => {
|
|
const geraetewart = sitzung(["GERAETEWART"]);
|
|
|
|
it("genügt ein Treffer bei hatEinesDerRechte", () => {
|
|
expect(hatEinesDerRechte(geraetewart, ["kasse.bearbeiten", "geraet.bearbeiten"])).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("verlangt hatAlleRechte jeden einzelnen Treffer", () => {
|
|
expect(hatAlleRechte(geraetewart, ["kasse.bearbeiten", "geraet.bearbeiten"])).toBe(false);
|
|
expect(hatAlleRechte(geraetewart, ["geraet.lesen", "geraet.bearbeiten"])).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("verlangeRecht", () => {
|
|
it("lässt durch, wenn das Recht vorliegt", () => {
|
|
expect(() => verlangeRecht(sitzung(["KOMMANDANT"]), "person.bearbeiten")).not.toThrow();
|
|
});
|
|
|
|
it("wirft KeinZugriffFehler, wenn das Recht fehlt", () => {
|
|
expect(() => verlangeRecht(sitzung(["MITGLIED"]), "person.bearbeiten")).toThrow(
|
|
KeinZugriffFehler,
|
|
);
|
|
});
|
|
|
|
it("nennt im Fehler das fehlende Recht", () => {
|
|
try {
|
|
verlangeRecht(sitzung(["MITGLIED"]), "kasse.abschluss");
|
|
expect.unreachable("hätte werfen müssen");
|
|
} catch (fehler) {
|
|
expect(fehler).toBeInstanceOf(KeinZugriffFehler);
|
|
expect((fehler as Error).message).toContain("kasse.abschluss");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Mandantentrennung", () => {
|
|
it("erkennt Datensätze der eigenen Wehr", () => {
|
|
expect(gehoertZurOrganisation(sitzung(["MITGLIED"], "org-1"), { organisationId: "org-1" })).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("weist Datensätze einer fremden Wehr ab — auch dem Administrator", () => {
|
|
expect(
|
|
gehoertZurOrganisation(sitzung(["SUPERADMIN"], "org-1"), { organisationId: "org-2" }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("weist null-Datensätze ab", () => {
|
|
expect(gehoertZurOrganisation(sitzung(["KOMMANDANT"]), null)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("darfPersonSehen", () => {
|
|
it("lässt jeden die eigenen Daten sehen, auch ohne person.lesen", () => {
|
|
const kiosk = sitzung(["KIOSK"]);
|
|
expect(hatRecht(kiosk, "person.lesen")).toBe(false);
|
|
expect(darfPersonSehen(kiosk, kiosk.personId)).toBe(true);
|
|
});
|
|
|
|
it("verlangt für fremde Personen das Recht person.lesen", () => {
|
|
expect(darfPersonSehen(sitzung(["KIOSK"]), "andere-person")).toBe(false);
|
|
expect(darfPersonSehen(sitzung(["MITGLIED"]), "andere-person")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("alsRolle", () => {
|
|
it("erkennt Rollen unabhängig von Groß- und Kleinschreibung", () => {
|
|
expect(alsRolle("kommandant")).toBe("KOMMANDANT");
|
|
expect(alsRolle(" GeraeteWart ")).toBe("GERAETEWART");
|
|
});
|
|
|
|
it("gibt bei Unsinn null zurück", () => {
|
|
expect(alsRolle("oberbrandmeister")).toBeNull();
|
|
expect(alsRolle("")).toBeNull();
|
|
expect(alsRolle(null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Kreisaufsicht", () => {
|
|
/**
|
|
* Der wichtigste Test dieser Datei.
|
|
*
|
|
* Die Kreisebene sieht Wehren, denen sie nicht angehört. Bekäme sie dort
|
|
* versehentlich ein Schreibrecht, wäre das kein Bedienfehler, sondern ein
|
|
* Loch in der Mandantentrennung — und zwar eines, das niemandem auffällt,
|
|
* weil in der Oberfläche alles normal aussieht.
|
|
*
|
|
* Die Zusicherung ist deshalb bewusst allgemein formuliert: nicht "diese
|
|
* zwölf Rechte", sondern "kein einziges, das nicht lesen heißt". So gilt
|
|
* sie auch für Module, die es noch gar nicht gibt.
|
|
*/
|
|
it("hat ausschließlich Leserechte — auch für künftige Module", () => {
|
|
const rechte = [...rechteVon(["AUFSICHT"])];
|
|
|
|
expect(rechte.length).toBeGreaterThan(0);
|
|
expect(rechte.filter((recht) => !recht.endsWith(".lesen"))).toEqual([]);
|
|
});
|
|
|
|
it("sieht die Kasse nicht", () => {
|
|
// Sie gehört dem Verein oder der Kommune, nicht der Kreisaufsicht — und
|
|
// wäre nur deshalb dabei, weil das Recht zufällig auf ".lesen" endet.
|
|
expect(hatRecht(aufsicht(), "kasse.lesen")).toBe(false);
|
|
});
|
|
|
|
it("sieht weder Audit-Log noch KI-Protokoll", () => {
|
|
// Wer wann was getan hat, bleibt innerhalb der Wehr.
|
|
expect(hatRecht(aufsicht(), "system.auditlog")).toBe(false);
|
|
expect(hatRecht(aufsicht(), "ki.protokoll")).toBe(false);
|
|
});
|
|
|
|
it("kann keine Personendaten exportieren", () => {
|
|
// Das Auskunftsrecht nach DSGVO erfüllt die Wehr, die die Daten führt.
|
|
expect(hatRecht(aufsicht(), "person.export")).toBe(false);
|
|
});
|
|
|
|
it("sieht den Atemschutz — dafür gibt es sie", () => {
|
|
expect(hatRecht(aufsicht(), "atemschutz.lesen")).toBe(true);
|
|
expect(hatRecht(aufsicht(), "atemschutz.bearbeiten")).toBe(false);
|
|
expect(hatRecht(aufsicht(), "atemschutz.ueberwachung")).toBe(false);
|
|
});
|
|
|
|
it("hat in dieser Wehr keine eigene Akte", () => {
|
|
// Daran hängt, dass sie in keiner Mitgliederliste auftaucht und als
|
|
// Urheber keiner Änderung eingetragen werden kann.
|
|
expect(aufsicht().personId).toBeNull();
|
|
expect(aufsicht().nurLesend).toBe(true);
|
|
});
|
|
|
|
it("sieht fremde Personen nur über person.lesen, nie über die eigene Akte", () => {
|
|
// `darfPersonSehen` lässt jeden die eigenen Daten sehen. Ohne Akte darf
|
|
// dieser Weg nicht versehentlich greifen: sonst entschiede ein Vergleich
|
|
// von null mit null darüber, wer wen sieht.
|
|
const kreis = aufsicht();
|
|
expect(darfPersonSehen(kreis, "irgendeine-person")).toBe(true);
|
|
expect(hatRecht(kreis, "person.lesen")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("Rollenkatalog und Mandantentrennung", () => {
|
|
it("führt AUFSICHT nicht als vergebbare Rolle in der Datenbank", () => {
|
|
// Sie ergibt sich aus der Mitgliedschaftsart, nicht aus einer Zuweisung.
|
|
// Wäre sie vergebbar, könnte jemand innerhalb einer Wehr Rechte an einer
|
|
// anderen verteilen.
|
|
expect(RolleSchluessel).not.toHaveProperty("AUFSICHT");
|
|
});
|
|
});
|