feat(rotation): gefuehrte Rotation mit Test vor dem Widerruf (#023)
Einen Schlüssel zu tauschen soll keinen Ausfall riskieren: Der Nachfolger übernimmt alle aktiven Zuweisungen, wird ausgerollt und per Verbindungstest geprüft – erst wenn alle Tests seit dem letzten Ausrollen grün sind, entfernt der Widerruf aus #024 den alten Schlüssel, der dann als ROTIERT endet. Private Keys bekommen einen neuen Pfad (-rJJJJMMTT) bei gleichem Alias. Damit ssh den neuen nimmt, schreibt privat.ts je Alias nur noch einen config-Eintrag; die jüngste Zuweisung gewinnt. Der Worker setzt vorgaengerId schon beim Erzeugen, so ist die Kette auch nach einem Abbruch vollständig und die Schrittfolge wiederaufnehmbar. Personen-Keys ohne Private Key in sshkv bekommen nur einen Hinweis: dafür braucht es einen neuen Public Key von der Person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { KeyRound } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Rotation } from "@/components/rotation/Rotation";
|
||||
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Leerzustand } from "@/components/ui/Leerzustand";
|
||||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||||
import { darf } from "@/server/auth/rechte";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { RotationsFehler, rotationsStand } from "@/server/services/rotation";
|
||||
import { offeneEntfernungen } from "@/server/services/widerruf";
|
||||
|
||||
export const metadata: Metadata = { title: "Schlüssel rotieren" };
|
||||
|
||||
/** Gefuehrte Rotation (#023): Schrittfolge, wiederaufnehmbar. */
|
||||
export default async function RotierenSeite({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const { benutzer } = await sitzungErforderlich("schluesselVerwalten", `/schluessel/${id}/rotieren`);
|
||||
if (!darf(benutzer.rolle, "publicKeyAusrollen")) notFound();
|
||||
let stand;
|
||||
try {
|
||||
stand = await rotationsStand(id);
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof RotationsFehler) notFound();
|
||||
throw fehler;
|
||||
}
|
||||
const { alt } = stand;
|
||||
|
||||
const kopf = (
|
||||
<Seitenkopf
|
||||
oben={
|
||||
<Breadcrumb
|
||||
pfad={[
|
||||
{ titel: "Schlüssel", href: "/schluessel" },
|
||||
{ titel: alt.name, href: `/schluessel/${alt.id}` },
|
||||
{ titel: "Rotieren" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
titel={`„${alt.name}“ rotieren`}
|
||||
untertitel="Neuer Schlüssel an allen Stellen, testen, erst dann den alten entfernen."
|
||||
/>
|
||||
);
|
||||
|
||||
// Ohne Nachfolger und nicht rotierbar: nur der Hinweis, keine halbe Rotation.
|
||||
if (!stand.neu && stand.gesperrt) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-7">
|
||||
{kopf}
|
||||
<Leerzustand
|
||||
icon={<KeyRound className="size-5" />}
|
||||
titel={stand.gesperrt.neuerPublicKeyNoetig ? "Neuer Public Key nötig" : "Nicht rotierbar"}
|
||||
text={stand.gesperrt.grund}
|
||||
aktion={<Button href={`/schluessel/${alt.id}`}>Zurück zum Schlüssel</Button>}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const widerruf =
|
||||
stand.neu && alt.widerrufEingeleitetAm && alt.status !== "ROTIERT"
|
||||
? await offeneEntfernungen([alt.id])
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-7">
|
||||
{kopf}
|
||||
<Rotation
|
||||
daten={{
|
||||
alt: { id: alt.id, name: alt.name },
|
||||
neu: stand.neu ? { id: stand.neu.id, name: stand.neu.name } : null,
|
||||
schritt: stand.schritt,
|
||||
neuOffen: stand.neu ? stand.neuOffen : 0,
|
||||
tests: stand.neu ? stand.tests : [],
|
||||
widerruf,
|
||||
darfPrivat: darf(benutzer.rolle, "privateKeyAusrollen"),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user