feat(widerruf): Widerruf und Massenwiderruf mit Dry-Run (#024)
Ein Schlüssel soll nach Projektende mit einem Klick von allen Hosts verschwinden – aber nie als „widerrufen“ gelten, solange er irgendwo noch liegt. Deshalb ein Zwischenzustand (widerrufEingeleitetAm): Verwendungen werden zum Entfernen vorgemerkt, der bestehende Dry-Run mit Hash-Sperre zeigt und schreibt die Änderungen, und erst wenn keine Verwendung mehr offen ist, setzt der Abschluss WIDERRUFEN (bzw. ROTIERT für #023). Scheitert ein Host, bleibt der Schlüssel im Zwischenzustand und die Seite nennt die offenen Ziele. Zielkonten lassen sich optional mit entfernen, aber erst nach den Schlüsseln und nie, wenn ein anderer aktiver Schlüssel sie noch nutzt. Massenwiderruf (Admin, frische Bestätigung) nutzt einen gemeinsamen Dry-Run und protokolliert je Schlüssel. Abgelaufene Schlüssel erscheinen im Filter „abgelaufen“ und bieten den Widerruf an – entfernt wird nie automatisch. Migration 20261012130000_widerruf_eingeleitet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
anmelden,
|
||||
archivieren,
|
||||
aufZiel,
|
||||
hostEinrichten,
|
||||
kernAufbauen,
|
||||
PORT,
|
||||
zieleZuruecksetzen,
|
||||
} from "./hilfen";
|
||||
|
||||
/**
|
||||
* Widerruf im Browser (#024), gebauter Stand + Worker: Ausgangslage ist der Kern-Anwendungsfall
|
||||
* (Private Key auf A, Public Key auf B). „Widerrufen“ → Dialog → Dry-Run → anwenden → Status
|
||||
* „widerrufen“; danach kommt A nicht mehr nach B.
|
||||
*/
|
||||
test.setTimeout(240_000);
|
||||
|
||||
test.beforeAll(() => zieleZuruecksetzen());
|
||||
|
||||
test("Widerruf mit einem Klick", async ({ page }) => {
|
||||
const kennung = Date.now().toString(36);
|
||||
const hostA = `zz-e2e-a-${kennung}`;
|
||||
const hostB = `zz-e2e-b-${kennung}`;
|
||||
const zweck = `zz-e2e Widerruf ${kennung}`;
|
||||
|
||||
await anmelden(page);
|
||||
await hostEinrichten(page, hostA, PORT.a);
|
||||
await hostEinrichten(page, hostB, PORT.b);
|
||||
await kernAufbauen(page, zweck, hostA, hostB);
|
||||
await page.reload();
|
||||
|
||||
// Dialog: zeigt genau, was passiert.
|
||||
await page.getByRole("button", { name: "Widerrufen" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: `„${zweck}“ widerrufen?` });
|
||||
await expect(dialog.getByText(`root@${hostB}`)).toBeVisible();
|
||||
await expect(dialog.getByText(`root@${hostA}:~/.ssh/`)).toBeVisible();
|
||||
await expect(dialog.getByText(/config-Eintrag „Host zu-b“/)).toBeVisible();
|
||||
await page.screenshot({ path: "docs/anleitung/bilder/widerruf-desktop.png" });
|
||||
await dialog.getByRole("button", { name: "Widerruf vorbereiten" }).click();
|
||||
|
||||
// Zwischenzustand: noch nicht widerrufen, nichts mehr zuweisbar.
|
||||
await expect(page.getByRole("heading", { name: "Widerruf läuft" })).toBeVisible();
|
||||
await expect(page.getByText("Widerruf unvollständig")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Ausrollen" })).toHaveCount(0);
|
||||
|
||||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||||
await expect(page.getByText(`root@${hostB}:/root/.ssh/authorized_keys`)).toBeVisible({ timeout: 30_000 });
|
||||
await page.screenshot({ path: "docs/anleitung/bilder/widerruf-dry-run-desktop.png", fullPage: true });
|
||||
await page.getByRole("button", { name: /Änderung anwenden/ }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Widerrufen", exact: true })).toBeVisible({
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(page.getByText("Widerruf unvollständig")).toHaveCount(0);
|
||||
expect(aufZiel("ziel-a", "ls /root/.ssh")).not.toContain("id_ed25519");
|
||||
expect(() => aufZiel("ziel-a", "ssh -o BatchMode=yes -o ConnectTimeout=5 zu-b true")).toThrow();
|
||||
|
||||
await archivieren(page, hostA);
|
||||
await archivieren(page, hostB);
|
||||
});
|
||||
Reference in New Issue
Block a user