feat(schluessel): erzeugen, importieren, exportieren (#012)
Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub. Warum so: - Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil nur er den Master-Key hat. Passphrase und importierter Private Key reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach. - Ein reiner Public Key ist kein Geheimnis und wird direkt im Web angelegt, mit verstaendlicher Meldung bei einem Duplikat. - Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist, verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf Schluessel ausserhalb des Workers ein festes select. - Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das Loeschen des Private Keys; CHECKs fuer den Slug (landet in Dateinamen) und "geloescht nur ohne Chiffrat". - Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import "dotenv/config";
|
||||
import { schluesselpaarErzeugen } from "../../src/lib/ssh/openssh";
|
||||
|
||||
/**
|
||||
* Schluessel im gebauten Stand mit laufendem Worker (#012–#014). Testdaten heissen zz-e2e-…
|
||||
* und bleiben liegen (Schluessel werden nicht geloescht, nur spaeter widerrufen).
|
||||
*/
|
||||
test.setTimeout(90_000);
|
||||
|
||||
async function oidcAnmelden(page: Page, rolle: "admin" | "operator" | "reader") {
|
||||
await page.goto("/anmelden");
|
||||
await page.getByRole("link", { name: /Mit Mock-OIDC anmelden/ }).click();
|
||||
await page.locator(`button[data-rolle="${rolle}"]`).click();
|
||||
await page.getByRole("button", { name: "Anmelden", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible();
|
||||
}
|
||||
|
||||
function eindeutig(): string {
|
||||
return `zz-e2e-${Date.now().toString(36)}`;
|
||||
}
|
||||
|
||||
test("Operator erzeugt einen Schlüssel und landet im Detail", async ({ page }) => {
|
||||
const name = `${eindeutig()} Backup`;
|
||||
await oidcAnmelden(page, "operator");
|
||||
await page.goto("/schluessel/neu");
|
||||
await page.getByLabel("Zweck").fill(name);
|
||||
await page.getByLabel("Tags").fill("e2e");
|
||||
await page.getByRole("button", { name: "Schlüssel erzeugen" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page).toHaveURL(/\/schluessel\/[^/]+$/);
|
||||
await expect(page.getByText("verschlüsselt gespeichert")).toBeVisible();
|
||||
await expect(page.getByText("ed25519", { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("Public Key", { exact: true })).toContainText("ssh-ed25519 AAAA");
|
||||
await expect(page.getByLabel("Public Key", { exact: true })).toContainText(
|
||||
`sshkv:${name.toLowerCase().replace(/ /g, "-")}`,
|
||||
);
|
||||
|
||||
await page.goto(`/schluessel?q=${encodeURIComponent(name)}`);
|
||||
await expect(page.getByRole("link", { name })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Public Key importieren, Duplikat wird erkannt", async ({ page }) => {
|
||||
const name = `${eindeutig()} Laptop`;
|
||||
const paar = await schluesselpaarErzeugen("ED25519", { kommentar: "e2e@laptop" });
|
||||
await oidcAnmelden(page, "operator");
|
||||
await page.goto("/schluessel/neu");
|
||||
await page.getByRole("radio", { name: "Importieren" }).click();
|
||||
await page.getByLabel("Zweck").fill(name);
|
||||
await page.getByLabel("Public Key").fill(paar.publicKeyZeile);
|
||||
await page.locator(`input[name="kategorie"][value="PERSON"]`).check();
|
||||
await page.getByRole("button", { name: "Schlüssel importieren" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name })).toBeVisible();
|
||||
await expect(page.getByText("nicht gespeichert (Personen-Key)")).toBeVisible();
|
||||
await expect(page.getByText(paar.fingerprint)).toBeVisible();
|
||||
|
||||
await page.goto("/schluessel/neu");
|
||||
await page.getByRole("radio", { name: "Importieren" }).click();
|
||||
await page.getByLabel("Zweck").fill(`${name} nochmal`);
|
||||
await page.getByLabel("Public Key").fill(paar.publicKeyZeile);
|
||||
await page.getByRole("button", { name: "Schlüssel importieren" }).click();
|
||||
await expect(page.getByRole("alert").first()).toContainText(`bereits verwaltet: „${name}“`);
|
||||
});
|
||||
|
||||
test("Leser sieht Schlüssel, darf aber keine anlegen", async ({ page }) => {
|
||||
await oidcAnmelden(page, "reader");
|
||||
await page.goto("/schluessel");
|
||||
await expect(page.getByRole("heading", { name: "Schlüssel", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Schlüssel anlegen" })).toHaveCount(0);
|
||||
await page.goto("/schluessel/neu");
|
||||
await expect(page).toHaveURL(/\/kein-zugriff/);
|
||||
});
|
||||
Reference in New Issue
Block a user