Merge branch 'phase/036-deploymentkey-abbrechen'
This commit is contained in:
@@ -12,12 +12,14 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
- Laufende Schlüsselrotation abbrechen – der Nachfolger wird per Dry-Run wieder entfernt (#032)
|
- Laufende Schlüsselrotation abbrechen – der Nachfolger wird per Dry-Run wieder entfernt (#032)
|
||||||
- „Zum Dry-Run“ in der Zuweisungsmatrix springt direkt zur Dry-Run-Karte (#032)
|
- „Zum Dry-Run“ in der Zuweisungsmatrix springt direkt zur Dry-Run-Karte (#032)
|
||||||
- Nach einer abgebrochenen Rotation lässt sich derselbe Schlüssel erneut rotieren (#033)
|
- Nach einer abgebrochenen Rotation lässt sich derselbe Schlüssel erneut rotieren (#033)
|
||||||
|
- Rotation des Deployment-Keys lässt sich vor dem Umstellen abbrechen; der neue Key wird verworfen und per Dry-Run wieder entfernt (#036)
|
||||||
- Lokale Konten mit Passwort und Pflicht-TOTP neben oder statt authentik: Admins legen sie unter Einstellungen → Benutzer an (Einmal-Passwort, Rollen, Sperren, Zurücksetzen); abschaltbar mit `SSHKV_LOKALE_KONTEN=0` (#035)
|
- Lokale Konten mit Passwort und Pflicht-TOTP neben oder statt authentik: Admins legen sie unter Einstellungen → Benutzer an (Einmal-Passwort, Rollen, Sperren, Zurücksetzen); abschaltbar mit `SSHKV_LOKALE_KONTEN=0` (#035)
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
- Protokoll-IDs werden lückenlos vergeben (#032)
|
- Protokoll-IDs werden lückenlos vergeben (#032)
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
- Aktionen der Benutzerverwaltung erscheinen im Audit-Log unter einer eigenen Gruppe „Benutzer“ statt unter „Anmeldung“ (#035)
|
||||||
- Massenwiderruf lehnte im Browser erzeugte Schlüssel ab (IDs mit Bindestrich) (#032)
|
- Massenwiderruf lehnte im Browser erzeugte Schlüssel ab (IDs mit Bindestrich) (#032)
|
||||||
- Hinweis nach verweigerter Bestätigung nannte beim Massenwiderruf den falschen Knopf (#032)
|
- Hinweis nach verweigerter Bestätigung nannte beim Massenwiderruf den falschen Knopf (#032)
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ auswählen (nur Admin).
|
|||||||
**erzeugen**, per Dry-Run auf alle Hosts **verteilen** (er steht dann neben dem alten), die Anmeldung
|
**erzeugen**, per Dry-Run auf alle Hosts **verteilen** (er steht dann neben dem alten), die Anmeldung
|
||||||
mit dem neuen Key **prüfen**, **umstellen** und zuletzt den alten per Dry-Run **aufräumen**. Scheitert
|
mit dem neuen Key **prüfen**, **umstellen** und zuletzt den alten per Dry-Run **aufräumen**. Scheitert
|
||||||
die Prüfung auf einem Host, lässt er sich bewusst übergehen; sshkv erreicht ihn danach nur noch nach
|
die Prüfung auf einem Host, lässt er sich bewusst übergehen; sshkv erreicht ihn danach nur noch nach
|
||||||
einem neuen Bootstrap.
|
einem neuen Bootstrap. Solange noch nicht umgestellt ist, lässt sich die
|
||||||
|
Rotation **abbrechen**: Der neue Key wird verworfen, ein Dry-Run nimmt ihn wieder von den Hosts.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# #036 Rotation des Deployment-Keys abbrechen
|
||||||
|
**Status:** erledigt
|
||||||
|
**Meilenstein:** v1.0.0
|
||||||
|
**Labels:** sicherheit, backend
|
||||||
|
**Geschätzt:** 0,5 h **Tatsächlich:** 0 h 25 min
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Eine begonnene Rotation des Deployment-Keys (#031) zurücknehmen können, etwa wenn ein Host den neuen Key dauerhaft nicht annimmt – bisher blieb nur, Hosts beim Umstellen zu übergehen.
|
||||||
|
|
||||||
|
## Aufgaben
|
||||||
|
- [x] Worker-Schritt `abbrechen`: Nachfolger WIDERRUFEN, Private Key sofort verwerfen, protokollieren
|
||||||
|
- [x] Nur vor dem Umstellen; danach meldet sich sshkv schon mit dem neuen Key an
|
||||||
|
- [x] Seite: Knopf „Rotation abbrechen“, danach Karte „Abgebrochene Rotation aufräumen“ mit Dry-Run
|
||||||
|
- [x] Neue Rotation sofort möglich (partieller Index aus #033)
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
- [x] Integrationstest: verteilen → abbrechen → Dry-Run entfernt den neuen Key, alter arbeitet weiter → neue Rotation startet
|
||||||
|
- [x] E2E: abbrechen und aufräumen im Browser; nimmt auch eine liegengebliebene Rotation mit
|
||||||
|
|
||||||
|
## Abhängigkeiten
|
||||||
|
#031, #033
|
||||||
|
|
||||||
|
## Notizen / Entscheidungen
|
||||||
|
- Gefunden, weil ein fehlgeschlagener E2E-Lauf eine Rotation auf verwaisten Testhosts hängen ließ: Das Umstellen verweigerte sich zu Recht, aber es gab keinen Weg zurück.
|
||||||
|
- Der verworfene Key bleibt bis zum Dry-Run in den Blöcken stehen (harmlos – sein Private Key existiert nicht mehr); die Aufräum-Karte zeigt das.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CheckCheck, KeyRound, ShieldCheck } from "lucide-react";
|
import { CheckCheck, KeyRound, ShieldCheck, Undo2 } from "lucide-react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AusrollenPanel } from "@/components/ausrollen/AusrollenPanel";
|
import { AusrollenPanel } from "@/components/ausrollen/AusrollenPanel";
|
||||||
@@ -81,6 +81,16 @@ export default async function DeploymentKeySeite() {
|
|||||||
)}
|
)}
|
||||||
</Karte>
|
</Karte>
|
||||||
|
|
||||||
|
{stand.phase === "bereit" && stand.abgebrochen && (
|
||||||
|
<Karte
|
||||||
|
titel="Abgebrochene Rotation aufräumen"
|
||||||
|
untertitel="Der verworfene Key kann noch in den Blöcken stehen. Ein Dry-Run zeigt es; steht alles auf Soll, ist nichts mehr zu tun."
|
||||||
|
>
|
||||||
|
<p className="text-muted mb-4 font-mono text-[13px] break-all">{stand.abgebrochen.fingerprint}</p>
|
||||||
|
<AusrollenPanel auswahl={{ deploymentKey: true }} offen={0} />
|
||||||
|
</Karte>
|
||||||
|
)}
|
||||||
|
|
||||||
{stand.phase === "bereit" && stand.aktuell && (
|
{stand.phase === "bereit" && stand.aktuell && (
|
||||||
<Karte
|
<Karte
|
||||||
titel="1 · Neuen Key erzeugen"
|
titel="1 · Neuen Key erzeugen"
|
||||||
@@ -136,6 +146,18 @@ export default async function DeploymentKeySeite() {
|
|||||||
<Umstellen fehlgeschlagen={fehlgeschlagen} />
|
<Umstellen fehlgeschlagen={fehlgeschlagen} />
|
||||||
</Karte>
|
</Karte>
|
||||||
)}
|
)}
|
||||||
|
<Karte
|
||||||
|
titel="Rotation abbrechen"
|
||||||
|
untertitel="Solange nicht umgestellt ist: der neue Key wird verworfen, der alte bleibt unverändert. Danach per Dry-Run den neuen wieder von den Hosts entfernen."
|
||||||
|
className="border-danger-border"
|
||||||
|
>
|
||||||
|
<VorgangsKnopf
|
||||||
|
aktion={rotationsSchrittAktion.bind(null, "abbrechen")}
|
||||||
|
beschriftung="Rotation abbrechen"
|
||||||
|
variante="gefahr"
|
||||||
|
icon={<Undo2 className="size-4" />}
|
||||||
|
/>
|
||||||
|
</Karte>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const AKTIONEN: Readonly<Record<string, Beschreibung>> = {
|
|||||||
"deploymentkey.erzeugt": a("Deployment-Key erzeugt", "schluessel"),
|
"deploymentkey.erzeugt": a("Deployment-Key erzeugt", "schluessel"),
|
||||||
"deploymentkey.geprueft": a("Neuer Deployment-Key geprüft", "widerruf", false),
|
"deploymentkey.geprueft": a("Neuer Deployment-Key geprüft", "widerruf", false),
|
||||||
"deploymentkey.umgestellt": a("Deployment-Key umgestellt", "widerruf"),
|
"deploymentkey.umgestellt": a("Deployment-Key umgestellt", "widerruf"),
|
||||||
|
"deploymentkey.abgebrochen": a("Rotation des Deployment-Keys abgebrochen", "widerruf"),
|
||||||
"deploymentkey.abgeschlossen": a("Rotation des Deployment-Keys abgeschlossen", "widerruf"),
|
"deploymentkey.abgeschlossen": a("Rotation des Deployment-Keys abgeschlossen", "widerruf"),
|
||||||
|
|
||||||
// Widerruf und Rotation (#023, #024)
|
// Widerruf und Rotation (#023, #024)
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ function alsAntwort(fehler: unknown): { fehler: string } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function rotationsSchrittAktion(
|
export async function rotationsSchrittAktion(
|
||||||
schritt: "erzeugen" | "pruefen" | "abschliessen",
|
schritt: "erzeugen" | "pruefen" | "abbrechen" | "abschliessen",
|
||||||
): Promise<VorgangsAntwort> {
|
): Promise<VorgangsAntwort> {
|
||||||
try {
|
try {
|
||||||
const { benutzer } = await rechtErforderlich("deploymentKeyVerwalten");
|
const { benutzer } = await rechtErforderlich("deploymentKeyVerwalten");
|
||||||
const geprueft = z.enum(["erzeugen", "pruefen", "abschliessen"]).parse(schritt);
|
const geprueft = z.enum(["erzeugen", "pruefen", "abbrechen", "abschliessen"]).parse(schritt);
|
||||||
return { vorgangId: await rotationsSchrittAnfordern(geprueft, akteurVon(benutzer)) };
|
return { vorgangId: await rotationsSchrittAnfordern(geprueft, akteurVon(benutzer)) };
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
return alsAntwort(fehler);
|
return alsAntwort(fehler);
|
||||||
|
|||||||
@@ -85,11 +85,21 @@ export async function rotationsStand() {
|
|||||||
: vorgaenger
|
: vorgaenger
|
||||||
? "aufraeumen"
|
? "aufraeumen"
|
||||||
: "bereit";
|
: "bereit";
|
||||||
return { aktuell, nachfolger, vorgaenger, phase, hosts, pruefung };
|
// Zuletzt abgebrochener Nachfolger des aktuellen Keys (#036): kann noch in Bloecken stehen,
|
||||||
|
// bis ein Dry-Run ihn entfernt.
|
||||||
|
const abgebrochen =
|
||||||
|
phase === "bereit" && aktuell
|
||||||
|
? await prisma.schluessel.findFirst({
|
||||||
|
where: { kategorie: "DEPLOYMENT", vorgaengerId: aktuell.id, status: "WIDERRUFEN" },
|
||||||
|
orderBy: { angelegtAm: "desc" },
|
||||||
|
select: { id: true, fingerprint: true },
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
return { aktuell, nachfolger, vorgaenger, phase, hosts, pruefung, abgebrochen };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rotationsSchrittAnfordern(
|
export async function rotationsSchrittAnfordern(
|
||||||
schritt: "erzeugen" | "pruefen" | "abschliessen",
|
schritt: "erzeugen" | "pruefen" | "abbrechen" | "abschliessen",
|
||||||
akteur: Akteur,
|
akteur: Akteur,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const stand = await rotationsStand();
|
const stand = await rotationsStand();
|
||||||
@@ -98,6 +108,9 @@ export async function rotationsSchrittAnfordern(
|
|||||||
}
|
}
|
||||||
if (schritt === "pruefen" && !stand.nachfolger)
|
if (schritt === "pruefen" && !stand.nachfolger)
|
||||||
throw new DeploymentKeyFehler("Es gibt keinen neuen Deployment-Key zu prüfen.");
|
throw new DeploymentKeyFehler("Es gibt keinen neuen Deployment-Key zu prüfen.");
|
||||||
|
// Abbrechen nur vor dem Umstellen – danach meldet sich sshkv schon mit dem neuen Key an.
|
||||||
|
if (schritt === "abbrechen" && !stand.nachfolger)
|
||||||
|
throw new DeploymentKeyFehler("Es läuft keine Rotation, die sich abbrechen ließe.");
|
||||||
if (schritt === "abschliessen" && !stand.vorgaenger)
|
if (schritt === "abschliessen" && !stand.vorgaenger)
|
||||||
throw new DeploymentKeyFehler("Es gibt nichts abzuschließen.");
|
throw new DeploymentKeyFehler("Es gibt nichts abzuschließen.");
|
||||||
return vorgangEinreihen({ art: "ROTATION", angefordertVonId: akteur.id, eingabe: { schritt } });
|
return vorgangEinreihen({ art: "ROTATION", angefordertVonId: akteur.id, eingabe: { schritt } });
|
||||||
|
|||||||
@@ -95,6 +95,30 @@ async function pruefen(angefordertVonId: string | null) {
|
|||||||
return { ergebnis: { hosts } };
|
return { ergebnis: { hosts } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotation abbrechen (#036), nur vor dem Umstellen: der Nachfolger wird widerrufen und sein
|
||||||
|
* Private Key sofort verworfen – sshkv hat sich mit ihm nie produktiv angemeldet. Aus den
|
||||||
|
* Bloecken verschwindet er beim naechsten Dry-Run, weil `sollFuer()` nur aktive Nachfolger kennt.
|
||||||
|
*/
|
||||||
|
async function abbrechen(angefordertVonId: string | null) {
|
||||||
|
const stand = await rotationsStand();
|
||||||
|
if (!stand.nachfolger) throw new VorgangsFehler("Es läuft keine Rotation, die sich abbrechen ließe.");
|
||||||
|
await prisma.schluessel.update({
|
||||||
|
where: { id: stand.nachfolger.id },
|
||||||
|
data: { status: "WIDERRUFEN", widerrufEingeleitetAm: new Date() },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
await privateKeyVerwerfen(stand.nachfolger.id);
|
||||||
|
await protokollieren({
|
||||||
|
akteur: await akteurVon(angefordertVonId),
|
||||||
|
aktion: "deploymentkey.abgebrochen",
|
||||||
|
ergebnis: "ERFOLG",
|
||||||
|
fingerprint: stand.nachfolger.fingerprint,
|
||||||
|
details: { aktuell: stand.aktuell?.fingerprint ?? null },
|
||||||
|
});
|
||||||
|
return { ergebnis: { fingerprint: stand.nachfolger.fingerprint } };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prueft auf jedem Instanz-Host, dass der alte Key nicht mehr im Block steht, und verwirft
|
* Prueft auf jedem Instanz-Host, dass der alte Key nicht mehr im Block steht, und verwirft
|
||||||
* erst dann seinen Private Key. Nicht erreichbare Hosts verhindern den Abschluss nicht – sie
|
* erst dann seinen Private Key. Nicht erreichbare Hosts verhindern den Abschluss nicht – sie
|
||||||
@@ -143,6 +167,7 @@ export const deploymentRotationHandler: Handler = async ({ vorgang }) => {
|
|||||||
const schritt = (vorgang.eingabe as { schritt?: unknown } | null)?.schritt;
|
const schritt = (vorgang.eingabe as { schritt?: unknown } | null)?.schritt;
|
||||||
if (schritt === "erzeugen") return erzeugen(vorgang.angefordertVonId);
|
if (schritt === "erzeugen") return erzeugen(vorgang.angefordertVonId);
|
||||||
if (schritt === "pruefen") return pruefen(vorgang.angefordertVonId);
|
if (schritt === "pruefen") return pruefen(vorgang.angefordertVonId);
|
||||||
|
if (schritt === "abbrechen") return abbrechen(vorgang.angefordertVonId);
|
||||||
if (schritt === "abschliessen") return abschliessen(vorgang.angefordertVonId);
|
if (schritt === "abschliessen") return abschliessen(vorgang.angefordertVonId);
|
||||||
throw new VorgangsFehler("Unbekannter Schritt der Rotation.");
|
throw new VorgangsFehler("Unbekannter Schritt der Rotation.");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,39 @@ async function bestaetigen(page: Page) {
|
|||||||
await expect(page.getByRole("heading", { name: "Deployment-Key rotieren" })).toBeVisible();
|
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 }) => {
|
test("Deployment-Key rotieren", async ({ page }) => {
|
||||||
const kennung = Date.now().toString(36);
|
const kennung = Date.now().toString(36);
|
||||||
const hostA = `zz-e2e-a-${kennung}`;
|
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 page.getByRole("link", { name: "Konten verwalten" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Benutzer", exact: true })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Benutzer", exact: true })).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel("Anmeldename").fill(anmeldename);
|
// Frische Bestaetigung immer selbst holen – ob ein frueherer Test sie schon gesetzt hat,
|
||||||
await page.getByLabel("Name", { exact: true }).fill("E2E Lokal");
|
// haengt von der Reihenfolge ab. Dass das Recht sie verlangt, pruefen die Unit-Tests.
|
||||||
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();
|
|
||||||
await page.goto("/bestaetigen?ziel=%2Feinstellungen%2Fbenutzer");
|
await page.goto("/bestaetigen?ziel=%2Feinstellungen%2Fbenutzer");
|
||||||
await oidcBestaetigen(page);
|
await oidcBestaetigen(page);
|
||||||
await expect(page).toHaveURL(/\/einstellungen\/benutzer$/);
|
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;
|
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 });
|
vorgangLaufen("ROTATION", { eingabe: { schritt: s }, angefordertVonId: AKTEUR.id });
|
||||||
|
|
||||||
async function trockenlaufUndAusrollen() {
|
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);
|
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 () => {
|
it("laesst Hosts mit eigenem Deployment-Key aus", async () => {
|
||||||
const a = await eingerichteterHost(zielA, "ziel-a");
|
const a = await eingerichteterHost(zielA, "ziel-a");
|
||||||
const b = await eingerichteterHost(zielB, "ziel-b");
|
const b = await eingerichteterHost(zielB, "ziel-b");
|
||||||
|
|||||||
Reference in New Issue
Block a user