Files
rspade_system/app/RSpade/man/acls.txt
root 84ca3dfe42 Fix code quality violations and rename select input components
Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:39:43 +00:00

599 lines
20 KiB
Plaintext
Executable File

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) { %>
<button @click=this.create_user>Add User</button>
<% } %>
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)