test(security): access-control regression gate for the leptos example
#10 in tonybierman/arium — merged 2026-05-25
What
Brings the Leptos adapter to parity with the Dioxus access-control gate (#8). Adds examples/leptos-fullstack-example/access-control-probe.sh and an access-control-leptos CI job that boots the example and asserts the same three properties against arium-leptos's server fns.
| Phase | Property |
|---|---|
| 1 | Every protected/admin endpoint denies an anonymous caller; public endpoints reachable; profile/logout/mfa-status return a benign default to anon, never real state. |
| 2 | Every /api/admin/* refuses a logged-in non-admin (vertical escalation). |
| 3 | B cannot revoke A's API token (horizontal / IDOR), with a control. |
Leptos-specific adaptations (all verified against the example)
The Dioxus probe couldn't be reused as-is — Leptos differs at the wire level:
- POST-only: server fns mount at
POST /api/{endpoint}(handle_server_fns), so the probe POSTs everything (no GET). - Form-encoded, not JSON:
server_fn's default codec isPostUrl(application/x-www-form-urlencoded). A JSON body fails withArgs|missing field. Bodies are form fields;Vec/struct args useserde_qsbracket encoding (role_ids[0]=1,query[event_type]=&query[limit]=10&…) so the request reachesrequire_admin_perminstead of dying in arg deserialization — otherwise a deser error would mask the gate and hide its removal in a future refactor. - Error shape: wire-
500with aType|messagebody (ServerError|Not signed in.) rather than JSON; the message text still matches the denial markers.
Same arium maybe_grant_first_admin first-user-wins bootstrap as Dioxus, so the sacrificial-admin preamble carries over unchanged.
Verified end-to-end against a fresh-DB instance: 37/37 PASS, with all 9 admin endpoints gate-confirmed under both anonymous and logged-in-non-admin. (37 vs Dioxus's 38 — the Dioxus example had an extra app-specific get_permissions fn.)
CI
access-control-leptos job: builds the example ssr-only, boots it headless (LEPTOS_SITE_ADDR + an empty site root, since a plain cargo build — unlike cargo leptos — doesn't populate one), waits for readiness, runs the probe with a seeded non-admin. As with #8, watch it go green here before merge.
Independent of #9 (the cache-key fix) — touches a different part of ci.yml.
🤖 Generated with Claude Code
Last updated 2026-05-26
Links to this note
Merged pull requests, newest first.