Personen lassen sich anlegen, bearbeiten und archivieren; ihr Detail zeigt ihre Schluessel. Unter "Meine Schluessel" hinterlegt jede Rolle eigene Public Keys (Freigabe Frage 4), wirksam erst nach Zuweisung durch einen Operator oder Admin. Warum so: - Die eigene Person entsteht beim ersten Aufruf oder wird verknuepft: per benutzerId, sonst per E-Mail - aber nur, wenn der OIDC-Anbieter die Adresse bestaetigt hat. Dafuer merkt sich Benutzer jetzt emailBestaetigt (Migration); sonst koennte jemand mit selbst gewaehlter Adresse die Keys einer fremden Person sehen. - Die Self-Service-Services nehmen nur den angemeldeten Benutzer und leiten die Person selbst ab; fremde Schluessel-IDs gelten als nicht vorhanden. - Entfernen nur fuer eigene, selbst hinterlegte Keys ohne Verwendung, bedingt geloescht, damit eine gleichzeitige Zuweisung nicht per Cascade verschwindet. - Archivieren statt Loeschen: wer bis wann welchen Key hatte, bleibt nachvollziehbar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
9.2 KiB
TypeScript
214 lines
9.2 KiB
TypeScript
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<unknown>): Promise<FachFehler> {
|
|
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/);
|
|
});
|
|
});
|