Files
sshkeyvault/tests/e2e/lokale-konten.spec.ts
T
florian.wildeandClaude Opus 5.5 4219701b29 feat(anmeldung): lokale Benutzerkonten mit Pflicht-TOTP (#035)
Ohne authentik gab es nur den Notfall-Admin – ein geteiltes Konto ohne
Rollen und ohne erkennbare Akteure im Audit-Log. Admins legen jetzt
lokale Konten an; sshkv erzeugt ein Einmal-Passwort, das bei der ersten
Anmeldung ersetzt wird, danach ist TOTP Pflicht.

Rollen, Sperren (beendet Sitzungen sofort) und Zurücksetzen unter
Einstellungen → Benutzer, nur für Admins mit frischer Bestätigung.
SSHKV_LOKALE_KONTEN=0 schaltet lokale Konten ab; der Notfall-Admin
bleibt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:36:25 +02:00

77 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from "@playwright/test";
import { base32Dekodieren, totpCode, totpSchritt } from "../../src/lib/totp";
import { anmelden, oidcBestaetigen } from "./hilfen";
/**
* Lokale Konten (#035) im gebauten Stand: Admin legt ein Konto an, die Person meldet sich zum
* ersten Mal an (eigenes Passwort, TOTP), der Admin sperrt sie – die Sitzung endet sofort.
*/
test.setTimeout(120_000);
test("lokales Konto anlegen, erste Anmeldung, sperren", async ({ page, browser }) => {
const anmeldename = `zz-e2e-${Date.now().toString(36)}`;
const passwort = "ein-eigenes-e2e-passwort";
await anmelden(page);
await page.goto("/einstellungen");
await page.getByRole("link", { name: "Konten verwalten" }).click();
await expect(page.getByRole("heading", { name: "Benutzer", exact: true })).toBeVisible();
await page.getByLabel("Anmeldename").fill(anmeldename);
await page.getByLabel("Name", { exact: true }).fill("E2E Lokal");
await page.getByLabel("Rolle", { exact: true }).selectOption("OPERATOR");
await page.getByRole("button", { name: "Konto anlegen" }).click();
// Ohne frische Bestaetigung lehnt der Server ab; danach klappt es.
await expect(page.getByText(/erneute Bestätigung/)).toBeVisible();
await page.goto("/bestaetigen?ziel=%2Feinstellungen%2Fbenutzer");
await oidcBestaetigen(page);
await expect(page).toHaveURL(/\/einstellungen\/benutzer$/);
await page.getByLabel("Anmeldename").fill(anmeldename);
await page.getByLabel("Name", { exact: true }).fill("E2E Lokal");
await page.getByLabel("Rolle", { exact: true }).selectOption("OPERATOR");
await page.getByRole("button", { name: "Konto anlegen" }).click();
const anzeige = page.getByRole("region", { name: `Einmal-Passwort für ${anmeldename}` });
const einmal = ((await anzeige.locator("code").textContent()) ?? "").trim();
expect(einmal).toMatch(/^[a-zA-Z2-9]{5}(-[a-zA-Z2-9]{5}){3}$/);
await page.screenshot({ path: "docs/anleitung/bilder/benutzer-desktop.png", fullPage: true });
// Zweiter Browser: die Person meldet sich zum ersten Mal an.
const kontext = await browser.newContext();
const person = await kontext.newPage();
await person.goto("/anmelden");
await person.getByText("Lokales Konto oder Notfallzugang", { exact: true }).click();
await person.getByLabel("Anmeldename").fill(anmeldename);
await person.getByLabel("Passwort").fill(einmal);
await person.getByRole("button", { name: "Mit Benutzername anmelden" }).click();
await expect(person).toHaveURL(/\/anmelden\/passwort/);
await person.getByLabel("Neues Passwort", { exact: true }).fill("zu-kurz");
await person.getByLabel("Neues Passwort wiederholen").fill("zu-kurz");
await person.getByRole("button", { name: "Passwort setzen" }).click();
await expect(person.getByText(/mindestens 12 Zeichen/).last()).toBeVisible();
await person.getByLabel("Neues Passwort", { exact: true }).fill(passwort);
await person.getByLabel("Neues Passwort wiederholen").fill(passwort);
await person.getByRole("button", { name: "Passwort setzen" }).click();
await expect(person).toHaveURL(/\/anmelden\/totp/);
const geheimnis = ((await person.locator("code").first().textContent()) ?? "").trim();
await person
.getByLabel("Code zur Bestätigung")
.fill(totpCode(base32Dekodieren(geheimnis), totpSchritt(Date.now())));
await person.getByRole("button", { name: /TOTP einrichten/ }).click();
await expect(person.getByRole("heading", { name: "Übersicht" })).toBeVisible();
await expect(person.getByText("Operator · über lokales Konto")).toBeVisible();
// Ein Operator verwaltet keine Benutzer.
await person.goto("/einstellungen/benutzer");
await expect(person).toHaveURL(/\/kein-zugriff/);
// Admin sperrt das Konto – die Sitzung der Person endet sofort.
await page.reload();
const zeile = page.getByRole("row").filter({ hasText: anmeldename });
await expect(zeile.getByText("Einrichtung offen")).toHaveCount(0);
await zeile.getByRole("button", { name: /Sperren/ }).click();
await expect(zeile.getByText("gesperrt", { exact: true })).toBeVisible();
await person.goto("/");
await expect(person).toHaveURL(/\/anmelden/);
await kontext.close();
});