Every authentik account that signs in to Audiobookshelf becomes a listener #134
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
|
||||
// spec until now: the 17-hermes "uncovered vhost broke silently" class).
|
||||
// 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.
|
||||
testMatch: /(10|12|13|16|17|18|20|21|22)-.*\.spec\.ts/,
|
||||
// the "not a login wall but maybe a 404" hole 20- closed for Grafana. 24-
|
||||
// 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" },
|
||||
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