web-svc-logout already reported every domain it swept - into a table inside
the invisible front-channel iframe Keycloak loads. The status existed and
nobody could see it, so the visitor read Keycloak's "you are logged out" and
left while the sweep was still running.
The injected script listens for the conductor's postMessage reports and
renders them on Keycloak's own page: one row per service, a counter, and a
terminal state that names an action rather than describing one - "You can
close this page now" is the question the visitor actually has.
Silence must not read as success. The panel paints itself on the logout path
before any message arrives, and a conductor that never loads leaves a visible
"could not check your other sessions" plus a link, instead of a page that
looks finished. The unload guard is armed from that same moment and released
on done, on a 60s stall, or when the grace period expires without a start -
a guard that outlives its sweep would prompt on every later navigation, and a
stall that only releases silently would leave a spinner that never resolves,
which reads worse than an error. Neither timeout claims the logout finished.
A ❌ carries the manual <host>/logout?manual=1 link the conductor's own table
had; the panel builds it from the reported host, so universal-logout is
untouched. Rows show "Shop" with the full host on hover, state text sits
beside every glyph so aria-live has something to announce, and the border
colour carries the state for anyone who does not read the heading.
The trusted origin comes from lookup('tls', 'web-svc-logout', 'url.base') -
the same expression vars/main.yml:85 uses for KEYCLOAK_FRONTCHANNEL_LOGOUT_URL,
so the framed origin and the accepted origin cannot drift apart. A test pins
that, because they live in different files and the coupling was accidental.
frontchannel.logout.session.required is "true" in the realm import, so
Keycloak must append ?iss= per the OIDC front-channel spec; document.referrer
is the fallback.
Eight mutations were checked against the collapsed script - dropping the
pessimistic render, either timer, the origin check, the manual link, or making
either terminal state claim success all fail the suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocking a persona removes the only end-to-end proof that a role's auth chain
works. docs/contributing/artefact/files/role/playwright.specs.js.md:146 has
always required each flag to carry a one-line nocheck stating the role contract
that justifies it, plus a matching paragraph in the role's README or TODO.
Nothing enforced it, so 89 flags across 46 roles carried neither.
The lint mirrors test_has_spec's shape and uses is_suppressed_at with
mode="line-above", so the marker must sit directly above its flag -- a blank
line between them breaks the association, which is what the persona helper's
reader will look at. A literal =false blocks nothing and is exempt; the
sso-conditional ternary is not, because it blocks in every variant that pins
services.sso.enabled false.
Writing 89 reasons turned out to be the point. Five roles audited in one pass
blocked for five entirely different mechanisms, all of which read as the same
shrug behind a bare =true: pgAdmin's login id is the account e-mail rather than
the Keycloak username; phpMyAdmin authenticates as the MariaDB root account;
GitLab's seeded password belongs to the built-in root, not the administrator;
Mastodon's tootctl accounts create never sets a password; phpLDAPadmin binds
with an LDAP DN and a different secret. Further out: MediaWiki removes the form
that would accept the Keycloak secret ($wgPluggableAuth_EnableLocalLogin=false)
and bounces anonymous requests back into the IdP, so the unauthenticated
landing the contract asserts can never be observed; Mattermost Team Edition has
no native OIDC, so Keycloak rides the GitLab OAuth slot behind a relabelled
link; MinIO's console returns redirectRules: null and renders no SSO button at
all; XWiki's administrator is an XWikiUsers object with no password property.
Two flags turned out to be unjustifiable and are gone rather than annotated.
web-app-mailu gated PERSONA_GUEST_BLOCKED on services.dashboard.enabled, but
runGuestFlow does one anonymous goto and asserts no signed-in marker -- it
never touches the dashboard. A leftover from a helper version that entered
through the dashboard account menu. And web-app-mig's rationale claimed the
role is an in-app module of web-app-dashboard; it has its own compose stack,
healthcheck and canonical domain, and dashboard is merely a tile consumer.
Nine of the reasons say plainly that the block is not a role property but a gap
in the shared persona helpers -- decidim's sign-out is a data-method link the
generic click cannot activate, fider's SSO entry is a two-step modal, taiga,
wordpress, xwiki, odoo and penpot bind OIDC on a path the personas never visit,
jenkins blocks unconditionally although its OIDC variant looks runnable, and
opentalk's own spec proves both personas work. That list did not exist before;
it is the actual yield here, not the 89 comment lines.
45 pre-existing comments came into the comment lint's scope because this sweep
touched their files -- it only inspects files that differ from HEAD. 28 of them
were the banner "Service gate flags rendered for closure.", every one sitting
above a line that is not a service gate flag at all, orphaned by the
alphabetical re-sort these templates once got.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rename env/static.env → env/default.env across the entire repo
(string label, doc links, Python file-path constants) — the
`parse_static_env*` function names stay as a stable API surface.
Sort all `*.env` / `*.env.j2` entries alphabetically (52 in
default.env, 4 in ci.env, 46 of 49 j2 templates; 3 templates
with Jinja2 `{% %}` control flow are skipped because conditional
blocks would tear apart under a naive sort). Sort all 83
Makefile target blocks alphabetically; .PHONY group headers
stay in place.
Collapse the two remaining multi-line Makefile recipe comments
(dotenv-force, test-signed) to single-line WHY comments.
Add four lint tests under tests/lint/filesystem/ to keep the
invariants enforced going forward:
- env/test_alphabetical_sort.py
- env/test_single_line_comments.py (# nocheck: markers exempt)
- makefile/test_alphabetical_targets.py
- makefile/test_single_line_comments.py (# nocheck: markers exempt)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the verbose
`(lookup('applications', application_id).server.domains.canonical | first)`
chain with the dedicated `lookup('domain', application_id)` plugin
across every playwright.env.j2 that renders `CANONICAL_DOMAIN`. The
helper already exists at plugins/lookup/domain.py and resolves the
canonical primary domain via the cached domain-merge pipeline, so the
inline canonical-list traversal is unnecessary noise that obscures
the actual contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five fixes against the matomo deploy CI failure mode:
* The persona helpers ([admin.js], [biber.js]) now perform the
Keycloak login on either auth shape — oauth2-proxy gate (the goto
is intercepted and the page lands directly on
`openid-connect/auth`) AND in-app OIDC plugin (Login link → click
→ redirect). Previously the Keycloak step was gated on
`oidcEnabled`, which left oauth2-proxy-only roles like prometheus
with no auth path. The login-link regex also tolerates leading /
trailing whitespace (`/^\s*(log\s*in|...)\s*$/i`), unblocking the
dashboard's `" Login"` link whose accessible name carries an
icon-induced leading space.
* `roles/web-app-keycloak/templates/playwright.env.j2`: declare
`PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`.
The persona stubs in the keycloak spec exist purely to satisfy the
persona-naming lint; Keycloak IS the auth provider so neither
persona has an in-role app surface. The bespoke
master-realm / normal-realm tests already cover the contract.
* `roles/web-app-matomo/templates/playwright.env.j2`: same — matomo's
local login form is not OIDC / oauth2-proxy, so the generic helper
has no useful flow. The bespoke "matomo administrator" test covers
the admin journey.
* `roles/web-app-prometheus/templates/playwright.env.j2`: only
`PERSONA_BIBER_BLOCKED=true`. Prometheus is admin-only via
oauth2-proxy with `allowed_groups:
web-app-prometheus-administrator`; biber's session is denied at the
proxy and biber has no in-app surface to drive a logout from.
Administrator goes through the standard oauth2-proxy → Keycloak
chain. Also rendered the missing `PROMETHEUS_BASE_URL`,
`CANONICAL_DOMAIN`, and pointed `APP_BASE_URL` at prometheus itself
so the persona helper drives the role under test (not the dashboard).
* `roles/web-app-prometheus/files/playwright.spec.js`: read
`process.env.CANONICAL_DOMAIN` directly so the env-keys-used lint
recognises the new env entry as consumed.
The 019 matrix note for prometheus is updated to describe the
biber-only opt-out and the admin's oauth2-proxy chain.
Out of scope: the matomo CSP biber-deny test still fails because
`roles/web-app-matomo/meta/services.yml` has `oauth2.enabled: false`
(TODO at line 12 — wire matomo's oauth2-proxy gate). That is a
deploy-config change, not a test fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).
* Document the rule in `docs/contributing/documentation.md` under
"Comments". Code MUST describe the *why* in present-state language;
contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
`cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
`eslint.config.js`. Pure comment / docstring / log-message edits;
no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
enforce the rule going forward across `.js`, `.py`, `.yml`,
`.yaml`, `.j2`, and `.sh` files. The legacy
`test_no_stale_requirement_refs.py` is exempt as the only file
whose purpose is to detect these very tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-service surface assertions for dashboard, prometheus and matomo
no longer run in every consumer role's persona. They are owned by the
provider's own spec, parameterised once over a `<NAME>_TARGET_ROLES_JSON`
manifest rendered into the provider's `templates/playwright.env.j2` via
the new `lookup('roles_with_service', '<svc>')` Ansible lookup.
Changes
-------
- New `plugins/lookup/roles_with_service.py` enumerates roles that
declare `services.<svc>.{enabled, shared}` truthy in the merged
applications config, returning `[{id, canonical_domain}, …]`.
- `roles/web-app-{dashboard,prometheus,matomo}/files/playwright.spec.js`
parse `<NAME>_TARGET_ROLES_JSON` and parameterise the per-consumer
assertion (dashboard tile reachability, prometheus `up=1`, matomo
SitesManager registration). The matomo spec gains an explicit
biber-denial test (request body validated against admin DOM markers
per the body-marker rule); the prometheus admin-reach test drops
the dashboard-iframe path in favour of direct goto.
- Persona helpers `roles/test-e2e-playwright/files/personas/{admin,
biber,guest}.js` enter at `${APP_BASE_URL}/` directly. The
`clickRoleTileFromDashboard` step and the `assertPrometheus*` /
`assertMatomo*` cross-service probes are gone; `utils/{dashboard,
prometheus,matomo}.js` are deleted. `utils/csp.js` switches to
literal `process.env.<KEY>` reads so the env-keys-used parity guard
recognises matomo's CSP injector consumption.
- 42 consumer-role `templates/playwright.env.j2` templates drop
`DASHBOARD_BASE_URL`, `DASHBOARD_SERVICE_ENABLED`, `PROMETHEUS_BASE_URL`,
`PROMETHEUS_SERVICE_ENABLED`. `MATOMO_BASE_URL` and
`MATOMO_SERVICE_ENABLED` stay because the per-role CSP injection
helper still gates on matomo's tracker host.
- `tests/integration/roles/test_playwright_env_services_match.py`
globally exempts `dashboard` and `prometheus` from the
services.yml-to-env parity rule.
- `tests/unit/plugins/lookup/test_roles_with_service.py` adds 14
unit tests for the lookup, including an explicit-contract test
that pins the over-inclusive behaviour on raw Jinja strings (the
lookup expects a templar-rendered payload).
- Docs: `docs/requirements/019-playwright-meta-services-parity.md`
rewords Rules 1, 3, 12 and adds Rule 13 describing the SPOT shift.
`docs/contributing/artefact/files/role/playwright.specs.js.md`
reflects the new persona shape and per-service catalogue, and is
cleaned per `docs/contributing/documentation.md` (no SPOT self-
labels, no req-references, em dashes replaced with complete
sentences, multi-clause lines split onto separate lines, broken
anchor `#two-personas-fixed-flow-` corrected).
Verification
------------
- 418 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- `markdownlint-cli2`: 0 errors on all touched docs.
- `make autoformat`: clean (FURB171 single-item-set inequality fixed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persona helpers learn three new behaviours:
* `clickRoleTileFromDashboard` and `clickSiblingTileFromDashboard`
open every visible Bootstrap dropdown / accordion trigger
(`[data-bs-toggle='dropdown']`, `[aria-expanded='false']`, ...) on
the dashboard before searching for the role tile, so roles that
live inside a collapsed category (e.g. keycloak under "Identity")
are reachable without coupling personas to the dashboard menu shape.
* `assertPrometheusForbiddenForBiber` and
`assertMatomoForbiddenForBiber` switch from fragile iframe DOM
inspection to a direct authoritative probe via
`page.request.get(<role-url>, { maxRedirects: 0 })`. They peek at
the FIRST hop only: 401 / 403 or a redirect into the auth chain
(`/oauth2/start|sign_in|callback`, `openid-connect/auth`) means
denial; any other status is examined further. A first-hop 200 is
acceptable ONLY when the body carries role-specific markers
(`prometheus_build_info` / `<title>Prometheus</title>` for
prometheus; matomo's login-form markers / `piwik|matomo` for
matomo). A 200 with a non-matching body is treated as a
misconfigured proxy or a denial-as-200 surface and fails loudly,
matching req 019 Rule 12.
* `web-app-keycloak`'s spec drops the generic persona-flow runners
(`runGuestFlow` / `runBiberFlow` / `runAdminFlow`) under the
auth-provider exemption: the bespoke "master-realm super
administrator", "normal-realm administrator", and "normal-realm
biber logs in through account interface" tests already cover the
three persona contracts via the realm account UI. The matching env
flags are marked `# nocheck: playwright-service-gate` /
`# nocheck: persona-required-envs` with auth-provider rationales.
Drift matrix in `019-playwright-meta-services-parity.md` strikes
through `web-app-matomo`, `web-app-prometheus`, `web-app-keycloak`,
`web-app-akaunting`, `web-app-baserow`, `web-svc-cdn`, and
`web-svc-simpleicons` after their FULL_CYCLE deploys produced green
Playwright runs (with the well-known kcadm.sh + sys-svc-mail
infrastructure failures documented in `web-app-mailu/TODO.md` per
the autonomy escape clause).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a lint that fails when a role's templates/playwright.env.j2 declares
DASHBOARD_SERVICE_ENABLED= but does NOT render DASHBOARD_BASE_URL,
PROMETHEUS_BASE_URL, and MATOMO_BASE_URL. Renders those three URLs
across 42 role env templates so the lint passes.
Also reverts the matomo "role-closed" strikethrough in req 019 — the
URL wiring remains in scope for the persona rollout.
Reason: the persona helpers (roles/test-e2e-playwright/files/personas/
{biber,admin,guest}.js) collapse the persona scenario when their
required URLs are empty (req 019 persona-collapse exception). Without
this lint, a role that declares dashboard: enabled but forgets the
URL silently SKIPs the persona — the deploy goes green but no real
flow ran. Per-line opt-out is `# nocheck: persona-required-envs` for
auth-less svc-* roles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces roles/test-e2e-playwright/files/personas/ — runGuestFlow,
runBiberFlow, runAdminFlow plus 10 utils (csp, dashboard, env, keycloak,
landing, logout, matomo, prometheus, interaction, index) — and wires
both the Ansible runner (run_one.yml) and the local rerun helper
(scripts/tests/e2e/rerun-spec.sh) to stage the tree alongside
service-gating.js so specs can `require("./personas")`.
First batch of role specs migrated to consume the shared library
(dashboard, keycloak, mailu, matomo, prometheus, web-svc-cdn,
web-svc-simpleicons) — each gets the spec.js persona scenarios plus
the matching meta/services.yml + playwright.env.j2 entries; matomo is
marked role-closed in req 019. dashboard/keycloak/mailu TODO.md
updated; docs/requirements/019 reflects matomo closure.
Reason: req 019 Rule 3 mandates that every role's guest/biber/
administrator flow runs the same canonical steps; centralising them
in one shared library keeps per-role drift from re-introducing the
stub-shaped bodies the new lint forbids.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Verification checkboxes in requirement 003. Each spec exercises
the `lookup('applications')` + `lookup('users')` pathway end-to-end:
login via lookup('users'), OIDC round-trip from dashboard (where the
app supports it), canonical-domain DOM assertion, and a full CSP
assertion covering every directive emitted by csp_filters.build_csp_header
(15 directives), meta-parity, enforced policy (not Report-Only), and
zero securitypolicyviolation events.
The dashboard spec is extended in place with the CSP + canonical-domain
assertions while keeping its existing login/logout coverage.
rerun-spec.sh is pointed at the Jinja template + the staged copy of
package.json (the repo has templates/package.json.j2, not files/package.json),
so the inner spec-only iteration loop works without re-deploying.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>