glueful / aegis
Aegis: Role-Based Access Control extension for Glueful
Requires
- php: ^8.3
Requires (Dev)
- glueful/framework: ^1.56.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
README
Overview
Aegis provides a comprehensive, modern Role-Based Access Control (RBAC) system for your Glueful application. It implements hierarchical roles, direct user permissions, resource-level filters, and optional audit logging.
Features
- Hierarchical roles: Create nested roles where child roles inherit ancestor permissions
- Direct user permissions: Per-user grants that augment role permissions
- Resource-level filters: Limit permissions to specific resources/types
- Temporal permissions: Expiry on roles and direct grants
- Scoped access: Multi-tenant friendly with scoping
- Audit service: Structured audit helpers + optional check logging
- Targeted caching: Catalog and non-temporal lookup caches via CacheStore
- REST API: Full CRUD + assignment endpoints
- Flexible config: Tunable caching, inheritance, and logging
Installation
Installation (Recommended)
Install via Composer
composer require glueful/aegis
# Rebuild the extensions cache after adding new packages
php glueful extensions:cache
Composer discovers packages of type glueful-extension, but installing does not auto-enable them — the provider must be added to config/extensions.php's enabled allow-list. The CLI does that for you:
# Enable (adds the provider FQCN to config/extensions.php + recompiles the cache) php glueful extensions:enable aegis # Disable (removes it) php glueful extensions:disable aegis
In production, manage the enabled list in config and run php glueful extensions:cache in your deploy step.
Run database migrations (if not auto-run by your workflow):
php glueful migrate:run
Local Development Installation
To develop the extension locally, register it as a Composer path repository in your app's composer.json, then require and enable it:
// composer.json "repositories": [ { "type": "path", "url": "extensions/aegis", "options": { "symlink": true } } ]
composer require glueful/aegis:@dev php glueful extensions:enable aegis
Entries in config/extensions.php are plain string FQCNs (no ::class) — prefer extensions:enable over editing by hand.
Run the migrations to create the necessary database tables:
php glueful migrate run
- Generate API documentation (optional, if your tooling supports it):
php glueful generate:json doc
- Restart your web server to apply the changes.
Verify Installation
Check status and details:
php glueful extensions:list php glueful extensions:info aegis php glueful extensions:diagnose
Post-install checklist:
- Run migrations (if not auto-run):
php glueful migrate run - Hit an endpoint to verify:
GET /rbac/roles - Rebuild cache after Composer operations:
php glueful extensions:cache - Check logs for initialization messages or errors
Quick Start
Create a role, assign it to a user, and verify via the API. Replace placeholders before running:
API_BASEwith your base URL (e.g., http://localhost:8000)TOKENwith a valid bearer tokenUSER_UUIDwith an existing user's UUID
API_BASE=http://localhost:8000 TOKEN="<YOUR_BEARER_TOKEN>" USER_UUID="<AN_EXISTING_USER_UUID>" # 1) Create a role create_resp=$(curl -s -X POST "$API_BASE/rbac/roles" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Editor", "slug": "editor", "description": "Can edit content" }') # Extract role UUID (requires jq). If jq is unavailable, inspect $create_resp ROLE_UUID=$(printf "%s" "$create_resp" | jq -r '.data.uuid') echo "Created role UUID: $ROLE_UUID" # 2) Assign the role to a user curl -s -X POST "$API_BASE/rbac/roles/$ROLE_UUID/assign" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\n \"user_uuid\": \"$USER_UUID\",\n \"scope\": {\"tenant_id\": \"tenant_1\"}\n }" | jq . # 3a) Verify: list the user's roles curl -s "$API_BASE/rbac/users/$USER_UUID/roles" \ -H "Authorization: Bearer $TOKEN" | jq . # 3b) Verify: explicit role check by slug curl -s -X POST "$API_BASE/rbac/users/$USER_UUID/check-role" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role_slug": "editor" }' | jq .
Quick Start (PHP)
Programmatic equivalent using the container and services:
<?php use Glueful\Extensions\Aegis\Services\RoleService; use Glueful\Extensions\Aegis\AegisPermissionProvider; // Resolve services from the container $roleService = container()->get(RoleService::class); $provider = container()->get(AegisPermissionProvider::class); $userUuid = '<AN_EXISTING_USER_UUID>'; // 1) Create a role $role = $roleService->createRole([ 'name' => 'Editor', 'slug' => 'editor', 'description' => 'Can edit content', ]); // 2) Assign the role to the user $roleService->assignRoleToUser($userUuid, $role->getUuid(), [ 'scope' => ['tenant_id' => 'tenant_1'], ]); // 3a) Verify via provider $hasRole = $provider->hasRole($userUuid, 'editor'); // 3b) Or verify via RoleService helper $hasRole2 = $roleService->userHasRole($userUuid, 'editor'); // Optional: check permissions using the permission manager $permissionManager = container()->get('permission.manager'); $canEdit = $permissionManager->can($userUuid, 'posts.edit', 'post:123'); var_dump([ 'role_uuid' => $role->getUuid(), 'has_editor_role' => $hasRole, 'has_editor_role_via_service' => $hasRole2, 'can_edit_post' => $canEdit, ]);
Database Schema
Aegis creates the following tables:
roles: Role definitions with hierarchy supportpermissions: Permission definitions with categories and resource typesrole_permissions: Role-to-permission mappingsuser_roles: User role assignments with scope and expiry supportuser_permissions: Direct user permission assignmentspermission_audit: Reserved schema for database audit trails; the current audit service writes structured events to therbac_auditlog channel.
Configuration
config/rbac.php ships empty so the extension can run with framework defaults. Applications can override the following keys in their app config:
<?php return [ 'roles' => [ // Inherit permissions up the role hierarchy 'inherit_permissions' => true, // Maximum depth for hierarchy resolution 'max_hierarchy_depth' => 10, ], 'permissions' => [ // Distributed caching for user permissions 'cache_enabled' => true, // TTL for user permissions (seconds) 'cache_ttl' => 3600, 'cache_prefix' => 'rbac:', // Enable role-permission inheritance 'inheritance_enabled' => true, ], 'logging' => [ // When true, logs each permission check (verbose) 'log_check_operations' => false, ], ];
Usage
Basic Permission Checking
use Glueful\Permissions\PermissionManager; $permissionManager = container()->get('permission.manager'); // Check if user has permission $canEdit = $permissionManager->can($userUuid, 'posts.edit', 'post:123'); // Check with additional context (e.g., tenant scope) $canDelete = $permissionManager->can($userUuid, 'posts.delete', 'post:123', [ 'scope' => ['tenant_id' => 'tenant_1'], ]);
Role Management
use Glueful\Extensions\Aegis\Services\RoleService; $roleService = container()->get(RoleService::class); // Create a new role $adminRole = $roleService->createRole([ 'name' => 'Administrator', 'slug' => 'admin', 'description' => 'System administrator with full access', ]); // Create a child role $moderatorRole = $roleService->createRole([ 'name' => 'Moderator', 'slug' => 'moderator', 'parent_uuid' => $adminRole->getUuid(), 'description' => 'Content moderator', ]); // Assign role to user $roleService->assignRoleToUser($userUuid, $adminRole->getUuid(), [ 'expires_at' => '2024-12-31 23:59:59', 'scope' => ['tenant_id' => 'tenant_1'], ]);
Permission Management
use Glueful\Extensions\Aegis\Services\PermissionAssignmentService; $permissionService = container()->get(PermissionAssignmentService::class); // Create a new permission $permission = $permissionService->createPermission([ 'name' => 'Edit Posts', 'slug' => 'posts.edit', 'description' => 'Ability to edit blog posts', 'category' => 'content', 'resource_type' => 'posts', ]); // Assign permission directly to user (adds to role permissions) $permissionService->assignPermissionToUser( $userUuid, 'posts.edit', 'post:123', // Specific resource [ 'expires_at' => '2024-06-30 23:59:59', 'constraints' => ['ip_range' => '192.168.1.0/24'], ] ); // Batch assign permissions $permissionService->batchAssignPermissions($userUuid, [ ['permission' => 'posts.read', 'resource' => '*'], ['permission' => 'posts.edit', 'resource' => 'post:123'], ['permission' => 'comments.moderate', 'resource' => '*'], ]);
Direct user grants use exact resource matching. Wildcard resource filters are supported on role-permission grants; direct user grants are intended for precise per-user exceptions.
Using the RBAC Permission Provider
The Aegis provider is auto-registered with the framework’s permission manager. You can either use the manager (recommended) or get the provider directly:
use Glueful\Extensions\Aegis\AegisPermissionProvider; // Recommended: use the permission manager for checks $permissionManager = container()->get('permission.manager'); $can = $permissionManager->can($userUuid, 'posts.edit', 'post:123'); // Direct access to provider when needed $rbacProvider = container()->get(AegisPermissionProvider::class); // Assign a role $rbacProvider->assignRole($userUuid, 'editor', [ 'scope' => ['tenant_id' => 'tenant_1'], 'expires_at' => '2024-12-31 23:59:59', ]); // Check if user has role $hasRole = $rbacProvider->hasRole($userUuid, 'admin'); // Get user's effective permissions $permissions = $rbacProvider->getUserPermissions($userUuid);
API Endpoints
All endpoints are prefixed with /rbac and require authentication. Highlights include:
Roles
GET /rbac/roles– List roles (with filters/pagination)POST /rbac/roles– Create a roleGET /rbac/roles/{uuid}– Get role detailsPUT /rbac/roles/{uuid}– Update a roleDELETE /rbac/roles/{uuid}– Delete a role- Extra:
GET /rbac/roles/stats, bulk operations, assign/revoke users
Permissions
GET /rbac/permissions– List permissions (with filters/pagination)POST /rbac/permissions– Create a permissionGET /rbac/permissions/{uuid}– Get permission detailsPUT /rbac/permissions/{uuid}– Update a permissionDELETE /rbac/permissions/{uuid}– Delete a permission- Extra:
GET /rbac/permissions/stats,POST /rbac/permissions/cleanup-expired, categories, resource-types
Users
GET /rbac/users/{user_uuid}/roles– List a user’s rolesPOST /rbac/users/{user_uuid}/roles– Assign roles to a userDELETE /rbac/users/{user_uuid}/roles/{role_uuid}– Revoke a user’s roleGET /rbac/users/{user_uuid}/permissions– List direct permissionsPOST /rbac/users/{user_uuid}/permissions– Grant direct permissionsDELETE /rbac/users/{user_uuid}/permissions/{permission_uuid}– Revoke direct permission
Hierarchical Roles
The RBAC system supports role hierarchy:
// Create role hierarchy: Admin -> Manager -> Employee $adminRole = $roleService->createRole([ 'name' => 'Administrator', 'slug' => 'admin', 'level' => 0 ]); $managerRole = $roleService->createRole([ 'name' => 'Manager', 'slug' => 'manager', 'parent_uuid' => $adminRole->getUuid(), 'level' => 1 ]); $employeeRole = $roleService->createRole([ 'name' => 'Employee', 'slug' => 'employee', 'parent_uuid' => $managerRole->getUuid(), 'level' => 2 ]); // Users with the employee role inherit manager and admin permissions through their ancestors.
Scoped Permissions
Support multi-tenant environments with scoped permissions:
// Assign role with scope $roleService->assignRoleToUser($userUuid, $managerRole->getUuid(), [ 'scope' => [ 'tenant_id' => 'tenant_1', 'department' => 'marketing' ] ]); // Check permission with scope context $canAccess = $permissionManager->can($userUuid, 'reports.view', '*', [ 'scope' => ['tenant_id' => 'tenant_1'] ]);
Audit Logging
An audit service is provided for RBAC-related events, and permission-check logging can be enabled via config:
use Glueful\Extensions\Aegis\Services\AuditService; $audit = container()->get(AuditService::class); $audit->logSecurityEvent('unauthorized_access', ['path' => '/rbac/roles'], $userUuid ?? null); // In app config: // 'logging' => ['log_check_operations' => true] // When enabled, permission checks are logged to the 'rbac_audit' channel.
Caching
- Catalog and lookup caches: Non-temporal catalog lookups and permission summaries can use in-process or distributed cache.
- Temporal safety: Final authorization decisions and active temporal grants are not cached past their
expires_at.
// Clear a user’s RBAC cache $rbacProvider->invalidateUserCache($userUuid); // Clear all RBAC cache entries $rbacProvider->invalidateAllCache();
Performance Considerations
- Batched lookups to minimize N+1 queries
- Prefer role-level resource filters and scoped assignments to keep permission sets focused
Security Considerations
- System roles/permissions can be protected
- Circular hierarchies are prevented
- All endpoints require authentication and proper permissions
- Optional audit trails improve accountability
- Expiring grants are evaluated against the database on the active authorization path
Migration from Legacy Systems
If migrating from an existing permission system:
- Export existing roles and permissions
- Use the batch assignment APIs to recreate the structure
- Test thoroughly with your existing codebase
- Update permission checks to use the new RBAC provider
Troubleshooting
Common Issues
- Permissions not working: Confirm Aegis is enabled and DB tables exist; use
permission.managerfor checks. - Cache issues: Clear RBAC cache via
invalidateUserCache()/invalidateAllCache(). - Performance issues: Use role-level resource filters and scoped assignments to keep permission sets focused.
- Audit logs not appearing: Enable
logging.log_check_operationsand verify yourrbac_auditlog channel.
Debugging
Set app-level rbac config overrides to tune caches or enable check logging as needed.
Requirements
- PHP 8.3 or higher
- Glueful 1.55.0 or higher
- MySQL, PostgreSQL, or SQLite database
- Redis or Memcached (optional, for distributed caching)
License
This extension is licensed under the same license as the Glueful framework.
Support
For issues, feature requests, or questions, please create an issue in the repository.