feat(deploymentkey): Rotation vor dem Umstellen abbrechen (#036)

Eine begonnene Rotation ließ sich nicht zurücknehmen; nahm ein Host den
neuen Key dauerhaft nicht an, blieb nur das Übergehen. Abbrechen
verwirft jetzt den neuen Key samt Private Key, ein Dry-Run nimmt ihn
wieder von den Hosts, und eine neue Rotation kann sofort starten.

Nebenbei: Aktionen der Benutzerverwaltung (#035) bekommen im Audit-Log
eine eigene Gruppe, statt den Filter „Anmeldung“ zu verwässern; der
E2E-Test für lokale Konten hängt nicht mehr von der Reihenfolge ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 22:52:32 +02:00
co-authored by Claude Opus 5.5
parent 68dbc7cadb
commit 5d03681f3c
11 changed files with 160 additions and 13 deletions
+30 -1
View File
@@ -41,7 +41,7 @@ beforeEach(async () => {
AKTEUR.id = (await prisma.benutzer.create({ data: { name: "Test-Admin", rolle: "ADMIN" } })).id;
});
const schritt = (s: "erzeugen" | "pruefen" | "abschliessen") =>
const schritt = (s: "erzeugen" | "pruefen" | "abbrechen" | "abschliessen") =>
vorgangLaufen("ROTATION", { eingabe: { schritt: s }, angefordertVonId: AKTEUR.id });
async function trockenlaufUndAusrollen() {
@@ -136,6 +136,35 @@ describe("Rotation des Deployment-Keys", () => {
expect(await prisma.abweichung.count({ where: { hostId: a.id, behobenAm: null } })).toBe(0);
});
it("bricht vor dem Umstellen ab und laesst danach eine neue Rotation zu (#036)", async () => {
const a = await eingerichteterHost(zielA, "ziel-a");
const alt = await prisma.schluessel.findFirstOrThrow({ where: { instanzStandard: true } });
expect((await schritt("erzeugen")).status).toBe("ERLEDIGT");
const neu = (await rotationsStand()).nachfolger!;
await trockenlaufUndAusrollen();
expect(await fingerprintsAuf(a)).toContain(neu.fingerprint);
expect((await schritt("abbrechen")).status).toBe("ERLEDIGT");
const verworfen = await prisma.schluessel.findUniqueOrThrow({ where: { id: neu.id } });
expect(verworfen).toMatchObject({ status: "WIDERRUFEN", privatVerschluesselt: null });
expect(await rotationsStand()).toMatchObject({ phase: "bereit", abgebrochen: { id: neu.id } });
// Ohne laufende Rotation gibt es nichts mehr abzubrechen.
expect((await schritt("abbrechen")).status).toBe("FEHLGESCHLAGEN");
// Der Dry-Run nimmt den verworfenen Key wieder heraus; der alte arbeitet unveraendert.
await trockenlaufUndAusrollen();
const fingerprints = await fingerprintsAuf(a);
expect(fingerprints).toContain(alt.fingerprint);
expect(fingerprints).not.toContain(neu.fingerprint);
expect(await anmeldungKlappt(a, alt.id)).toBe(true);
// Eine neue Rotation beginnt von vorn.
expect((await schritt("erzeugen")).status).toBe("ERLEDIGT");
const zweiter = (await rotationsStand()).nachfolger!;
expect(zweiter.id).not.toBe(neu.id);
expect(zweiter.vorgaengerId).toBe(alt.id);
});
it("laesst Hosts mit eigenem Deployment-Key aus", async () => {
const a = await eingerichteterHost(zielA, "ziel-a");
const b = await eingerichteterHost(zielB, "ziel-b");