fix(auth): gate the bunyip path on user and tenant status #476

Merged
Claude-Run merged 1 commit from fix/PMS-698-bunyip-principal-gate into main 2026-08-01 05:56:01 +02:00
Member

The bunyip Resource-Server branch of auth_middleware resolved a placement, JIT-mirrored the users row, reconciled the role, and authenticated - without ever looking at users.status or the owning tenant's status. Only the legacy HS256 fallback ran those checks, so on the path the SPA actually uses a user flipped to inactive, or a member of a tenant suspended via POST /tenants/{id}/suspend, kept full read and write access to every /api/v1/* route until their bunyip token expired. Deactivation and tenant suspension were effectively no-ops in production.

Extract the two checks into AuthService::ensure_principal_usable and call it from both branches: the legacy branch through ensure_user_and_tenant_active (which keeps the PMS-681 password-change cutoff on top of it) and the bunyip branch from place_bunyip_user, which now returns None on failure so the request ends unauthenticated instead of silently authenticating. The iat-vs-password_changed_at cutoff deliberately stays legacy-only: bunyip owns the credential on the RS path, so a mokosh-side password change is not a revocation signal for a bunyip token.

tests/bunyip_principal_gate.rs boots the real router with the RS verifier mounted against a stub OP (Ed25519 JWKS + userinfo, signed with the RFC 8032 test vector so no key-generation dependency is needed) and asserts that both a bunyip bearer and a legacy bearer for the same fixture go non-2xx on GET /api/v1/tickets after a tenant suspension and after a user deactivation. tests/bunyip_login.rs adds the matching function-level assertions that place_bunyip_user returns None in both cases.

#PMS-698

The bunyip Resource-Server branch of `auth_middleware` resolved a placement, JIT-mirrored the users row, reconciled the role, and authenticated - without ever looking at `users.status` or the owning tenant's status. Only the legacy HS256 fallback ran those checks, so on the path the SPA actually uses a user flipped to `inactive`, or a member of a tenant suspended via `POST /tenants/{id}/suspend`, kept full read and write access to every `/api/v1/*` route until their bunyip token expired. Deactivation and tenant suspension were effectively no-ops in production. Extract the two checks into `AuthService::ensure_principal_usable` and call it from both branches: the legacy branch through `ensure_user_and_tenant_active` (which keeps the PMS-681 password-change cutoff on top of it) and the bunyip branch from `place_bunyip_user`, which now returns `None` on failure so the request ends unauthenticated instead of silently authenticating. The `iat`-vs-`password_changed_at` cutoff deliberately stays legacy-only: bunyip owns the credential on the RS path, so a mokosh-side password change is not a revocation signal for a bunyip token. `tests/bunyip_principal_gate.rs` boots the real router with the RS verifier mounted against a stub OP (Ed25519 JWKS + userinfo, signed with the RFC 8032 test vector so no key-generation dependency is needed) and asserts that both a bunyip bearer and a legacy bearer for the same fixture go non-2xx on `GET /api/v1/tickets` after a tenant suspension and after a user deactivation. `tests/bunyip_login.rs` adds the matching function-level assertions that `place_bunyip_user` returns `None` in both cases. #PMS-698
fix(auth): gate the bunyip path on user and tenant status
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 42s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m0s
Integration / integration tests (pull_request) Successful in 9m1s
Create release / Gate (release-branch merges only) (pull_request) Successful in 1s
Create release / Create release from merged PR (pull_request) Has been skipped
12db844536
The bunyip Resource-Server branch of `auth_middleware` resolved a placement, JIT-mirrored the users row, reconciled the role, and authenticated - without ever looking at `users.status` or the owning tenant's status. Only the legacy HS256 fallback ran those checks, so on the path the SPA actually uses a user flipped to `inactive`, or a member of a tenant suspended via `POST /tenants/{id}/suspend`, kept full read and write access to every `/api/v1/*` route until their bunyip token expired. Deactivation and tenant suspension were effectively no-ops in production.

Extract the two checks into `AuthService::ensure_principal_usable` and call it from both branches: the legacy branch through `ensure_user_and_tenant_active` (which keeps the PMS-681 password-change cutoff on top of it) and the bunyip branch from `place_bunyip_user`, which now returns `None` on failure so the request ends unauthenticated instead of silently authenticating. The `iat`-vs-`password_changed_at` cutoff deliberately stays legacy-only: bunyip owns the credential on the RS path, so a mokosh-side password change is not a revocation signal for a bunyip token.

`tests/bunyip_principal_gate.rs` boots the real router with the RS verifier mounted against a stub OP (Ed25519 JWKS + userinfo, signed with the RFC 8032 test vector so no key-generation dependency is needed) and asserts that both a bunyip bearer and a legacy bearer for the same fixture go non-2xx on `GET /api/v1/tickets` after a tenant suspension and after a user deactivation. `tests/bunyip_login.rs` adds the matching function-level assertions that `place_bunyip_user` returns `None` in both cases.

#PMS-698
Claude-Run deleted branch fix/PMS-698-bunyip-principal-gate 2026-08-01 05:56:02 +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!476
No description provided.