Files
serverAppConfigs/tests/e2e/specs/11-admin.spec.ts
T
Frank 5a083eee6c test(e2e): prove guard chain blocks direct navigation and admins keep admin reach
14-guard.spec.ts: every authentik-guarded vhost (www, search, romm, files,
seerr, grafana, prometheus, loki) must challenge an unauthenticated direct
navigation; plex is asserted exempt (PMS answers directly, no IdP form).
11-admin.spec.ts: admin direct-nav to grafana/prometheus/loki passes through
with host preserved and no uidField challenge.
2026-09-03 16:16:11 -07:00

70 lines
3.2 KiB
TypeScript

import { expect, test } from "@playwright/test";
// The admin panel as a homepage-admins member. The 200 itself is the RBAC
// allow-leg: the same path returns 403 for the standard user (10-homepage).
test("admin panel renders admin-category services", async ({ page }) => {
await page.goto("/admin/");
await expect(page).toHaveTitle(/frank — admin/, { timeout: 60_000 });
const registry: { name: string; category: string }[] = await (await page.request.get("/services.json")).json();
const adminNames = registry.filter((s) => s.category === "admin").map((s) => s.name);
// Panels hydrate client-side; poll the count, then compare exact sets.
await expect
.poll(() => page.locator("#grid .panel .name").count(), { timeout: 60_000 })
.toBe(adminNames.length);
const names = await page.locator("#grid .panel .name").allTextContents();
expect([...names].sort()).toEqual([...adminNames].sort());
// RBAC tripwire: a user-category panel in the admin grid means category leakage.
expect(names).not.toContain("SearXNG");
});
test("admin open links reach their apps through SSO", async ({ page }) => {
test.setTimeout(240_000);
await page.goto("/admin/");
await expect(page.locator("#grid a.open")).toHaveCount(3); // grafana, prometheus, loki
const hrefs = await page.locator("#grid a.open").evaluateAll((els) =>
els.map((el) => (el as HTMLAnchorElement).href),
);
for (const href of hrefs) {
const target = new URL(href);
await page.goto(href);
// Every guarded vhost runs its own Anubis proof-of-work on first visit.
await expect
.poll(
() => page.locator("body").innerText().then((t) => !t.includes("Making sure you're not a bot")),
{ timeout: 60_000 },
)
.toBe(true);
await expect
.poll(() => new URL(page.url()).hostname, { timeout: 60_000 })
.toBe(target.hostname);
expect(await page.locator('input[name="uidField"]').count()).toBe(0);
const resp = await page.request.get(page.url());
expect(resp.status(), `${target.hostname} must answer, not error`).toBeLessThan(500);
}
});
// Admin-space reachability must not depend on the panel's link list: an admin
// session must reach every guarded admin app by direct navigation, even where
// no open-link exists. This is the RBAC allow-leg for the outpost gates: an
// authenticated admin always lands in the app, never on a login wall or a 403.
test("admin direct navigation reaches every admin app without an auth wall", async ({ page }) => {
test.setTimeout(240_000);
for (const sub of ["grafana", "prometheus", "loki"]) {
const host = `${sub}.franksdank.cloud`;
await page.goto(`https://${host}/`);
// The outpost hands off silently for an authenticated session; the admin
// must land in the app, not on authentik's flow UI.
await expect
.poll(() => page.locator('input[name="uidField"]').isVisible().catch(() => false), { timeout: 60_000 })
.toBe(false);
expect(new URL(page.url()).hostname, `${host} must not bounce an admin off its host`).toBe(host);
const resp = await page.request.get(`https://${host}/`);
expect(resp.status(), `${host} must answer an authenticated admin, not error`).toBeLessThan(500);
}
});