feat(auth): translate Bunyip bunyip_role claim to mokosh role (PMS-172) #142

Merged
nrupard merged 2 commits from feat/pms-172-bunyip-role-translation into main 2026-06-09 18:29:47 +02:00
Owner

What

Implements PMS-172: on the Bunyip Resource-Server auth path, mokosh now derives the request role by translating Bunyip's bunyip_role claim instead of blindly using the local JIT-created users.role (which defaults to technician). This is why a Bunyip admin showed up as technician in mokosh and the PMS-155 E2E suite 403'd.

Model (confirmed with the team)

Bunyip is the SSO/identity manager and governs only the TOP role; everything below it stays mokosh-internal:

  • bunyip_role = "admin" -> mokosh super_admin (authoritative).
  • bunyip_role = "subscriber" -> the user's locally-assigned mokosh role (admin/manager/finance/technician/...), EXCEPT super_admin is Bunyip-exclusive, so a stale/local super_admin is clamped to admin.
  • any unknown/future value -> treated like subscriber (never elevates, still clamps a stale super_admin).
  • claim absent (None) -> keep the local role unchanged.

Bunyip's role vocabulary is only {subscriber, admin} (bunyip/crates/bunyip-domain/src/models/user.rs); mokosh owns the translation so Bunyip never has to know mokosh's taxonomy.

Changes

  • oidc_rs::AtClaims gains bunyip_role: Option<String> (#[serde(default)], so older tokens -> None).
  • effective_role_from_bunyip(bunyip_role, local) -> UserRole in middleware.rs (pure, unit-tested).
  • ensure_user_from_bunyip computes the effective role and reconciles the shadow row via the new AuthService::set_user_role (plain UPDATE, no audit, fires only when the role changed); the effective role flows into AuthState.

Back-compat / scope

With no claim the effective role equals the local role, so the legacy HS256 cookie path and standalone/single-tenant deployments are completely untouched. The mokosh-auth at+jwt path (parse_mokosh_role) is deliberately not reused or changed.

Depends on

  • BUNYIP-66 (Bunyip emits bunyip_role in the at+jwt). This change is inert until that lands - safe to merge first.

Tests

  • 5 unit tests for the translation (admin->super_admin, subscriber->local, super_admin clamp, unknown-value, absent-claim). cargo check --all-targets, clippy --all-targets, and fmt --check all clean.
  • A full token->admin-route integration test is deferred until BUNYIP-66 emits the claim (the bunyip JWKS verifier is not mockable here without a live issuer). Folding in the e2e/README.md doc update tracked on PMS-172 once the claim is live.

🤖 Generated with Claude Code

## What Implements PMS-172: on the Bunyip Resource-Server auth path, mokosh now derives the request role by translating Bunyip's `bunyip_role` claim instead of blindly using the local JIT-created `users.role` (which defaults to `technician`). This is why a Bunyip admin showed up as `technician` in mokosh and the PMS-155 E2E suite 403'd. ## Model (confirmed with the team) Bunyip is the SSO/identity manager and governs only the TOP role; everything below it stays mokosh-internal: - `bunyip_role = "admin"` -> mokosh `super_admin` (authoritative). - `bunyip_role = "subscriber"` -> the user's locally-assigned mokosh role (admin/manager/finance/technician/...), EXCEPT `super_admin` is Bunyip-exclusive, so a stale/local `super_admin` is clamped to `admin`. - any unknown/future value -> treated like `subscriber` (never elevates, still clamps a stale `super_admin`). - claim absent (`None`) -> keep the local role unchanged. Bunyip's role vocabulary is only `{subscriber, admin}` (`bunyip/crates/bunyip-domain/src/models/user.rs`); mokosh owns the translation so Bunyip never has to know mokosh's taxonomy. ## Changes - `oidc_rs::AtClaims` gains `bunyip_role: Option<String>` (`#[serde(default)]`, so older tokens -> `None`). - `effective_role_from_bunyip(bunyip_role, local) -> UserRole` in `middleware.rs` (pure, unit-tested). - `ensure_user_from_bunyip` computes the effective role and reconciles the shadow row via the new `AuthService::set_user_role` (plain UPDATE, no audit, fires only when the role changed); the effective role flows into `AuthState`. ## Back-compat / scope With no claim the effective role equals the local role, so the legacy HS256 cookie path and standalone/single-tenant deployments are completely untouched. The mokosh-auth `at+jwt` path (`parse_mokosh_role`) is deliberately not reused or changed. ## Depends on - BUNYIP-66 (Bunyip emits `bunyip_role` in the at+jwt). This change is inert until that lands - safe to merge first. ## Tests - 5 unit tests for the translation (admin->super_admin, subscriber->local, super_admin clamp, unknown-value, absent-claim). `cargo check --all-targets`, `clippy --all-targets`, and `fmt --check` all clean. - A full token->admin-route integration test is deferred until BUNYIP-66 emits the claim (the bunyip JWKS verifier is not mockable here without a live issuer). Folding in the `e2e/README.md` doc update tracked on PMS-172 once the claim is live. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(auth): translate Bunyip bunyip_role claim to mokosh role on the RS path
Some checks failed
E2E (staging) / Playwright against staging (pull_request) Failing after 49s
Check / fmt + clippy + compile + tests (pull_request) Successful in 1m0s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m18s
83782890aa
Bunyip is the SSO/identity manager and should govern mokosh's top role, but its admin status never reached mokosh: the Bunyip at+jwt carried no role claim, so the RS path (ensure_user_from_bunyip) derived the role from a local JIT-created users row that defaults to technician. A Bunyip admin was seen as technician until someone hand-edited the DB (this blocked the PMS-155 E2E suite).

Read the new bunyip_role claim and translate it into mokosh's taxonomy, with Bunyip authoritative only for the top role:

- oidc_rs::AtClaims gains bunyip_role: Option<String> (serde default, so older tokens deserialize to None).
- effective_role_from_bunyip(): admin -> super_admin; subscriber -> the local users.role, except super_admin is Bunyip-exclusive so a stale/local super_admin is clamped to admin; any unknown value behaves like subscriber (never elevates, still clamps); absent claim -> keep the local role unchanged.
- ensure_user_from_bunyip computes the effective role and reconciles the shadow row via the new AuthService::set_user_role (a plain UPDATE, no audit, fires only on change) so the DB stays accurate; the effective role flows into AuthState.

Back-compatible: with no claim the effective role equals the local role, so the legacy HS256 cookie path and standalone/single-tenant deployments are untouched. Depends on BUNYIP-66 emitting the claim. The translation is unit-tested; a full token->admin-route integration test is deferred until BUNYIP-66 emits the claim (the bunyip verifier is not mockable here without a live JWKS).

#PMS-172

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(auth): clarify authz source + log Bunyip role transitions (PMS-172 review)
Some checks failed
E2E (staging) / Playwright against staging (pull_request) Failing after 1m46s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + compile + tests (pull_request) Successful in 3m16s
Build OCI container / Build and push mokosh-api image (push) Successful in 8m9s
e950947477
Code-review follow-up on the bunyip_role translation. No behavior change to the authorization decision.

- Document that the translated effective role is the authoritative authz for the request (derived from the already-verified at+jwt), and that set_user_role is best-effort persistence only: a failed write neither under- nor over-grants, since the request runs on the in-memory effective role and the next request re-syncs. This was a review-noted source of confusion.
- Log every role transition at info (fires only on change) so an elevation/demotion is observable; full audit-table row deferred (needs request-context plumbing).
- Debug-log an unrecognized bunyip_role value to surface Bunyip integration drift; behavior is unchanged (unknown values are treated as non-admin and never elevate).

#PMS-172

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/pms-172-bunyip-role-translation 2026-06-09 18:29:48 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!142
No description provided.