feat(schluessel): Private Key anzeigen, herunterladen, loeschen (#014)
Admins koennen einen gespeicherten Private Key nach frischer Bestaetigung einmalig anzeigen oder als Datei <slug> herunterladen und ihn endgueltig aus SSHKeyVault loeschen. Warum so: - Der Web-Prozess hat keinen Master-Key. Er bittet den Worker per Vorgang, den Key zu oeffnen; der legt ihn mit SSHKV_APP_SECRET verschluesselt in Vorgang.ergebnis ab. Das Web holt ihn genau einmal ab: ein einziges UPDATE unter Zeilensperre liest und ueberschreibt. - Nicht abgeholte Keys verfallen nach zwei Minuten: Worker-Takt alle 30 s, Aufraeumen vor jeder Abholung, und das Web gibt aeltere nicht mehr heraus. Die Status-Schnittstelle zeigt nur "bereit". - Download nur per POST (ein GET-Link oder Vorabruf wuerde die Abholung verbrauchen), no-store, Herkunft per Sec-Fetch-Site, weil no-referrer bei Formular-POSTs Origin: null sendet. - Loeschen laeuft als Worker-Vorgang, damit nur geheimnisse.ts die Chiffrat-Felder anfasst; eigenes Recht privateKeyLoeschen mit Bestaetigung. Deployment-Keys gibt sshkv weder heraus noch loescht es sie. - Protokoll: angefordert, angezeigt, heruntergeladen, geloescht - mit Fingerprint, nie mit Key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "@/lib/validierung";
|
||||
import { akteurVon } from "@/server/actions/formular";
|
||||
import { BestaetigungFehler, RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { HostFehler } from "@/server/services/hosts";
|
||||
import {
|
||||
abholungAnfordern,
|
||||
privateKeyAbholen,
|
||||
privateKeyLoeschenAnfordern,
|
||||
} from "@/server/services/privatekey";
|
||||
|
||||
/**
|
||||
* Private Key anzeigen und loeschen (#014). Jede Aktion prueft selbst Recht **und** frische
|
||||
* Bestaetigung (`rechtErforderlich` mit einem Recht aus BESTAETIGUNG_NOETIG). Fehlt die
|
||||
* Bestaetigung, meldet die Antwort `bestaetigen: true`, und die Oberflaeche leitet nach
|
||||
* /bestaetigen weiter.
|
||||
*
|
||||
* `privateKeyAnzeigenAktion` ist die einzige Stelle, an der ein Private Key als Antwort an den
|
||||
* Browser geht – genau einmal, fuer die Anzeige im Dialog.
|
||||
*/
|
||||
export type PrivateKeyAntwort = {
|
||||
readonly vorgangId?: string;
|
||||
readonly fehler?: string;
|
||||
readonly bestaetigen?: boolean;
|
||||
readonly privateKey?: string;
|
||||
};
|
||||
|
||||
const id = z.string().min(1).max(64);
|
||||
const zweck = z.enum(["anzeigen", "herunterladen"]);
|
||||
|
||||
function fehlerAntwort(fehler: unknown): PrivateKeyAntwort {
|
||||
if (fehler instanceof BestaetigungFehler) return { fehler: fehler.message, bestaetigen: true };
|
||||
if (fehler instanceof RechteFehler || fehler instanceof HostFehler) return { fehler: fehler.message };
|
||||
if (fehler instanceof z.ZodError) return { fehler: "Ungültige Anfrage." };
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
export async function abholungAnfordernAktion(schluesselId: string, art: string): Promise<PrivateKeyAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("privateKeyAnzeigen");
|
||||
return {
|
||||
vorgangId: await abholungAnfordern(id.parse(schluesselId), zweck.parse(art), akteurVon(benutzer)),
|
||||
};
|
||||
} catch (fehler) {
|
||||
return fehlerAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function privateKeyAnzeigenAktion(vorgangId: string): Promise<PrivateKeyAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("privateKeyAnzeigen");
|
||||
const { privateKey } = await privateKeyAbholen(id.parse(vorgangId), "anzeigen", akteurVon(benutzer));
|
||||
return { privateKey };
|
||||
} catch (fehler) {
|
||||
return fehlerAntwort(fehler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function privateKeyLoeschenAktion(schluesselId: string): Promise<PrivateKeyAntwort> {
|
||||
try {
|
||||
const { benutzer } = await rechtErforderlich("privateKeyLoeschen");
|
||||
const vorgangId = await privateKeyLoeschenAnfordern(id.parse(schluesselId), akteurVon(benutzer));
|
||||
revalidatePath(`/schluessel/${schluesselId}`);
|
||||
return { vorgangId };
|
||||
} catch (fehler) {
|
||||
return fehlerAntwort(fehler);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user