import { expect, test, type Page } from "@playwright/test"; import pg from "pg"; import "dotenv/config"; import { base32Dekodieren, totpCode, totpSchritt } from "../../src/lib/totp"; /** * Anmeldung im gebauten Stand (#004): OIDC ueber den Mock-Anbieter und Notfallzugang mit * TOTP-Einrichtung. Laeuft gegen die Entwicklungsdatenbank und setzt dort vorher das TOTP * des Notfall-Admins zurueck – deshalb nie gegen einen Betrieb richten. */ const NOTFALL_PASSWORT = process.env["SSHKV_BREAKGLASS_PASSWORD"] ?? ""; async function datenbank(arbeit: (client: pg.Client) => Promise): Promise { const client = new pg.Client({ connectionString: process.env["DATABASE_URL"] }); await client.connect(); try { return await arbeit(client); } finally { await client.end(); } } async function oidcAnmelden(page: Page, rolle: "admin" | "operator" | "reader" | "ohne") { await page.goto("/"); await expect(page).toHaveURL(/\/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(); } test.beforeAll(async () => { await datenbank(async (client) => { await client.query( `UPDATE "Benutzer" SET "totpGeheimnis" = NULL, "totpEingerichtetAm" = NULL, "totpLetzterSchritt" = NULL WHERE "istNotfall"`, ); await client.query(`DELETE FROM "Ratenbegrenzung"`); }); }); test("ohne Sitzung geht es zur Anmeldung, mit Ziel", async ({ page }) => { await page.goto("/hosts"); await expect(page).toHaveURL(/\/anmelden\?ziel=%2Fhosts/); await expect(page.getByRole("heading", { name: "Anmelden" })).toBeVisible(); }); test("OIDC-Admin landet in der Übersicht und kann sich abmelden", async ({ page }) => { await oidcAnmelden(page, "admin"); await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible(); await expect(page.getByText("Admin (Mock)")).toBeVisible(); await expect(page.getByText("Admin · über Mock-OIDC")).toBeVisible(); await page.getByRole("button", { name: "Abmelden" }).click(); await expect(page.getByText("Sie wurden abgemeldet.")).toBeVisible(); await page.goto("/"); await expect(page).toHaveURL(/\/anmelden/); }); test("OIDC ohne sshkv-Gruppe wird abgewiesen", async ({ page }) => { await oidcAnmelden(page, "ohne"); await expect(page).toHaveURL(/fehler=keine-gruppe/); await expect(page.getByText(/keiner SSHKeyVault-Gruppe zugeordnet/)).toBeVisible(); }); test("Notfall-Admin richtet TOTP ein und bestätigt sich erneut", async ({ page }) => { test.skip(NOTFALL_PASSWORT.length < 16, "SSHKV_BREAKGLASS_PASSWORD fehlt in .env"); await page.goto("/anmelden"); await page.getByText("Notfallzugang", { exact: true }).click(); await page.getByLabel("Anmeldename").fill("notfall"); await page.getByLabel("Passwort").fill(NOTFALL_PASSWORT); await page.getByRole("button", { name: "Mit Notfallzugang anmelden" }).click(); await expect(page).toHaveURL(/\/anmelden\/totp/); await expect(page.getByRole("img", { name: /QR-Code/ })).toBeVisible(); const geheimnis = ((await page.locator("code").first().textContent()) ?? "").trim(); expect(geheimnis).toMatch(/^[A-Z2-7]{32}$/); const code = () => totpCode(base32Dekodieren(geheimnis), totpSchritt(Date.now())); await page.getByLabel("Code zur Bestätigung").fill("000000"); await page.getByRole("button", { name: /TOTP einrichten/ }).click(); await expect(page.getByText(/Der Code passt nicht/)).toBeVisible(); await page.getByLabel("Code zur Bestätigung").fill(code()); await page.getByRole("button", { name: /TOTP einrichten/ }).click(); await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible(); await expect(page.getByText("Admin · über Notfallzugang")).toBeVisible(); // Erneute Bestaetigung: derselbe Code gilt nicht noch einmal, also auf den naechsten // Zeitschritt warten. await page.goto("/bestaetigen?ziel=%2Fhosts"); const schritt = totpSchritt(Date.now()); await expect .poll(() => totpSchritt(Date.now()), { timeout: 35_000, intervals: [1000] }) .toBeGreaterThan(schritt); await page.getByLabel("Passwort").fill(NOTFALL_PASSWORT); await page.getByLabel("Code aus der Authenticator-App").fill(code()); await page.getByRole("button", { name: "Bestätigen" }).click(); await expect(page).toHaveURL(/\/hosts$/); });