diff --git a/docs/anleitung/bilder/dry-run-desktop.png b/docs/anleitung/bilder/dry-run-desktop.png new file mode 100644 index 0000000..534a8d8 Binary files /dev/null and b/docs/anleitung/bilder/dry-run-desktop.png differ diff --git a/tests/e2e/ausrollen.spec.ts b/tests/e2e/ausrollen.spec.ts new file mode 100644 index 0000000..44ca83c --- /dev/null +++ b/tests/e2e/ausrollen.spec.ts @@ -0,0 +1,103 @@ +import { execFileSync } from "node:child_process"; +import { generateKeyPairSync, randomUUID } from "node:crypto"; +import { expect, test, type Page } from "@playwright/test"; +import pg from "pg"; +import "dotenv/config"; +import { fingerprintSha256 } from "../../src/lib/ssh/openssh"; + +/** + * Public Key ausrollen ueber die Hostseite (#016), gebauter Stand + Worker, Testziel B. + * Schluessel und Zuweisung legt der Test direkt in der Entwicklungsdatenbank an (Praefix zz-), + * damit er nur den Ausroll-Weg prueft. + */ +const ZIEL_PORT = (process.env["SSHKV_TEST_ZIEL_B"] ?? "127.0.0.1:2222").split(":")[1] ?? "2222"; +const ROOT_PASSWORT = process.env["SSHKV_TEST_ROOT_PASSWORT"] || "sshkv-test"; + +test.setTimeout(150_000); + +function aufZielB(befehl: string): string { + return execFileSync( + "docker", + ["compose", "-f", "docker-compose.test.yml", "exec", "-T", "ziel-b", "sh", "-c", befehl], + { + encoding: "utf8", + }, + ); +} + +function publicKey(kommentar: string) { + const { publicKey: schluessel } = generateKeyPairSync("ed25519"); + const roh = schluessel.export({ format: "der", type: "spki" }).subarray(-32); + const typ = Buffer.from("ssh-ed25519"); + const blob = Buffer.concat([Buffer.from([0, 0, 0, typ.length]), typ, Buffer.from([0, 0, 0, 32]), roh]); + return { + zeile: `ssh-ed25519 ${blob.toString("base64")} ${kommentar}`, + fingerprint: fingerprintSha256(blob), + }; +} + +async function anmelden(page: Page) { + await page.goto("/anmelden"); + await page.getByRole("link", { name: /Mit Mock-OIDC anmelden/ }).click(); + await page.locator('button[data-rolle="admin"]').click(); + await page.getByRole("button", { name: "Anmelden", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible(); +} + +test.beforeAll(() => { + aufZielB("userdel -r sshkv 2>/dev/null; rm -f /etc/sudoers.d/sshkv /root/.ssh/authorized_keys*; true"); +}); + +test("Public Key ueber die Hostseite ausrollen", async ({ page }) => { + const name = `zz-e2e-${Date.now().toString(36)}`; + await anmelden(page); + + // Host einrichten wie in host-einrichten.spec.ts. + await page.goto("/hosts/neu"); + await page.getByLabel("Name").fill(name); + await page.getByLabel("Adresse").fill("127.0.0.1"); + await page.getByLabel("Port").fill(ZIEL_PORT); + await page.getByRole("button", { name: "Host anlegen" }).click(); + await page.getByRole("button", { name: "Host-Key abrufen" }).click(); + await page.getByRole("button", { name: "Fingerprint stimmt – vertrauen" }).click({ timeout: 30_000 }); + await page.getByLabel("Passwort", { exact: true }).fill(ROOT_PASSWORT); + await page.getByRole("button", { name: "Zugang einrichten" }).click(); + await expect(page.getByRole("button", { name: "Verbindung prüfen" })).toBeVisible({ timeout: 60_000 }); + const hostId = page.url().split("/").pop()!; + + // Schluessel und Zuweisung direkt anlegen. + const key = publicKey(`sshkv:${name}`); + const client = new pg.Client({ connectionString: process.env["DATABASE_URL"] }); + await client.connect(); + try { + const schluesselId = randomUUID(); + await client.query( + `INSERT INTO "Schluessel" ("id","name","slug","typ","kategorie","publicKey","fingerprint","kommentar","geaendertAm") + VALUES ($1,$2,$3,'ED25519','DIENST',$4,$5,$6,now())`, + [schluesselId, `${name} Backup`, name, key.zeile, key.fingerprint, `sshkv:${name}`], + ); + await client.query( + `INSERT INTO "PublicVerwendung" ("id","schluesselId","hostId","zielbenutzer","optionen") VALUES ($1,$2,$3,'root',$4)`, + [randomUUID(), schluesselId, hostId, 'restrict,from="10.0.0.12"'], + ); + } finally { + await client.end(); + } + + await page.reload(); + await expect(page.getByText("1 Zuweisung(en) noch nicht ausgerollt.")).toBeVisible(); + await page.getByRole("button", { name: "Dry-Run starten" }).click(); + await expect(page.getByText(`root@${name}:/root/.ssh/authorized_keys`)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(`restrict,from="10.0.0.12" ${key.zeile}`)).toBeVisible(); + expect(aufZielB("cat /root/.ssh/authorized_keys 2>/dev/null || true")).not.toContain(key.fingerprint); + await page.screenshot({ path: "docs/anleitung/bilder/dry-run-desktop.png", fullPage: true }); + + await page.getByRole("button", { name: /Änderung anwenden/ }).click(); + await expect(page.getByText("Ausgerollt.")).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText("synchron").first()).toBeVisible(); + expect(aufZielB("cat /root/.ssh/authorized_keys")).toContain(`restrict,from="10.0.0.12" ${key.zeile}`); + + await page.getByRole("button", { name: "Host archivieren" }).click(); + await page.getByRole("dialog").getByRole("button", { name: "Archivieren", exact: true }).click(); + await expect(page).toHaveURL(/\/hosts$/); +});