fix(admin): revoke refresh-tokens on claim-feeding mutations (BUNYIP-144) #175

Merged
YousifShkara merged 1 commit from fix/bunyip-144-revoke-rt-on-claim-mutations into main 2026-06-20 01:15:19 +02:00
Owner

Extends the BUNYIP-137 revoke-on-role-change pattern to every admin
handler that mutates a field feeding has_member_access. Without this,
the dashboard's per-app gate (Mokosh / Drillmark / Lets Chat tiles)
keeps rendering the pre-grant state for an unbounded window because the
user's still-valid refresh token mints access tokens with stale claims
on every rotation - and the SPA reads CurrentUser from those cached
claims, not from a live DB join.

Hit in the field: brendon@netcal.com (2026-06-19) was promoted to
lifetime member but his Mokosh tile stayed greyed out until a manual
sign-out + sign-in.

Helper revoke_user_sessions(pool, user_id) wraps the existing
TokenRepository::revoke_all_user_refresh_tokens call so each handler
collapses to a one-liner and the audit-log metadata can carry
sessions_revoked: true consistently.

Wired into:

  • update_user_status (both deactivate AND reactivate branches; the
    latter is belt-and-braces against a future regression that would let
    a stale token leak past restore).
  • grant_membership (sets lifetime_member=true + subscription=active).
  • revoke_membership (privilege-DOWNGRADE; the strongest security
    argument of any handler in this set - without it the user keeps
    admin-or-member claims for the full 30-day refresh TTL).
  • grant_lifetime_membership (the brendon@netcal.com fix).
  • revoke_lifetime_membership (privilege-downgrade, same posture).

Not touched:

  • update_user_role already had it (BUNYIP-137); pattern reused.
  • change_password (domain service) already revokes at line 864 of
    crates/bunyip-domain/src/services/auth.rs; password-reset confirmation
    flows through that path.
  • admin_reset_password (only sends the reset email; the actual
    password change happens through the user-initiated confirmation,
    which is already covered).
  • delete_user (soft_delete sets deleted_at; the auth middleware drops
    tokens whose user row has deleted_at IS NOT NULL, so revocation here
    is redundant; file a follow-up if a regression ever changes that).
  • Trusted devices (BUNYIP-138) deliberately NOT touched - they bind to
    a verified TOTP enrolment, not to membership state; revoking them on
    every grant would log the user out of their browser device too, which
    is the opposite of what we want.

just check-container green (fmt + clippy -D warnings + workspace
lib tests).

#BUNYIP-144
#BUNYIP-137

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

Extends the BUNYIP-137 revoke-on-role-change pattern to every admin handler that mutates a field feeding `has_member_access`. Without this, the dashboard's per-app gate (Mokosh / Drillmark / Lets Chat tiles) keeps rendering the pre-grant state for an unbounded window because the user's still-valid refresh token mints access tokens with stale claims on every rotation - and the SPA reads `CurrentUser` from those cached claims, not from a live DB join. Hit in the field: brendon@netcal.com (2026-06-19) was promoted to lifetime member but his Mokosh tile stayed greyed out until a manual sign-out + sign-in. Helper `revoke_user_sessions(pool, user_id)` wraps the existing `TokenRepository::revoke_all_user_refresh_tokens` call so each handler collapses to a one-liner and the audit-log metadata can carry `sessions_revoked: true` consistently. Wired into: - `update_user_status` (both deactivate AND reactivate branches; the latter is belt-and-braces against a future regression that would let a stale token leak past restore). - `grant_membership` (sets lifetime_member=true + subscription=active). - `revoke_membership` (privilege-DOWNGRADE; the strongest security argument of any handler in this set - without it the user keeps admin-or-member claims for the full 30-day refresh TTL). - `grant_lifetime_membership` (the brendon@netcal.com fix). - `revoke_lifetime_membership` (privilege-downgrade, same posture). Not touched: - `update_user_role` already had it (BUNYIP-137); pattern reused. - `change_password` (domain service) already revokes at line 864 of crates/bunyip-domain/src/services/auth.rs; password-reset confirmation flows through that path. - `admin_reset_password` (only sends the reset email; the actual password change happens through the user-initiated confirmation, which is already covered). - `delete_user` (soft_delete sets deleted_at; the auth middleware drops tokens whose user row has deleted_at IS NOT NULL, so revocation here is redundant; file a follow-up if a regression ever changes that). - Trusted devices (BUNYIP-138) deliberately NOT touched - they bind to a verified TOTP enrolment, not to membership state; revoking them on every grant would log the user out of their browser device too, which is the opposite of what we want. `just check-container` green (fmt + clippy -D warnings + workspace lib tests). #BUNYIP-144 #BUNYIP-137 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(admin): revoke refresh-tokens on claim-feeding mutations (BUNYIP-144)
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 2m32s
aae2c9c1ce
Extends the BUNYIP-137 revoke-on-role-change pattern to every admin
handler that mutates a field feeding `has_member_access`. Without this,
the dashboard's per-app gate (Mokosh / Drillmark / Lets Chat tiles)
keeps rendering the pre-grant state for an unbounded window because the
user's still-valid refresh token mints access tokens with stale claims
on every rotation - and the SPA reads `CurrentUser` from those cached
claims, not from a live DB join.

Hit in the field: brendon@netcal.com (2026-06-19) was promoted to
lifetime member but his Mokosh tile stayed greyed out until a manual
sign-out + sign-in.

Helper `revoke_user_sessions(pool, user_id)` wraps the existing
`TokenRepository::revoke_all_user_refresh_tokens` call so each handler
collapses to a one-liner and the audit-log metadata can carry
`sessions_revoked: true` consistently.

Wired into:
- `update_user_status` (both deactivate AND reactivate branches; the
  latter is belt-and-braces against a future regression that would let
  a stale token leak past restore).
- `grant_membership` (sets lifetime_member=true + subscription=active).
- `revoke_membership` (privilege-DOWNGRADE; the strongest security
  argument of any handler in this set - without it the user keeps
  admin-or-member claims for the full 30-day refresh TTL).
- `grant_lifetime_membership` (the brendon@netcal.com fix).
- `revoke_lifetime_membership` (privilege-downgrade, same posture).

Not touched:
- `update_user_role` already had it (BUNYIP-137); pattern reused.
- `change_password` (domain service) already revokes at line 864 of
  crates/bunyip-domain/src/services/auth.rs; password-reset confirmation
  flows through that path.
- `admin_reset_password` (only sends the reset email; the actual
  password change happens through the user-initiated confirmation,
  which is already covered).
- `delete_user` (soft_delete sets deleted_at; the auth middleware drops
  tokens whose user row has deleted_at IS NOT NULL, so revocation here
  is redundant; file a follow-up if a regression ever changes that).
- Trusted devices (BUNYIP-138) deliberately NOT touched - they bind to
  a verified TOTP enrolment, not to membership state; revoking them on
  every grant would log the user out of their browser device too, which
  is the opposite of what we want.

`just check-container` green (fmt + clippy -D warnings + workspace
lib tests).

#BUNYIP-144
#BUNYIP-137

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
YousifShkara deleted branch fix/bunyip-144-revoke-rt-on-claim-mutations 2026-06-20 01:15:19 +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/bunyip!175
No description provided.