Files
computer-playbook/roles/web-app-wordpress/files/playwright/test-rbac-roles.js
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00

141 lines
5.2 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { installCspViolationObserver, gotoOnion } = require("./personas");
const { skipUnlessServiceEnabled } = require("./service-gating");
// Auto-provisioned LDAP/Keycloak groups drive WordPress roles via the
// OIDC `groups` claim, consumed by the mu-plugin
// infinito-oidc-rbac-mapper.php. Test three roles across the privilege
// spectrum serially so a regression in one mapping does not mask another.
const RBAC_ROLE_SEQUENCE = ["subscriber", "editor", "administrator"];
// Keycloak group path is hierarchical:
// /roles/web-app-wordpress/<role> (Single-Site) or
// /roles/web-app-wordpress/<tenant>/<role> (Multisite).
// RBAC_GROUP_PATH_PREFIX renders `/roles/web-app-wordpress/`; the spec
// appends the role segment. Multisite scenarios opt in via
// services.wordpress.multisite.enabled = true.
exports.register = function (shared) {
for (const role of RBAC_ROLE_SEQUENCE) {
test(`rbac: membership in ${role} group grants WordPress ${role} role`, async ({
browser,
}) => {
skipUnlessServiceEnabled("sso");
skipUnlessServiceEnabled("ldap");
test.skip(
shared.env.multisiteEnabled,
"WORDPRESS_MULTISITE_ENABLED=true; Single-Site RBAC scenarios run only when Multisite is disabled"
);
const groupPath = `${shared.env.rbacGroupPathPrefix}${role}`;
let biberAddedToGroup = false;
// Each identity runs in its own isolated context so WP session
// cookies, Keycloak SSO cookies, and OIDC post-logout redirect
// state cannot leak between super-admin, biber, and wp-admin hops.
const newCtx = async () => {
const ctx = await browser.newContext({
ignoreHTTPSErrors: true,
viewport: { width: 1440, height: 1100 },
});
const p = await ctx.newPage();
await installCspViolationObserver(p);
return { ctx, page: p };
};
try {
const adminKc = await newCtx();
try {
await gotoOnion(adminKc.page, `${shared.env.keycloakBaseUrl}/admin/master/console/`);
await shared.fillKeycloakLoginForm(
adminKc.page,
shared.env.superAdminUsername,
shared.env.superAdminPassword
);
await expect
.poll(() => adminKc.page.url(), {
timeout: resolveTimeout(60_000),
message: "Expected to land in the Keycloak admin console",
})
.toContain("/admin/master/console/");
biberAddedToGroup = await shared.keycloakAdminAddUserToGroup(
adminKc.page,
shared.env.keycloakBaseUrl,
shared.env.realmName,
groupPath,
shared.env.biberUsername
);
} finally {
await adminKc.ctx.close().catch(() => {});
}
const biberWp = await newCtx();
try {
await shared.wpAdminLoginViaOidc(
biberWp.page,
shared.env.wpBaseUrl,
shared.env.biberUsername,
shared.env.biberPassword
);
} finally {
await biberWp.ctx.close().catch(() => {});
}
const wpAdmin = await newCtx();
try {
await shared.wpAdminLoginViaOidc(
wpAdmin.page,
shared.env.wpBaseUrl,
shared.env.adminUsername,
shared.env.adminPassword
);
await gotoOnion(wpAdmin.page, `${shared.env.wpBaseUrl}/wp-admin/users.php`, {
waitUntil: "domcontentloaded",
});
const biberRow = wpAdmin.page
.locator("tr")
.filter({ hasText: new RegExp(shared.env.biberUsername, "i") })
.first();
await expect(
biberRow,
`Expected biber row to be visible on /wp-admin/users.php`
).toBeVisible({ timeout: resolveTimeout(30_000) });
const rowText = (await biberRow.textContent()) || "";
const expectedLabel = role.charAt(0).toUpperCase() + role.slice(1);
expect(
rowText.includes(expectedLabel),
`biber's row on /wp-admin/users.php MUST show WordPress role "${expectedLabel}" after OIDC login; row text: ${rowText}`
).toBe(true);
} finally {
await wpAdmin.ctx.close().catch(() => {});
}
} finally {
// Idempotency: only teardown when this test performed the join.
// If biber was already a member at start, leave the membership
// untouched.
if (biberAddedToGroup) {
try {
const reqCtx = await browser.newContext({ ignoreHTTPSErrors: true });
try {
await shared.keycloakRemoveUserFromGroupViaRest(
reqCtx.request,
shared.env.keycloakBaseUrl,
shared.env.realmName,
shared.env.superAdminUsername,
shared.env.superAdminPassword,
groupPath,
shared.env.biberUsername
);
} finally {
await reqCtx.close().catch(() => {});
}
} catch (err) {
console.warn(`Cleanup removal of biber from ${groupPath} failed: ${err}`);
}
}
}
});
}
};