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:
@@ -19,6 +19,39 @@ async function bestaetigen(page: Page) {
|
||||
await expect(page.getByRole("heading", { name: "Deployment-Key rotieren" })).toBeVisible();
|
||||
}
|
||||
|
||||
test("Rotation des Deployment-Keys abbrechen und aufräumen (#036)", async ({ page }) => {
|
||||
const kennung = Date.now().toString(36);
|
||||
const hostA = `zz-e2e-a-${kennung}`;
|
||||
await anmelden(page);
|
||||
await hostEinrichten(page, hostA, PORT.a);
|
||||
await page.goto("/deployment-key");
|
||||
await bestaetigen(page);
|
||||
|
||||
// Eine liegengebliebene Rotation (etwa aus einem abgebrochenen Testlauf) wird mit abgebrochen.
|
||||
const erzeugen = page.getByRole("button", { name: "Neuen Deployment-Key erzeugen" });
|
||||
if (await erzeugen.isVisible()) {
|
||||
await erzeugen.click();
|
||||
await expect(page.getByText("Neuer Key (noch nicht Standard)")).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||||
await page.getByRole("button", { name: /Änderung anwenden/ }).click({ timeout: 30_000 });
|
||||
await expect(page.getByText("Ausgerollt.", { exact: true })).toBeVisible({ timeout: 60_000 });
|
||||
expect(aufZiel("ziel-a", "grep -c sshkv-deployment /home/sshkv/.ssh/authorized_keys").trim()).toBe("2");
|
||||
|
||||
await page.getByRole("button", { name: "Rotation abbrechen" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Abgebrochene Rotation aufräumen" })).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||||
await page.getByRole("button", { name: /Änderung anwenden/ }).click({ timeout: 30_000 });
|
||||
await expect(page.getByText("Ausgerollt.", { exact: true })).toBeVisible({ timeout: 60_000 });
|
||||
expect(aufZiel("ziel-a", "grep -c sshkv-deployment /home/sshkv/.ssh/authorized_keys").trim()).toBe("1");
|
||||
// Ein neuer Anlauf ist sofort moeglich.
|
||||
await expect(page.getByRole("button", { name: "Neuen Deployment-Key erzeugen" })).toBeVisible();
|
||||
|
||||
await archivieren(page, hostA);
|
||||
});
|
||||
|
||||
test("Deployment-Key rotieren", async ({ page }) => {
|
||||
const kennung = Date.now().toString(36);
|
||||
const hostA = `zz-e2e-a-${kennung}`;
|
||||
|
||||
@@ -17,12 +17,8 @@ test("lokales Konto anlegen, erste Anmeldung, sperren", async ({ page, browser }
|
||||
await page.getByRole("link", { name: "Konten verwalten" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Benutzer", exact: true })).toBeVisible();
|
||||
|
||||
await page.getByLabel("Anmeldename").fill(anmeldename);
|
||||
await page.getByLabel("Name", { exact: true }).fill("E2E Lokal");
|
||||
await page.getByLabel("Rolle", { exact: true }).selectOption("OPERATOR");
|
||||
await page.getByRole("button", { name: "Konto anlegen" }).click();
|
||||
// Ohne frische Bestaetigung lehnt der Server ab; danach klappt es.
|
||||
await expect(page.getByText(/erneute Bestätigung/)).toBeVisible();
|
||||
// Frische Bestaetigung immer selbst holen – ob ein frueherer Test sie schon gesetzt hat,
|
||||
// haengt von der Reihenfolge ab. Dass das Recht sie verlangt, pruefen die Unit-Tests.
|
||||
await page.goto("/bestaetigen?ziel=%2Feinstellungen%2Fbenutzer");
|
||||
await oidcBestaetigen(page);
|
||||
await expect(page).toHaveURL(/\/einstellungen\/benutzer$/);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user