import { execFileSync } from "node:child_process"; import { mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeEach, describe, expect, it } from "vitest"; import * as liste from "@/app/api/v1/schluessel/route"; import * as einzeln from "@/app/api/v1/schluessel/[id]/route"; import { verschluesseln } from "@/lib/geheimnisse"; import { schluesselErgaenzen, ausrollenVorbereiten } from "@/server/services/ausrollen"; import { katalogSynchronisieren } from "@/server/services/katalog"; import { fingerabdruck } from "@/server/schluessel/quelle"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; import { angemeldet, anfrage, kontext } from "./helpers/anfragen"; function neuerSchluessel(kommentar: string): string { const ordner = mkdtempSync(join(tmpdir(), "pd-key-")); execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", kommentar, "-f", join(ordner, "k")]); return readFileSync(join(ordner, "k.pub"), "utf8").trim(); } describe("Fingerprint", () => { it("entspricht ssh-keygen -lf", () => { const ordner = mkdtempSync(join(tmpdir(), "pd-key-")); execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-f", join(ordner, "k")]); const erwartet = execFileSync("ssh-keygen", ["-lf", join(ordner, "k.pub")]) .toString() .split(" ")[1]; expect(fingerabdruck(readFileSync(join(ordner, "k.pub"), "utf8"))).toBe(erwartet); }); }); describe.runIf(datenbankVerfuegbar)("SSH-Schlüssel", () => { let admin: { cookie: string; benutzer: { id: string; benutzername: string } }; let operator: { cookie: string; benutzer: { id: string; benutzername: string } }; beforeEach(async () => { await datenbankLeeren(); admin = await angemeldet("ADMIN"); operator = await angemeldet("OPERATOR"); }); const anlegen = async (cookie: string, rumpf: unknown) => await liste.POST(anfrage("POST", "/api/v1/schluessel", cookie, rumpf)); it("eigene Schlüssel für alle, Team-Schlüssel nur für Admins; jeder sieht eigene + Team", async () => { expect( (await anlegen(operator.cookie, { name: "Laptop", oeffentlich: neuerSchluessel("op") })).status, ).toBe(201); expect( (await anlegen(operator.cookie, { name: "Team?", oeffentlich: neuerSchluessel("x"), team: true })) .status, ).toBe(403); expect( (await anlegen(admin.cookie, { name: "Notfall", oeffentlich: neuerSchluessel("team"), team: true })) .status, ).toBe(201); expect( (await anlegen(admin.cookie, { name: "Admin", oeffentlich: neuerSchluessel("admin") })).status, ).toBe(201); const sicht = await (await liste.GET(anfrage("GET", "/api/v1/schluessel", operator.cookie))).json(); expect(sicht.map((s: { name: string; bereich: string }) => `${s.name}:${s.bereich}`).sort()).toEqual([ "Laptop:persoenlich", "Notfall:team", ]); }); it("weist private Schlüssel und Doppelte ab", async () => { const privat = await anlegen(operator.cookie, { name: "x", oeffentlich: "-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END OPENSSH PRIVATE KEY-----", }); expect(privat.status).toBe(400); expect((await privat.json()).fehler.felder.oeffentlich).toMatch(/nie den privaten/); const schluessel = neuerSchluessel("doppelt"); await anlegen(operator.cookie, { name: "a", oeffentlich: schluessel }); expect((await anlegen(operator.cookie, { name: "b", oeffentlich: schluessel })).status).toBe(409); }); it("fremde Schlüssel darf niemand löschen, Team-Schlüssel nur Admins", async () => { const eigener = await ( await anlegen(admin.cookie, { name: "Admin", oeffentlich: neuerSchluessel("a") }) ).json(); const team = await ( await anlegen(admin.cookie, { name: "Team", oeffentlich: neuerSchluessel("t"), team: true }) ).json(); const loeschen = (id: string, cookie: string) => einzeln.DELETE(anfrage("DELETE", `/api/v1/schluessel/${id}`, cookie), kontext({ id })); expect((await loeschen(eigener.id, operator.cookie)).status).toBe(403); expect((await loeschen(team.id, operator.cookie)).status).toBe(403); expect((await loeschen(team.id, admin.cookie)).status).toBe(204); }); it("Ausrollen ohne Schlüssel nimmt die hinterlegten (eigene + Team); angegebene gewinnen", async () => { await katalogSynchronisieren(); await testPrisma.server.create({ data: { name: "rz1", titel: "RZ 1", url: "https://127.0.0.1:8006", tokenId: "a@pve!b", tokenGeheimnis: verschluesseln("x"), fingerabdruck: "AA", }, }); const eigen = neuerSchluessel("eigen"); const team = neuerSchluessel("team"); await anlegen(operator.cookie, { name: "eigen", oeffentlich: eigen }); await anlegen(admin.cookie, { name: "team", oeffentlich: team, team: true }); const roh = { typ: "vm", server: "rz1", distribution: "debian-13", name: "web01", storage: "s", netz: { bridge: "vmbr0" }, zugang: { benutzer: "florian" }, }; const aufrufer = { benutzer: { ...operator.benutzer } as never, quelle: "CLI" as const }; const v = await ausrollenVorbereiten(await schluesselErgaenzen(roh, aufrufer)); expect(v.anfrage.zugang.sshSchluessel.sort()).toEqual([eigen, team].sort()); const anders = neuerSchluessel("anders"); const nurAngegeben = await schluesselErgaenzen( { ...roh, zugang: { benutzer: "florian", sshSchluessel: [anders] } }, aufrufer, ); expect((nurAngegeben as { zugang: { sshSchluessel: string[] } }).zugang.sshSchluessel).toEqual([anders]); }); it("ohne hinterlegte und ohne angegebene Schlüssel: verständlicher Fehler", async () => { await katalogSynchronisieren(); await testPrisma.server.create({ data: { name: "rz1", titel: "RZ 1", url: "https://127.0.0.1:8006", tokenId: "a@pve!b", tokenGeheimnis: verschluesseln("x"), fingerabdruck: "AA", }, }); const roh = { typ: "vm", server: "rz1", distribution: "debian-13", name: "web01", storage: "s", netz: { bridge: "vmbr0" }, zugang: { benutzer: "florian" }, }; await expect( ausrollenVorbereiten( await schluesselErgaenzen(roh, { benutzer: operator.benutzer as never, quelle: "CLI" }), ), ).rejects.toThrow(); }); });