Move resolved_permissions to user object via toArray()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-13 09:04:15 +00:00
parent 6f69534047
commit bb9046af1b
6 changed files with 129 additions and 88 deletions

View File

@@ -285,10 +285,6 @@ abstract class Rsx_Bundle_Abstract
$rsxapp_data['site'] = Session::get_site(); $rsxapp_data['site'] = Session::get_site();
$rsxapp_data['csrf'] = Session::get_csrf_token(); $rsxapp_data['csrf'] = Session::get_csrf_token();
// Add resolved permissions for client-side permission checks
$user = Session::get_user();
$rsxapp_data['resolved_permissions'] = $user ? $user->get_resolved_permissions() : [];
// Add browser error logging flag (enabled in both dev and production) // Add browser error logging flag (enabled in both dev and production)
if (config('rsx.log_browser_errors', false)) { if (config('rsx.log_browser_errors', false)) {
$rsxapp_data['log_browser_errors'] = true; $rsxapp_data['log_browser_errors'] = true;

View File

@@ -2,10 +2,11 @@
* Permission - Client-side permission checking * Permission - Client-side permission checking
* *
* Provides permission and role checking for JavaScript using pre-resolved * Provides permission and role checking for JavaScript using pre-resolved
* permissions from window.rsxapp.resolved_permissions. This mirrors the * permissions from window.rsxapp.user.resolved_permissions. This mirrors the
* PHP Permission class functionality for client-side UI logic. * PHP Permission class functionality for client-side UI logic.
* *
* The resolved_permissions array is computed server-side by applying: * The resolved_permissions array is computed server-side via User_Model::toArray()
* by applying:
* 1. Role default permissions * 1. Role default permissions
* 2. Supplementary GRANTs (added) * 2. Supplementary GRANTs (added)
* 3. Supplementary DENYs (removed) * 3. Supplementary DENYs (removed)
@@ -56,7 +57,7 @@ class Permission {
* @returns {boolean} * @returns {boolean}
*/ */
static has_permission(permission) { static has_permission(permission) {
const permissions = window.rsxapp?.resolved_permissions ?? []; const permissions = window.rsxapp?.user?.resolved_permissions ?? [];
return permissions.includes(permission); return permissions.includes(permission);
} }
@@ -67,7 +68,7 @@ class Permission {
* @returns {boolean} * @returns {boolean}
*/ */
static has_any_permission(permissions) { static has_any_permission(permissions) {
const resolved = window.rsxapp?.resolved_permissions ?? []; const resolved = window.rsxapp?.user?.resolved_permissions ?? [];
return permissions.some(p => resolved.includes(p)); return permissions.some(p => resolved.includes(p));
} }
@@ -78,7 +79,7 @@ class Permission {
* @returns {boolean} * @returns {boolean}
*/ */
static has_all_permissions(permissions) { static has_all_permissions(permissions) {
const resolved = window.rsxapp?.resolved_permissions ?? []; const resolved = window.rsxapp?.user?.resolved_permissions ?? [];
return permissions.every(p => resolved.includes(p)); return permissions.every(p => resolved.includes(p));
} }
@@ -124,6 +125,6 @@ class Permission {
* @returns {number[]} Array of permission IDs * @returns {number[]} Array of permission IDs
*/ */
static get_resolved_permissions() { static get_resolved_permissions() {
return window.rsxapp?.resolved_permissions ?? []; return window.rsxapp?.user?.resolved_permissions ?? [];
} }
} }

View File

@@ -506,4 +506,30 @@ class User_Model extends Rsx_Site_Model_Abstract
return $data; return $data;
} }
// =========================================================================
// SERIALIZATION
// =========================================================================
/**
* Convert model to array with resolved permissions
*
* Adds resolved_permissions and removes role_id__permissions
* (which is redundant since resolved_permissions includes it with
* supplementary grants/denies applied).
*
* @return array
*/
public function toArray()
{
$data = parent::toArray();
// Add resolved permissions
$data['resolved_permissions'] = $this->get_resolved_permissions();
// Remove role_id__permissions (redundant, use resolved_permissions instead)
unset($data['role_id__permissions']);
return $data;
}
} }

View File

@@ -309,9 +309,9 @@ JAVASCRIPT PERMISSION CLASS
Data Source Data Source
The Permission class reads from window.rsxapp.resolved_permissions, The Permission class reads from window.rsxapp.user.resolved_permissions,
which is populated by the bundle renderer from the session user's which is populated via User_Model::toArray() from get_resolved_permissions().
get_resolved_permissions() result. Empty array if not authenticated. Returns empty array if user is not authenticated.
ROUTE PROTECTION ROUTE PROTECTION

View File

@@ -5,13 +5,13 @@ NAME
SYNOPSIS SYNOPSIS
JavaScript: JavaScript:
window.rsxapp.build_key // Manifest build hash window.rsxapp.build_key // Manifest build hash
window.rsxapp.user // Current user model data window.rsxapp.user // Current user model data
window.rsxapp.site // Current site model data window.rsxapp.user.resolved_permissions // Pre-computed user permissions
window.rsxapp.resolved_permissions // Pre-computed user permissions window.rsxapp.site // Current site model data
window.rsxapp.page_data // Custom page-specific data window.rsxapp.page_data // Custom page-specific data
window.rsxapp.is_spa // Whether current page is SPA window.rsxapp.is_spa // Whether current page is SPA
window.rsxapp.csrf // CSRF token for forms window.rsxapp.csrf // CSRF token for forms
DESCRIPTION DESCRIPTION
window.rsxapp is a global JavaScript object rendered with every page bundle. window.rsxapp is a global JavaScript object rendered with every page bundle.
@@ -64,16 +64,17 @@ OBJECT STRUCTURE
Session Data (when authenticated): Session Data (when authenticated):
user Object. Current user model with all fields. user Object. Current user model with all fields.
Includes enum properties (role_id__label, etc.) Includes enum properties (role_id__label, etc.),
and __MODEL marker. resolved_permissions array, and __MODEL marker.
user.resolved_permissions
Array. Pre-computed permission IDs for current user.
Includes role defaults plus supplementary grants,
minus supplementary denies. Use Permission class
for checking: Permission.has_permission(perm_id).
site Object. Current site model. site Object. Current site model.
resolved_permissions Array. Pre-computed permission IDs for current user.
Includes role defaults plus supplementary grants,
minus supplementary denies. Empty array if not
authenticated. Use Permission.has_permission() to check.
Time Synchronization: Time Synchronization:
server_time String. ISO 8601 UTC timestamp from server. server_time String. ISO 8601 UTC timestamp from server.
@@ -120,6 +121,7 @@ EXAMPLE OUTPUT
"email": "test@example.com", "email": "test@example.com",
"role_id": 300, "role_id": 300,
"role_id__label": "Site Owner", "role_id__label": "Site Owner",
"resolved_permissions": [2, 3, 4, 5, 6, 7],
"__MODEL": "User_Model" "__MODEL": "User_Model"
}, },
"site": { "site": {
@@ -128,7 +130,6 @@ EXAMPLE OUTPUT
"timezone": "America/Chicago", "timezone": "America/Chicago",
"__MODEL": "Site_Model" "__MODEL": "Site_Model"
}, },
"resolved_permissions": [2, 3, 4, 5, 6, 7],
"csrf": "f290180b609f8f353c3226accdc798961...", "csrf": "f290180b609f8f353c3226accdc798961...",
"page_data": { "page_data": {
"contact_internal_id": 17 "contact_internal_id": 17
@@ -195,7 +196,7 @@ FRAMEWORK CONSUMERS
Rsx_Storage Reads session_hash, user.id, site.id, build_key for scoping Rsx_Storage Reads session_hash, user.id, site.id, build_key for scoping
Ajax Reads csrf for request headers Ajax Reads csrf for request headers
Spa Reads is_spa, params for routing Spa Reads is_spa, params for routing
Permission Reads resolved_permissions for access control checks Permission Reads user.resolved_permissions for access control checks
Debugger Reads console_debug for output filtering Debugger Reads console_debug for output filtering
SEE ALSO SEE ALSO

View File

@@ -3,78 +3,44 @@ Date: 2026-01-13
SUMMARY SUMMARY
The framework now provides a client-side Permission class for JavaScript The framework now provides a client-side Permission class for JavaScript
permission checking. This uses a new get_resolved_permissions() method on permission checking. User_Model now includes resolved_permissions in its
User_Model that returns pre-computed permissions (role defaults with toArray() output and removes role_id__permissions (which is now redundant).
supplementary grants/denies applied). The resolved permissions array is
automatically included in window.rsxapp.resolved_permissions.
If your application has forked User_Model, you must add the new method. The resolved_permissions array contains the user's final permissions after
applying role defaults, supplementary grants, and supplementary denies.
If your application has forked User_Model, you must:
1. Add get_resolved_permissions() method
2. Add or update toArray() to include resolved_permissions
3. Update any code that checks role_id__permissions directly
AFFECTED FILES AFFECTED FILES
Applications with a forked User_Model (e.g., /rsx/models/user_model.php) - /rsx/models/user_model.php (if forked)
that extends the framework's User_Model need to ensure the parent class - Any JavaScript files checking role_id__permissions directly
method is accessible. - Any PHP files checking role_id__permissions directly
If your User_Model OVERRIDES has_permission(), update it to use the new
pattern.
CHANGES REQUIRED CHANGES REQUIRED
1. If You Have NOT Forked User_Model 1. If You Have NOT Forked User_Model
No action required. The framework's User_Model already has the new No action required. The framework's User_Model already has all new
method and your application will inherit it automatically. methods and your application will inherit them automatically.
2. If You Have Forked User_Model (extends framework User_Model) HOWEVER: Search your codebase for role_id__permissions usage (see
section 5 below).
Ensure your class does NOT override has_permission() or if it does, 2. If You Have Forked User_Model - Add get_resolved_permissions()
update it to use get_resolved_permissions():
BEFORE (if overridden): Add this method to your User_Model:
public function has_permission(int $permission): bool
{
// Custom logic
if ($this->role_id === self::ROLE_DISABLED) {
return false;
}
if ($this->has_supplementary_deny($permission)) {
return false;
}
if ($this->has_supplementary_grant($permission)) {
return true;
}
return in_array($permission, $this->role_id__permissions ?? [], true);
}
AFTER:
public function has_permission(int $permission): bool
{
return in_array($permission, $this->get_resolved_permissions(), true);
}
If you need custom permission logic, override get_resolved_permissions()
instead of has_permission().
3. If You Have Completely Replaced User_Model (not extending framework)
Add the get_resolved_permissions() method to your model:
/**
* Get all resolved permissions for this user
*
* @return array Array of permission IDs the user has
*/
public function get_resolved_permissions(): array public function get_resolved_permissions(): array
{ {
// Return empty for disabled users
if ($this->role_id === self::ROLE_DISABLED) { if ($this->role_id === self::ROLE_DISABLED) {
return []; return [];
} }
// Start with role default permissions
$permissions = $this->role_id__permissions ?? []; $permissions = $this->role_id__permissions ?? [];
// Add supplementary GRANTs
$supplementary = $this->_load_supplementary_permissions(); $supplementary = $this->_load_supplementary_permissions();
foreach ($supplementary['grants'] as $perm_id) { foreach ($supplementary['grants'] as $perm_id) {
if (!in_array($perm_id, $permissions, true)) { if (!in_array($perm_id, $permissions, true)) {
@@ -82,20 +48,70 @@ CHANGES REQUIRED
} }
} }
// Remove supplementary DENYs
$permissions = array_values(array_diff($permissions, $supplementary['denies'])); $permissions = array_values(array_diff($permissions, $supplementary['denies']));
sort($permissions); sort($permissions);
return $permissions; return $permissions;
} }
Then update has_permission() to use it: 3. If You Have Forked User_Model - Add/Update toArray()
If you DON'T have a toArray() override, add one:
public function toArray()
{
$data = parent::toArray();
$data['resolved_permissions'] = $this->get_resolved_permissions();
unset($data['role_id__permissions']);
return $data;
}
If you ALREADY have a toArray() override, add these lines:
$data['resolved_permissions'] = $this->get_resolved_permissions();
unset($data['role_id__permissions']);
4. If You Have Forked User_Model - Update has_permission()
If you override has_permission(), simplify it:
public function has_permission(int $permission): bool public function has_permission(int $permission): bool
{ {
return in_array($permission, $this->get_resolved_permissions(), true); return in_array($permission, $this->get_resolved_permissions(), true);
} }
5. REQUIRED: Search and Replace role_id__permissions Usage
Search your /rsx/ directory for any direct usage of role_id__permissions:
grep -r "role_id__permissions" rsx/
For each match, update to use the new pattern:
JAVASCRIPT - BEFORE:
if (window.rsxapp.user.role_id__permissions.includes(User_Model.PERM_EDIT_DATA)) {
// or
const perms = user.role_id__permissions;
JAVASCRIPT - AFTER:
if (Permission.has_permission(User_Model.PERM_EDIT_DATA)) {
// or
const perms = Permission.get_resolved_permissions();
// or
const perms = window.rsxapp.user.resolved_permissions;
PHP - BEFORE:
if (in_array($perm, $user->role_id__permissions)) {
PHP - AFTER:
if ($user->has_permission($perm)) {
// or
if (in_array($perm, $user->get_resolved_permissions())) {
IMPORTANT: role_id__permissions only contains role defaults. It does NOT
include supplementary grants or respect supplementary denies. Always use
resolved_permissions or the Permission class for accurate permission checks.
NEW FEATURES AVAILABLE NEW FEATURES AVAILABLE
PHP: PHP:
@@ -110,15 +126,16 @@ NEW FEATURES AVAILABLE
Permission.can_admin_role(User_Model.ROLE_USER) Permission.can_admin_role(User_Model.ROLE_USER)
Permission.get_resolved_permissions() Permission.get_resolved_permissions()
window.rsxapp: window.rsxapp.user:
window.rsxapp.resolved_permissions // Array of permission IDs window.rsxapp.user.resolved_permissions // Array of permission IDs
VERIFICATION VERIFICATION
1. Load any authenticated page 1. Load any authenticated page
2. Open browser console 2. Open browser console
3. Verify: window.rsxapp.resolved_permissions is an array of integers 3. Verify: window.rsxapp.user.resolved_permissions is an array of integers
4. Verify: Permission.has_permission(User_Model.PERM_VIEW_DATA) returns boolean 4. Verify: Permission.has_permission(User_Model.PERM_VIEW_DATA) returns boolean
5. If you have a forked User_Model, verify no PHP errors on page load 5. Verify: window.rsxapp.user.role_id__permissions is undefined
6. If you have a forked User_Model, verify no PHP errors on page load
REFERENCE REFERENCE
php artisan rsx:man acls php artisan rsx:man acls