feat(schluessel): SSH-Schlüssel hinterlegen statt in jedes Formular

Benutzer hinterlegen ihre öffentlichen Schlüssel einmal, Admins
zusätzlich Team-Schlüssel. Sie kommen in jeden Gast: im Assistenten
vorausgewählt und abwählbar, per API und CLI automatisch, wenn die
Anfrage keine nennt. Die Schlüssel kommen über die Schnittstelle
SchluesselQuelle – dort lässt sich später eine externe Verwaltung
anbinden.

Vorgezogen aus #015 auf die Frage, ob Schlüssel ins Template gehören:
nein, in die Verwaltung.

Refs #015

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 14:28:45 +02:00
co-authored by Claude Opus 5.5
parent 00712dc6ce
commit 426d3ec95a
18 changed files with 690 additions and 39 deletions
+49
View File
@@ -0,0 +1,49 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "@playwright/test";
import { anmelden, E2E_PRAEFIX, keinQuerscrollen } from "./helfer";
/** Je Lauf ein eigener Schlüssel – derselbe zweimal wäre für denselben Benutzer ein Duplikat. */
function neuerSchluessel(): string {
const ordner = mkdtempSync(join(tmpdir(), "pd-e2e-"));
execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "e2e@test", "-f", join(ordner, "k")]);
return readFileSync(join(ordner, "k.pub"), "utf8").trim();
}
test("SSH-Schlüssel hinterlegen, im Assistenten vorausgewählt, wieder entfernen", async ({ page }) => {
const name = `${E2E_PRAEFIX}-${test.info().project.name}`;
await anmelden(page, "/einstellungen");
// Reste eines abgebrochenen Laufs wegräumen.
for (const alt of (await (await page.request.get("/api/v1/schluessel")).json()) as Array<{
id: string;
name: string;
}>) {
if (alt.name.startsWith(E2E_PRAEFIX)) await page.request.delete(`/api/v1/schluessel/${alt.id}`);
}
await page.reload();
await page.getByLabel("Name").fill(name);
await page.getByLabel("Öffentlicher Schlüssel").fill("-----BEGIN OPENSSH PRIVATE KEY-----");
await page.getByRole("button", { name: "Hinzufügen" }).click();
await expect(page.getByText(/nie den privaten Schlüssel/)).toBeVisible();
await page.getByLabel("Öffentlicher Schlüssel").fill(neuerSchluessel());
await page.getByRole("button", { name: "Hinzufügen" }).click();
await expect(page.getByText(name)).toBeVisible();
await keinQuerscrollen(page);
await page.goto("/ausrollen");
if (await page.getByText("Erst einen Server anlegen").isVisible())
test.skip(true, "Kein getesteter Server vorhanden");
for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "Weiter" }).click();
await page.getByLabel("Name (Hostname)").fill("pd-e2e");
for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "Weiter" }).click();
await expect(page.getByLabel(name)).toBeChecked();
// Mit hinterlegtem Schlüssel geht es ohne Eingabe weiter.
await expect(page.getByRole("button", { name: "Weiter" })).toBeEnabled();
await page.goto("/einstellungen");
page.once("dialog", (d) => void d.accept());
await page.getByRole("button", { name: `Schlüssel ${name} entfernen` }).click();
await expect(page.getByText(name)).toHaveCount(0);
});