From a91a6a9c49d1ca6693c8b4a5e15e03893577ee38 Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Mon, 2 Nov 2026 18:14:38 +1100 Subject: [PATCH] fix(auth): take JWT sys_admin/support from the user record 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 --- spec/live_permissions_spec.cr | 13 ++++++++----- src/placeos-rest-api/utilities/current-user.cr | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/spec/live_permissions_spec.cr b/spec/live_permissions_spec.cr index 2a7b15bb..f90fed18 100644 --- a/spec/live_permissions_spec.cr +++ b/spec/live_permissions_spec.cr @@ -1,8 +1,8 @@ require "./helper" module PlaceOS::Api - # sys_admin / support are capped by the user's current model, so revoking them applies - # before the JWT expires. API keys deliberately keep their own stored permissions. + # sys_admin / support come from the user's current model, not the JWT claims, so changes + # apply to existing sessions. API keys deliberately keep their own stored permissions. describe "live user permissions" do admin_path = ApiKeys.base_route # check_admin support_path = Storages.base_route # check_support @@ -70,16 +70,19 @@ module PlaceOS::Api client.get(support_path, headers: headers).status_code.should eq 200 end - it "does not elevate a JWT when the user is promoted" do + it "elevates a JWT when the user is promoted" do user = new_user.call(false, false) headers = bearer_headers.call(user) + client.get(admin_path, headers: headers).status_code.should eq 403 + client.get(support_path, headers: headers).status_code.should eq 403 + user.sys_admin = true user.support = true user.save! - client.get(admin_path, headers: headers).status_code.should eq 403 - client.get(support_path, headers: headers).status_code.should eq 403 + client.get(admin_path, headers: headers).status_code.should eq 200 + client.get(support_path, headers: headers).status_code.should eq 200 end it "lets an API key grant more than its owner holds" do diff --git a/src/placeos-rest-api/utilities/current-user.cr b/src/placeos-rest-api/utilities/current-user.cr index 86c5926b..8a9091bb 100644 --- a/src/placeos-rest-api/utilities/current-user.cr +++ b/src/placeos-rest-api/utilities/current-user.cr @@ -83,7 +83,7 @@ module PlaceOS::Api raise JWT::Error.new("logged out") end @current_user = user_model - user_token = cap_permissions(user_token, user_model) + user_token = apply_user_permissions(user_token, user_model) end @user_token = user_token @@ -109,14 +109,14 @@ module PlaceOS::Api user end - # A JWT can't grant more than the user currently holds, so revoking sys_admin or - # support applies before the token expires. Promotions still need a new token. - private def cap_permissions(token : ::PlaceOS::Model::UserJWT, user : ::PlaceOS::Model::User) : ::PlaceOS::Model::UserJWT + # The user record, not the JWT claims, decides sys_admin / support, so permission + # changes apply to existing sessions without the user signing in again. + private def apply_user_permissions(token : ::PlaceOS::Model::UserJWT, user : ::PlaceOS::Model::User) : ::PlaceOS::Model::UserJWT held = ::PlaceOS::Model::UserJWT::Permissions::User held |= ::PlaceOS::Model::UserJWT::Permissions::Admin if user.sys_admin held |= ::PlaceOS::Model::UserJWT::Permissions::Support if user.support meta = token.user - return token if meta.permissions <= held + return token if meta.permissions == held ::PlaceOS::Model::UserJWT.new( iss: token.iss,