Skip to content

fix(security): take JWT sys_admin/support from the user record - #394

Merged
stakach merged 2 commits into
masterfrom
fix/live-user-permissions
Nov 2, 2026
Merged

stakach merged 2 commits into
masterfrom
fix/live-user-permissions

Conversation

@stakach

@stakach stakach commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Port of the matching PlaceOS/rest-api change.

Summary

  • Database decides permissions (bearer JWTs): authorize! already loads the user for the logged_out_at check. It now replaces the token's permissions with the user's current sys_admin/support, ignoring the claims. Promotions and demotions apply to existing sessions without signing in again. Every is_admin?/is_support?/user_admin?/user_support? check uses the result.
  • API keys unchanged: a key keeps its stored permissions, which may exceed its owner's (intended).
  • Deleted users → 401: hard-deleted or soft-deleted (deleted = true) users are refused on both the JWT and API key paths. Previously a hard-deleted user's JWT returned 404 (User.find raises RecordNotFound) and a soft-deleted user got through.
  • Guest-scoped tokens are unchanged.

Spec fixture change

Mock::Token.office/google always claim Admin, but by default named a user with sys_admin: false (the flags only choose the user's email). Now that the user record decides, those specs would have dropped to plain-user permissions, so the mocks now mark that user sys_admin. Spec behaviour is unchanged.

Tests

New spec/controllers/live_permissions_spec.cr:

  • demoted to user (admin and support routes now 403)
  • demoted to support only
  • promoted (admin and support routes now allowed on the same token)
  • an API key granting more than its owner holds
  • JWTs for hard- and soft-deleted users, and an API key for a soft-deleted user (401)

🤖 Generated with Claude Code

Port of PlaceOS/rest-api 8779928.

authorize! already loads the user row for bearer JWTs, but sys_admin and
support were read only from the token claims, so revoking them had no
effect until the token expired. The decoded JWT is now capped to the
user's current permissions, so every is_admin?/is_support? check sees
the effective value. A token is never raised above its claims.

API keys keep their stored permissions, which may exceed the owner's.

Requests from deleted (hard or soft) users now return 401 on both the
JWT and API key paths, rather than proceeding or returning 404 from the
RecordNotFound raised by User.find.

Mock::Token.office/google claim Admin but named a non-admin user, so the
mocks now mark that user sys_admin to keep spec behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the type: bug something isn't working label Nov 2, 2026
The user record now decides permissions for bearer JWTs, rather than
only capping the token's claims, so a promotion also applies to existing
sessions without the user signing in again. API keys keep their own
stored permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added type: bug something isn't working and removed type: bug something isn't working labels Nov 2, 2026
@stakach stakach changed the title fix(security): cap JWT permissions by the user's current admin/support fix(security): take JWT sys_admin/support from the user record Nov 2, 2026
@stakach
stakach merged commit 3aeb826 into master Nov 2, 2026
11 of 12 checks passed
@stakach
stakach deleted the fix/live-user-permissions branch November 2, 2026 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: bug something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant