ACLS(7) RSpade Developer Manual ACLS(7) NAME acls - Role-based access control with supplementary permissions SYNOPSIS // Check if current user has a permission Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS) // Check if current user has at least a certain role Permission::has_role(User_Model::ROLE_SITE_ADMIN) // Check on specific user instance $user->has_permission(User_Model::PERM_EDIT_DATA) // Check if user can administer another user's role $user->can_admin_role($target_user->role_id) DESCRIPTION RSpade provides a role-based access control (RBAC) system where: 1. Users have a primary role on their site membership (users.role_id) 2. Roles grant a predefined set of permissions 3. Supplementary permissions can GRANT or DENY specific permissions per-user 4. Permission checks resolve: role grants → DENY override → GRANT override Key design principles: Identity vs Membership login_users = identity (email, password, one per person) users = site membership (role, permissions, many per login_user) Roles and permissions attach to site memberships (users table), not login identities. One person can have different roles on different sites. Integer Constants All roles and permissions are integer constants, not strings. This provides type safety, IDE autocompletion, and works with the RSX enum system for magic properties. Hierarchical Roles Roles are hierarchical. Higher roles inherit all permissions from lower roles. Role IDs are ordered by privilege level (lower ID = more privilege). Supplementary Permissions Individual users can have permissions granted or denied beyond their role defaults. DENY always wins over role grants. GRANT adds permissions the role doesn't provide. ARCHITECTURE Database Tables users (site membership) role_id Primary role for this site membership ... Other membership fields user_permissions (supplementary) user_id FK to users permission_id Which permission constant is_grant 1 = GRANT, 0 = DENY Permission Resolution Order 1. Check if user role is DISABLED → deny all 2. Check user_permissions for explicit DENY → deny if found 3. Check user_permissions for explicit GRANT → allow if found 4. Check role's default permissions → allow if included 5. Deny (permission not granted) Role Hierarchy ID Constant Label Can Admin Roles -- -------- ----- --------------- 1 ROLE_ROOT_ADMIN Root Admin 2,3,4,5,6,7 2 ROLE_SITE_OWNER Site Owner 3,4,5,6,7 3 ROLE_SITE_ADMIN Site Admin 4,5,6,7 4 ROLE_MANAGER Manager 5,6,7 5 ROLE_USER User (none) 6 ROLE_VIEWER Viewer (none) 7 ROLE_DISABLED Disabled (none) "Can Admin Roles" means a user with that role can create, edit, or change the role of users with the listed role IDs. This prevents privilege escalation (admin can't create root admin). PERMISSIONS Core Permissions (granted by role) ID Constant Granted By Default To -- -------- --------------------- 1 PERM_MANAGE_SITES_ROOT Root Admin only 2 PERM_MANAGE_SITE_BILLING Site Owner+ 3 PERM_MANAGE_SITE_SETTINGS Site Admin+ 4 PERM_MANAGE_SITE_USERS Site Admin+ 5 PERM_VIEW_USER_ACTIVITY Manager+ 6 PERM_EDIT_DATA User+ 7 PERM_VIEW_DATA Viewer+ Supplementary Permissions (not granted by any role by default) ID Constant Purpose -- -------- ------- 8 PERM_API_ACCESS Allow API key creation/usage 9 PERM_DATA_EXPORT Allow bulk data export Role-Permission Matrix Permission Root Owner Admin Mgr User View Dis ---------- ---- ----- ----- --- ---- ---- --- MANAGE_SITES_ROOT X MANAGE_SITE_BILLING X X MANAGE_SITE_SETTINGS X X X MANAGE_SITE_USERS X X X VIEW_USER_ACTIVITY X X X X EDIT_DATA X X X X X VIEW_DATA X X X X X X API_ACCESS - - - - - - DATA_EXPORT - - - - - - Legend: X = granted by role, - = must be granted individually MODEL IMPLEMENTATION User_Model Definition class User_Model extends Rsx_Model_Abstract { // Role constants const ROLE_ROOT_ADMIN = 1; const ROLE_SITE_OWNER = 2; const ROLE_SITE_ADMIN = 3; const ROLE_MANAGER = 4; const ROLE_USER = 5; const ROLE_VIEWER = 6; const ROLE_DISABLED = 7; // Permission constants const PERM_MANAGE_SITES_ROOT = 1; const PERM_MANAGE_SITE_BILLING = 2; const PERM_MANAGE_SITE_SETTINGS = 3; const PERM_MANAGE_SITE_USERS = 4; const PERM_VIEW_USER_ACTIVITY = 5; const PERM_EDIT_DATA = 6; const PERM_VIEW_DATA = 7; const PERM_API_ACCESS = 8; const PERM_DATA_EXPORT = 9; public static $enums = [ 'role_id' => [ self::ROLE_ROOT_ADMIN => [ 'constant' => 'ROLE_ROOT_ADMIN', 'label' => 'Root Admin', 'permissions' => [ self::PERM_MANAGE_SITES_ROOT, self::PERM_MANAGE_SITE_BILLING, self::PERM_MANAGE_SITE_SETTINGS, self::PERM_MANAGE_SITE_USERS, self::PERM_VIEW_USER_ACTIVITY, self::PERM_EDIT_DATA, self::PERM_VIEW_DATA, ], 'can_admin_roles' => [2,3,4,5,6,7], ], // ... additional roles ], ]; public function has_permission(int $permission): bool { if ($this->role_id === self::ROLE_DISABLED) { return false; } // Check supplementary DENY (overrides everything) if ($this->has_supplementary_deny($permission)) { return false; } // Check supplementary GRANT if ($this->has_supplementary_grant($permission)) { return true; } // Check role default permissions return in_array($permission, $this->role_permissions ?? []); } public function can_admin_role(int $role_id): bool { return in_array($role_id, $this->role_can_admin_roles ?? []); } } Magic Properties (via enum system) $user->role_label // "Site Admin" $user->role_permissions // [3,4,5,6,7] $user->role_can_admin_roles // [4,5,6,7] PERMISSION CLASS API Static Methods (use current session user) Permission::has_permission(int $permission): bool Check if current logged-in user has permission. Returns false if not logged in or no site selected. if (Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS)) { // Show user management UI } Permission::has_role(int $role_id): bool Check if current user has at least the specified role. "At least" means same or higher privilege (lower role_id). if (Permission::has_role(User_Model::ROLE_SITE_ADMIN)) { // User is Site Admin or higher (Owner, Root) } Permission::get_user(): ?User_Model Get current user's site membership record. Returns null if not logged in or no site selected. $user = Permission::get_user(); if ($user && $user->has_permission(User_Model::PERM_EDIT_DATA)) { // ... } Instance Methods (on User_Model) $user->has_permission(int $permission): bool Check if this specific user has permission. $user->can_admin_role(int $role_id): bool Check if user can create/edit users with given role. $user->has_supplementary_grant(int $permission): bool Check if user has explicit GRANT for permission. $user->has_supplementary_deny(int $permission): bool Check if user has explicit DENY for permission. ROUTE PROTECTION Using #[Auth] Attribute Standard permission checks work with #[Auth]: #[Route('/settings/users')] #[Auth('Permission::authenticated()')] public static function users(Request $request, array $params = []) { // Manual permission check inside route if (!Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS)) { return response_error(Ajax::ERROR_UNAUTHORIZED); } // ... } Custom Permission Methods Define reusable permission checks in rsx/permission.php: class Permission { public static function can_manage_users(): bool { return self::has_permission(User_Model::PERM_MANAGE_SITE_USERS); } public static function can_edit(): bool { return self::has_permission(User_Model::PERM_EDIT_DATA); } } Usage in routes: #[Route('/users/create')] #[Auth('Permission::can_manage_users()')] public static function create(Request $request, array $params = []) { // Guaranteed to have PERM_MANAGE_SITE_USERS } SUPPLEMENTARY PERMISSIONS Purpose Supplementary permissions allow per-user exceptions to role defaults: - GRANT: Give a permission the role doesn't include - DENY: Remove a permission the role normally includes Common use cases: - Grant API access to specific users regardless of role - Deny export access to a user who normally has it - Temporary elevated permissions during onboarding Database Table user_permissions id BIGINT PRIMARY KEY user_id BIGINT NOT NULL (FK to users) permission_id INT NOT NULL is_grant TINYINT(1) NOT NULL (1=GRANT, 0=DENY) created_at TIMESTAMP updated_at TIMESTAMP UNIQUE KEY (user_id, permission_id) Management API // Grant a permission User_Permission_Model::grant($user_id, User_Model::PERM_API_ACCESS); // Deny a permission User_Permission_Model::deny($user_id, User_Model::PERM_DATA_EXPORT); // Remove supplementary (revert to role default) User_Permission_Model::remove($user_id, User_Model::PERM_API_ACCESS); // Get all supplementary permissions for user $supplementary = User_Permission_Model::for_user($user_id); Resolution Priority DENY always wins. Order of precedence: 1. Explicit DENY → permission denied 2. Explicit GRANT → permission granted 3. Role default → permission granted if in role 4. Not granted → permission denied Example: User is Site Admin (has PERM_MANAGE_SITE_USERS by role) - No supplementary → has permission (from role) - GRANT added → has permission (redundant but harmless) - DENY added → NO permission (DENY overrides role) - Both GRANT and DENY → NO permission (DENY wins) EXAMPLES Example 1: Check Permission in Controller #[Ajax_Endpoint] #[Auth('Permission::authenticated()')] public static function delete_user(Request $request, array $params = []) { if (!Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS)) { return response_error(Ajax::ERROR_UNAUTHORIZED, 'Cannot manage users'); } $target_id = $params['user_id']; $target = User_Model::find($target_id); // Check can admin this user's role $current = Permission::get_user(); if (!$current->can_admin_role($target->role_id)) { return response_error(Ajax::ERROR_UNAUTHORIZED, 'Cannot modify this user'); } $target->delete(); return ['success' => true]; } Example 2: Conditional UI Based on Permissions // In controller, pass permissions to view return rsx_view('Settings_Users', [ 'can_create' => Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS), 'can_export' => Permission::has_permission(User_Model::PERM_DATA_EXPORT), ]); // In jqhtml template <% if (this.args.can_create) { %> <% } %> Example 3: Role Assignment Validation #[Ajax_Endpoint] public static function update_user_role(Request $request, array $params = []) { $target = User_Model::find($params['user_id']); $new_role_id = $params['role_id']; $current = Permission::get_user(); // Can't change own role if ($target->id === $current->id) { return response_error(Ajax::ERROR_VALIDATION, 'Cannot change own role'); } // Must be able to admin both current and new role if (!$current->can_admin_role($target->role_id)) { return response_error(Ajax::ERROR_UNAUTHORIZED, 'Cannot modify this user'); } if (!$current->can_admin_role($new_role_id)) { return response_error(Ajax::ERROR_UNAUTHORIZED, 'Cannot assign this role'); } $target->role_id = $new_role_id; $target->save(); return ['success' => true]; } Example 4: Grant Supplementary Permission // Admin grants API access to a regular user $user = User_Model::find($user_id); // Verify current user can admin this user if (!Permission::get_user()->can_admin_role($user->role_id)) { throw new Exception('Unauthorized'); } User_Permission_Model::grant($user->id, User_Model::PERM_API_ACCESS); // User now has API access despite being a regular User role Example 5: Check Multiple Permissions // User needs EITHER permission $can_view = Permission::has_permission(User_Model::PERM_VIEW_DATA) || Permission::has_permission(User_Model::PERM_EDIT_DATA); // User needs BOTH permissions $can_admin = Permission::has_permission(User_Model::PERM_MANAGE_SITE_SETTINGS) && Permission::has_permission(User_Model::PERM_MANAGE_SITE_USERS); ADDING NEW PERMISSIONS 1. Add constant to User_Model: const PERM_NEW_FEATURE = 10; 2. Add to role definitions in $enums if role should grant it: self::ROLE_SITE_ADMIN => [ 'permissions' => [ // ... existing self::PERM_NEW_FEATURE, ], ], 3. Run rsx:migrate:document_models to regenerate stubs 4. Use in code: if (Permission::has_permission(User_Model::PERM_NEW_FEATURE)) { // ... } ADDING NEW ROLES 1. Add constant (maintain hierarchy order): const ROLE_SUPERVISOR = 4; // Between Admin and Manager const ROLE_MANAGER = 5; // Renumber if needed // ... 2. Add to $enums with permissions and can_admin_roles: self::ROLE_SUPERVISOR => [ 'constant' => 'ROLE_SUPERVISOR', 'label' => 'Supervisor', 'permissions' => [ self::PERM_VIEW_USER_ACTIVITY, self::PERM_EDIT_DATA, self::PERM_VIEW_DATA, ], 'can_admin_roles' => [5,6,7], ], 3. Update can_admin_roles for roles above: self::ROLE_SITE_ADMIN => [ 'can_admin_roles' => [4,5,6,7], // Add new role ID ], 4. Run migration if role_id column needs updating 5. Run rsx:migrate:document_models FRAMEWORK IMPLEMENTATION DETAILS This section is for framework developers modifying the ACL system. Core Files rsx/models/user_model.php Role and permission constants $enums definition with role metadata has_permission(), can_admin_role() methods Supplementary permission lookup methods rsx/permission.php Permission class with static helper methods has_permission(), has_role(), get_user() Custom permission methods for #[Auth] rsx/models/user_permission_model.php Supplementary permissions CRUD grant(), deny(), remove() static methods Session Integration Permission::get_user() retrieves current site membership: 1. Get login_user_id from Session::get_user_id() 2. Get site_id from Session::get_site_id() 3. Query users WHERE login_user_id AND site_id 4. Cache result for request duration Caching Strategy Supplementary permissions are loaded once per request: 1. First has_permission() call loads all user_permissions 2. Stored in User_Model instance property 3. Subsequent checks use cached data 4. No cache invalidation needed (request-scoped) Enum Integration The $enums system provides magic properties: $user->role_permissions // Array from enum definition $user->role_can_admin_roles // Array from enum definition $user->role_label // String label These are resolved via Rsx_Model_Abstract::__get() FUTURE ENHANCEMENTS Attribute-Based Permission Checks Future goal: Declare permissions directly in route attributes. #[Route('/settings/users')] #[Auth('Permission::authenticated()')] #[RequiresPermission(User_Model::PERM_MANAGE_SITE_USERS)] public static function users(...) This section will be replaced with implementation details when attribute-based permission checking is complete. Custom Filters Future goal: Allow permission modifications based on context. Use cases: - Site-level feature toggles (site disables user management) - Subscription limits (free plan removes export permission) - Time-based access (permission valid only during hours) - Feature flags (A/B testing permission-gated features) Architecture concept: Role Permissions ↓ Custom Filters (modify permission set) ↓ Supplementary GRANT/DENY ↓ Final Permission Decision Filters would be registered callbacks that receive the permission set and context, returning modified permissions. This allows dynamic permission modification without changing role definitions. This section will be replaced with implementation details when custom filters are implemented. MIGRATION FROM LEGACY If upgrading from a system without ACLs: 1. Add role_id column to users table (default ROLE_USER) 2. Create user_permissions table 3. Assign appropriate roles to existing users 4. Add Permission checks to sensitive routes 5. Test with rsx:debug --user_id= to verify SEE ALSO auth - Authentication system (login, sessions, invitations) enums - Enum system for role/permission metadata routing - Route protection with #[Auth] attribute session - Session management and user context RSpade 1.0 November 2024 ACLS(7)