import { readFileSync } from "node:fs"; import { afterAll, beforeEach, describe, expect, it } from "vitest"; import type { Benutzer } from "@/generated/prisma/client"; import { prisma } from "@/lib/db"; import { darf } from "@/server/auth/rechte"; import { kontenAuswahl, personDetail, personenListe } from "@/server/queries/personen"; import { FachFehler } from "@/server/services/fachfehler"; import { eigenePersonSicherstellen, eigenenPublicKeyHinterlegen, eigenenSchluesselEntfernen, eigeneSchluessel, } from "@/server/services/meine-schluessel"; import { personAnlegen, personArchivieren, personBearbeiten } from "@/server/services/personen"; import { publicKeyImportieren } from "@/server/services/schluessel"; import { datenbankLeeren } from "./hilfen/db"; import { warteschlangeStoppen } from "./hilfen/vorgang"; import { ed25519Blob, zeile } from "./ssh/hilfen"; /** Personen und Self-Service „Meine Schluessel“ (#013). */ let operator: Benutzer; let leser: Benutzer; let andererLeser: Benutzer; beforeEach(async () => { await datenbankLeeren(); operator = await prisma.benutzer.create({ data: { name: "Olga Operator", rolle: "OPERATOR" } }); leser = await prisma.benutzer.create({ data: { name: "Lea Leser", rolle: "LESER", email: "lea@example.org", emailBestaetigt: true }, }); andererLeser = await prisma.benutzer.create({ data: { name: "Max Mitleser", rolle: "LESER", email: "max@example.org", emailBestaetigt: true }, }); }); afterAll(async () => { await warteschlangeStoppen(); }); async function fachfehlerVon(versuch: Promise): Promise { const fehler = await versuch.then( () => undefined, (f: unknown) => f, ); expect(fehler).toBeInstanceOf(FachFehler); return fehler as FachFehler; } const akteur = () => ({ id: operator.id, name: operator.name }); const neueZeile = (kommentar = "test") => zeile("ssh-ed25519", ed25519Blob(), kommentar); describe("Personen verwalten", () => { it("legt an, bearbeitet, archiviert und protokolliert", async () => { const person = await personAnlegen( { name: "Anna Berger", email: "A.Berger@Example.org", organisation: "Netzwerk GmbH" }, akteur(), ); const detail = await personDetail(person.id); expect(detail).toMatchObject({ email: "a.berger@example.org", organisation: "Netzwerk GmbH" }); await personBearbeiten( person.id, { name: "Anna Berger", email: "a.berger@example.org", notiz: "privat" }, akteur(), ); const geaendert = await prisma.protokoll.findFirstOrThrow({ where: { aktion: "person.geaendert" } }); // Die Notiz steht nicht im Protokoll, nur dass sie sich aenderte. expect(JSON.stringify(geaendert.details)).not.toContain("privat"); expect(geaendert.details).toMatchObject({ notiz: "geändert", organisation: { vorher: "Netzwerk GmbH" } }); await personArchivieren(person.id, akteur()); expect((await personenListe()).map((p) => p.name)).toEqual([]); expect((await personenListe({ archivierte: true })).map((p) => p.name)).toEqual(["Anna Berger"]); await fachfehlerVon(personBearbeiten(person.id, { name: "X" }, akteur())); expect(await prisma.protokoll.count({ where: { aktion: { startsWith: "person." } } })).toBe(3); }); it("prueft E-Mail und verknuepft ein Konto nur einmal", async () => { const fehler = await fachfehlerVon(personAnlegen({ name: "X", email: "keine-mail" }, akteur())); expect(fehler.feldfehler["email"]).toBeDefined(); await personAnlegen({ name: "Lea", benutzerId: leser.id }, akteur()); const doppelt = await fachfehlerVon(personAnlegen({ name: "Lea 2", benutzerId: leser.id }, akteur())); expect(doppelt.feldfehler["benutzerId"]).toBeDefined(); expect((await kontenAuswahl()).map((k) => k.name)).toEqual(["Max Mitleser", "Olga Operator"]); }); it("loest beim Archivieren die Kontoverknuepfung", async () => { const person = await personAnlegen({ name: "Lea", benutzerId: leser.id }, akteur()); await personArchivieren(person.id, akteur()); expect((await personDetail(person.id))?.benutzerId).toBeNull(); // Der Self-Service landet danach bei einer neuen Person, nicht bei der archivierten. expect((await eigenePersonSicherstellen(leser)).id).not.toBe(person.id); }); }); describe("Eigene Person", () => { it("wird beim ersten Aufruf angelegt und danach wiederverwendet", async () => { const [a, b] = await Promise.all([eigenePersonSicherstellen(leser), eigenePersonSicherstellen(leser)]); expect(a.id).toBe(b.id); expect(await prisma.person.count()).toBe(1); expect((await personDetail(a.id))?.benutzerId).toBe(leser.id); }); it("verknuepft per bestaetigter E-Mail mit einer vorhandenen Person", async () => { const person = await personAnlegen({ name: "Lea Leser (IT)", email: "LEA@example.org" }, akteur()); expect((await eigenePersonSicherstellen(leser)).id).toBe(person.id); expect(await prisma.protokoll.count({ where: { aktion: "person.verknuepft" } })).toBe(1); }); it("verknuepft nicht ueber eine unbestaetigte Adresse", async () => { const person = await personAnlegen({ name: "Chefin", email: "chefin@example.org" }, akteur()); const angreifer = await prisma.benutzer.create({ data: { name: "Mallory", rolle: "LESER", email: "chefin@example.org", emailBestaetigt: false }, }); expect((await eigenePersonSicherstellen(angreifer)).id).not.toBe(person.id); expect((await personDetail(person.id))?.benutzerId).toBeNull(); }); }); describe("Meine Schluessel", () => { it("hinterlegt eigene Public Keys als Personen-Keys", async () => { const neu = await eigenenPublicKeyHinterlegen(leser, { name: "Laptop", publicKey: neueZeile("lea@laptop"), }); const schluessel = await prisma.schluessel.findUniqueOrThrow({ where: { id: neu.id }, select: { kategorie: true, selbstHinterlegt: true, personId: true, erstelltVonId: true, masterKeyVersion: true, }, }); const person = await eigenePersonSicherstellen(leser); expect(schluessel).toEqual({ kategorie: "PERSON", selbstHinterlegt: true, personId: person.id, erstelltVonId: leser.id, masterKeyVersion: null, }); const eintrag = await prisma.protokoll.findFirstOrThrow({ where: { aktion: "schluessel.selbst_hinterlegt" }, }); expect(eintrag.akteurId).toBe(leser.id); }); it("zeigt nur eigene Keys und laesst fremde nicht entfernen", async () => { const eigener = await eigenenPublicKeyHinterlegen(leser, { name: "Meiner", publicKey: neueZeile() }); const fremder = await eigenenPublicKeyHinterlegen(andererLeser, { name: "Fremd", publicKey: neueZeile(), }); expect((await eigeneSchluessel(leser)).schluessel.map((s) => s.name)).toEqual(["Meiner"]); expect((await eigeneSchluessel(andererLeser)).schluessel.map((s) => s.name)).toEqual(["Fremd"]); const fehler = await fachfehlerVon(eigenenSchluesselEntfernen(leser, fremder.id)); expect(fehler.message).toMatch(/unter Ihren Schlüsseln nicht/); expect(await prisma.schluessel.count({ where: { id: fremder.id } })).toBe(1); await eigenenSchluesselEntfernen(leser, eigener.id); expect(await prisma.schluessel.count({ where: { id: eigener.id } })).toBe(0); expect(await prisma.protokoll.count({ where: { aktion: "schluessel.selbst_entfernt" } })).toBe(1); }); it("entfernt keine zugewiesenen und keine von Operatoren angelegten Keys", async () => { const zugewiesen = await eigenenPublicKeyHinterlegen(leser, { name: "Zugewiesen", publicKey: neueZeile(), }); const host = await prisma.host.create({ data: { name: "srv-test01", adresse: "10.0.0.1" } }); await prisma.publicVerwendung.create({ data: { schluesselId: zugewiesen.id, hostId: host.id, zielbenutzer: "lea" }, }); await fachfehlerVon(eigenenSchluesselEntfernen(leser, zugewiesen.id)); const person = await eigenePersonSicherstellen(leser); const vomOperator = await publicKeyImportieren( { name: "Vom Operator", kategorie: "PERSON", publicKey: neueZeile(), personId: person.id }, akteur(), ); await fachfehlerVon(eigenenSchluesselEntfernen(leser, vomOperator.id)); expect(await prisma.schluessel.count()).toBe(2); }); it("meldet Duplikate auch im Self-Service", async () => { const z = neueZeile(); await eigenenPublicKeyHinterlegen(leser, { name: "Einmal", publicKey: z }); const fehler = await fachfehlerVon( eigenenPublicKeyHinterlegen(andererLeser, { name: "Nochmal", publicKey: z }), ); expect(fehler.message).toMatch(/bereits verwaltet/); }); }); describe("Rechte (Freigabe Frage 4)", () => { it("Leser darf eigene Keys hinterlegen, aber weder verwalten noch zuweisen", () => { expect(darf("LESER", "eigeneSchluesselHinterlegen")).toBe(true); expect(darf("LESER", "schluesselVerwalten")).toBe(false); expect(darf("LESER", "publicKeyAusrollen")).toBe(false); }); it("die Self-Service-Aktionen verlangen das Self-Service-Recht und reichen nur den eigenen Benutzer weiter", () => { const text = readFileSync("src/server/actions/meine-schluessel.ts", "utf8"); expect(text.match(/rechtErforderlich\("eigeneSchluesselHinterlegen"\)/g)).toHaveLength(2); expect(text).not.toMatch(/personId/); }); });