Auf der Seite eines Schluessels: Public Key einem Host oder einer Gruppe zuweisen, Private Key (nur Admin) auf einen Quell-Host legen - mit ~/.ssh/config-Eintrag und dem Host-Key des Ziels in known_hosts -, Dry-Run fuer alle Ziele, anwenden und die Verbindung von A nach B testen. Der Kern-Anwendungsfall laeuft damit im Browser durch (E2E). Der Dry-Run nimmt Private-Key-Ziele nur fuer Admins auf: sonst koennte ein Operator einen vorbereiteten Private Key ausrollen lassen. Die Vorschau zeigt je Datei die Aktion; der Inhalt eines Private Keys erreicht den Browser nie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
5.6 KiB
TypeScript
118 lines
5.6 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
||
import { expect, test, type Page } from "@playwright/test";
|
||
import "dotenv/config";
|
||
|
||
/**
|
||
* Kern-Anwendungsfall im Browser (#012, #016, #017, #020), gebauter Stand + Worker:
|
||
* Schluessel erzeugen → Private Key auf Ziel A (mit ~/.ssh/config und known_hosts) →
|
||
* Public Key auf Ziel B → Dry-Run → anwenden → Verbindung von A nach B testen.
|
||
* Beide Hosts heissen zz-e2e-… und werden am Ende archiviert.
|
||
*/
|
||
const ROOT_PASSWORT = process.env["SSHKV_TEST_ROOT_PASSWORT"] || "sshkv-test";
|
||
const PORT = {
|
||
a: (process.env["SSHKV_TEST_ZIEL_A"] ?? "127.0.0.1:2221").split(":")[1] ?? "2221",
|
||
b: (process.env["SSHKV_TEST_ZIEL_B"] ?? "127.0.0.1:2222").split(":")[1] ?? "2222",
|
||
};
|
||
|
||
test.setTimeout(240_000);
|
||
|
||
function aufZiel(ziel: "ziel-a" | "ziel-b", befehl: string): string {
|
||
return execFileSync(
|
||
"docker",
|
||
["compose", "-f", "docker-compose.test.yml", "exec", "-T", ziel, "sh", "-c", befehl],
|
||
{
|
||
encoding: "utf8",
|
||
},
|
||
);
|
||
}
|
||
|
||
async function hostEinrichten(page: Page, name: string, port: string): Promise<void> {
|
||
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(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 });
|
||
}
|
||
|
||
async function archivieren(page: Page, name: string): Promise<void> {
|
||
await page.goto("/hosts");
|
||
await page.getByRole("link", { name }).first().click();
|
||
await page.getByRole("button", { name: "Host archivieren" }).click();
|
||
await page.getByRole("dialog").getByRole("button", { name: "Archivieren", exact: true }).click();
|
||
await expect(page).toHaveURL(/\/hosts$/);
|
||
}
|
||
|
||
test.beforeAll(() => {
|
||
const reset = "userdel -r sshkv 2>/dev/null; rm -f /etc/sudoers.d/sshkv; rm -rf /root/.ssh; true";
|
||
aufZiel("ziel-a", reset);
|
||
aufZiel("ziel-b", reset);
|
||
});
|
||
|
||
test("Backup A → B: erzeugen, ausrollen, Verbindung testen", async ({ page }) => {
|
||
const kennung = Date.now().toString(36);
|
||
const hostA = `zz-e2e-a-${kennung}`;
|
||
const hostB = `zz-e2e-b-${kennung}`;
|
||
const zweck = `zz-e2e Backup ${kennung}`;
|
||
|
||
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();
|
||
|
||
await hostEinrichten(page, hostA, PORT.a);
|
||
await hostEinrichten(page, hostB, PORT.b);
|
||
|
||
// 1. Schluessel erzeugen
|
||
await page.goto("/schluessel/neu");
|
||
await page.getByLabel("Zweck").fill(zweck);
|
||
await page.getByRole("button", { name: "Schlüssel erzeugen" }).click();
|
||
await expect(page.getByRole("heading", { name: zweck })).toBeVisible({ timeout: 30_000 });
|
||
|
||
// 2a. Private Key auf A, mit config-Eintrag und Host-Key von B
|
||
await page.getByRole("button", { name: "Ausrollen" }).first().click();
|
||
const privat = page.getByRole("dialog", { name: "Private Key ausrollen" });
|
||
await privat.getByLabel("Quell-Host (bekommt den Private Key)").selectOption({ label: hostA });
|
||
await privat.getByLabel("Ziel-Host (für known_hosts)").selectOption({ label: hostB });
|
||
await privat.getByRole("textbox", { name: "Alias" }).fill("zu-b");
|
||
// A erreicht B im Docker-Netz unter dem Dienstnamen auf Port 22.
|
||
await privat.getByRole("textbox", { name: "HostName" }).fill("ziel-b");
|
||
await privat.getByRole("spinbutton", { name: "Port" }).fill("22");
|
||
await privat.getByRole("button", { name: "Zuweisen" }).click();
|
||
await expect(privat).toBeHidden();
|
||
await expect(page.getByText(`root@${hostA}`)).toBeVisible();
|
||
|
||
// 2b. Public Key auf B
|
||
await page.getByRole("button", { name: "Ausrollen" }).nth(1).click();
|
||
const oeffentlich = page.getByRole("dialog", { name: "Public Key zuweisen" });
|
||
await oeffentlich.getByLabel("Ziel", { exact: true }).selectOption({ label: hostB });
|
||
await oeffentlich.getByLabel(/Einschränkungen/).fill("no-pty,no-agent-forwarding");
|
||
await oeffentlich.getByRole("button", { name: "Zuweisen" }).click();
|
||
await expect(oeffentlich).toBeHidden();
|
||
await expect(page.getByText(`root@${hostB}`)).toBeVisible();
|
||
|
||
// 3. Dry-Run und anwenden
|
||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||
await expect(page.getByText(`root@${hostB}:/root/.ssh/authorized_keys`)).toBeVisible({ timeout: 30_000 });
|
||
await expect(page.getByText("Host zu-b").first()).toBeVisible();
|
||
// Kein Private Key im Browser.
|
||
expect(await page.content()).not.toContain("PRIVATE KEY");
|
||
await page.screenshot({ path: "docs/anleitung/bilder/kern-dry-run-desktop.png", fullPage: true });
|
||
await page.getByRole("button", { name: /Änderung anwenden/ }).click();
|
||
await expect(page.getByText("Ausgerollt.")).toBeVisible({ timeout: 60_000 });
|
||
|
||
// 4. Verbindungstest von A nach B
|
||
await page.getByRole("button", { name: "Verbindung testen" }).click();
|
||
await expect(page.getByText("Erledigt.")).toBeVisible({ timeout: 30_000 });
|
||
expect(aufZiel("ziel-a", "ssh -o BatchMode=yes zu-b id -un").trim()).toBe("root");
|
||
await page.screenshot({ path: "docs/anleitung/bilder/kern-ausgerollt-desktop.png", fullPage: true });
|
||
|
||
await archivieren(page, hostA);
|
||
await archivieren(page, hostB);
|
||
});
|