test(e2e): pin the Audiobookshelf default listener grant
spec 24- signs in through SSO as qa-e2e — an identity in no Audiobookshelf group — and must land inside the app with type "user". It is the counterpart to 23-, which pins the admin escalation: together they catch both directions of a scope-mapping regression (household locked out, or household made site admin). Verified red against the pre-change mapping — the identity never received a token — and green after re-provisioning: 4 passed. Joined to the standard project, which is the only project whose identity observes the default.
This commit is contained in:
1 parent
30186d8365
commit
5cf5416471
2 files changed
+58
-2
No files matched your search
@@ -35,8 +35,11 @@ export default defineConfig({
|
|||||||
// 2026-10-19 - the newest guarded app, in neither GUARDED list nor any
|
// 2026-10-19 - the newest guarded app, in neither GUARDED list nor any
|
||||||
// spec until now: the 17-hermes "uncovered vhost broke silently" class).
|
// spec until now: the 17-hermes "uncovered vhost broke silently" class).
|
||||||
// 22- joins Prometheus's app-is-really-there proof (its own API), closing
|
// 22- joins Prometheus's app-is-really-there proof (its own API), closing
|
||||||
// the "not a login wall but maybe a 404" hole 20- closed for Grafana.
|
// the "not a login wall but maybe a 404" hole 20- closed for Grafana. 24-
|
||||||
testMatch: /(10|12|13|16|17|18|20|21|22)-.*\.spec\.ts/,
|
// joins 2026-11-03 (Audiobookshelf's DEFAULT listener grant) and belongs to
|
||||||
|
// this project precisely because the standard identity is in no Audiobookshelf
|
||||||
|
// group — the admin project could not observe the default.
|
||||||
|
testMatch: /(10|12|13|16|17|18|20|21|22|24)-.*\.spec\.ts/,
|
||||||
use: { storageState: ".auth/qa-e2e.json" },
|
use: { storageState: ".auth/qa-e2e.json" },
|
||||||
dependencies: ["setup"],
|
dependencies: ["setup"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
// Audiobookshelf grants the LISTENER role to anyone the IdP lets sign in (2026-11-03).
|
||||||
|
//
|
||||||
|
// The role used to come from membership of `audiobookshelf_users` — a group nobody was ever
|
||||||
|
// added to. The household accounts exist in authentik, and every one of them was denied at ABS
|
||||||
|
// AFTER a successful authentication, which reads as a broken login rather than a policy decision.
|
||||||
|
// The scope mapping now answers `abs_groups: ["user"]` for every signer and reserves `admin` for
|
||||||
|
// `audiobookshelf_admins`, so the IdP is the only gate to the library and onboarding a listener
|
||||||
|
// is exactly "create the account".
|
||||||
|
//
|
||||||
|
// This is the counterpart to 23-: that spec proves the admin escalation, this one proves the
|
||||||
|
// DEFAULT grant, from an identity in neither group. Against the old mapping it is red — ABS
|
||||||
|
// refuses the sign-in and the SPA never leaves /login.
|
||||||
|
const HOST = "192.168.0.235:8000";
|
||||||
|
const ABS = `http://${HOST}/audiobookshelf`;
|
||||||
|
|
||||||
|
test("an SSO signer in no Audiobookshelf group lands as a listener", async ({ page }) => {
|
||||||
|
test.setTimeout(180_000);
|
||||||
|
|
||||||
|
await page.goto(`${ABS}/login`, { waitUntil: "domcontentloaded" });
|
||||||
|
const sso = page.getByText("Sign in with Authentik");
|
||||||
|
await expect(sso).toBeVisible();
|
||||||
|
await sso.click();
|
||||||
|
|
||||||
|
// The standard project's storageState carries a live authentik session, so the round trip is
|
||||||
|
// silent: authorize -> code -> callback -> token. Still being on /login means the app refused
|
||||||
|
// the identity (the denial this change removes) or the exchange failed.
|
||||||
|
await expect
|
||||||
|
.poll(() => !new URL(page.url()).pathname.endsWith("/login"), { timeout: 90_000 })
|
||||||
|
.toBe(true);
|
||||||
|
|
||||||
|
const grabToken = () =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const v = localStorage.getItem(localStorage.key(i) as string) || "";
|
||||||
|
if (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(v)) return v;
|
||||||
|
}
|
||||||
|
const c = document.cookie.match(/(?:^|;\s*)abs_token=([^;]+)/);
|
||||||
|
return c ? decodeURIComponent(c[1]) : "";
|
||||||
|
});
|
||||||
|
await expect.poll(grabToken, { timeout: 60_000 }).not.toBe("");
|
||||||
|
|
||||||
|
const me = await page.request.get(`${ABS}/api/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${await grabToken()}` },
|
||||||
|
});
|
||||||
|
expect(me.status()).toBe(200);
|
||||||
|
const user = await me.json();
|
||||||
|
expect(user.username).toBe("qa-e2e");
|
||||||
|
// The default, not a grant: the listener role for a signer in neither group. Being `user`
|
||||||
|
// rather than `admin` is also the proof that the default never escalates.
|
||||||
|
expect(user.type).toBe("user");
|
||||||
|
});
|
||||||
Reference in new issue
Block a user