hosseinhezami / laravel-permission-manager
The most advanced enterprise-grade permission management system for Laravel. RBAC + ABAC + Role Hierarchy + Multi-Tenancy + Audit Logging.
Package info
github.com/hosseinhezami/laravel-permission-manager
pkg:composer/hosseinhezami/laravel-permission-manager
Requires
- php: ^8.2
- illuminate/cache: ^10.0|^11.0|^12.0|^13.0
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0
README
The most advanced, enterprise-grade permission management system for Laravel applications.
RBAC + ABAC + Role Hierarchy + Multi-Tenancy + Audit Logging + Condition Engine
๐ Table of Contents
- โจ Why This Package?
- ๐ Features
- ๐ฆ Installation
- โก Quick Start (5 minutes)
- ๐ฏ Core Concepts
- ๐ข Enterprise Features
- ๐ก๏ธ Middleware DSL
- ๐จ Blade Directives
- ๐ Laravel Gate & Policy Integration
- ๐ป Facade API
- ๐ฅ๏ธ Artisan Commands
- ๐งช Testing Helpers
- โ๏ธ Configuration
- ๐ Comparison with Spatie
- ๐บ๏ธ Roadmap
- ๐ค Contributing
- ๐ License
โจ Why This Package?
Most Laravel permission packages only offer basic RBAC. This package goes far beyond that, combining RBAC + ABAC + Policy-based authorization + Role Hierarchy + Multi-Tenancy into a single, cohesive engine.
// Basic RBAC $user->hasRole('admin'); $user->hasPermissionTo('users.edit'); // Direct permissions with expiry $user->givePermissionTo('reports.export', expiresAt: now()->addDay()); // Role hierarchy - editor inherits all permissions from viewer $editor->inheritFrom('viewer'); // Explicit deny overrides everything $user->denyPermissionTo('users.delete'); // ABAC - contextual permissions $user->canPermission('posts.update', $post); // Only if $post->owner_id === $user->id AND $post->status === 'draft' // Multi-tenancy - different roles per team $user->assignRoleForTeam('admin', $engineeringTeam); $user->assignRoleForTeam('editor', $marketingTeam); // Audit trail - who did what, when PermissionAudit::byActor($admin->id)->action('granted')->get(); // Explain API - debug why access was denied PermissionManager::explain($user, 'orders.delete');
๐ Features
Core Authorization Engine
- โ RBAC โ Role-Based Access Control with multiple roles per user
- โ Direct Permissions โ Assign permissions directly to users without roles
- โ Explicit Allow/Deny โ Fine-grained control with precedence rules
- โ Role Hierarchy โ Multi-level inheritance with cycle detection
- โ
Wildcard Permissions โ Flexible pattern matching (
users.*,*.edit,!users.delete) - โ Temporary Permissions โ Time-based access with automatic expiry
- โ Permission Groups & Sets โ Organize permissions logically
- โ
Multi-Guard Support โ Isolated permissions per guard (
web,api,admin) - โ Super Admin Bypass โ Configurable root access
- โ Authorization Result โ Detailed reasons for allow/deny decisions
Enterprise Features
- ๐ข Teams / Multi-Tenancy โ Different roles per team/tenant
- ๐ง ABAC (Attribute-Based Access Control) โ Context-aware permissions
- ๐ Condition Engine โ JSON-based rules without
eval() - ๐ Audit Logging โ Track all permission changes
- ๐ Authorization Audit Trail โ Log access attempts (optional)
- ๐ฏ Policy Integration โ Native Laravel
Gate::before()integration - ๐ Route Sync โ Auto-generate permissions from routes
- ๐ฆ Resource Generator โ Auto-create CRUD permissions for models
Developer Experience
- ๐จ Advanced Blade Directives โ
@role,@permission,@hasAnyRole,@unlessPermission - ๐ก๏ธ Middleware DSL โ
pm:permission:any:edit,view,role:admin|manager - ๐ป Rich Facade API โ
PermissionManager::user(),::role(),::explain() - ๐ฅ๏ธ Powerful CLI โ
permission:doctor,permission:why,permission:tree - ๐งช Testing Helpers โ
actingAsRole(),assertHasPermission() - โก Smart Caching โ Tagged cache with hierarchical invalidation
- ๐ค Import/Export โ JSON, CSV, YAML support
- ๐ฏ Permission Snapshot โ Debug user's complete authorization state
๐ฆ Installation
Step 1: Install via Composer
composer require hosseinhezami/laravel-permission-manager
Step 2: Publish Configuration & Migrations
php artisan vendor:publish --provider="HosseinHezami\PermissionManager\PermissionManagerServiceProvider" --tag="config" php artisan vendor:publish --provider="HosseinHezami\PermissionManager\PermissionManagerServiceProvider" --tag="migrations"
Step 3: Run Migrations
php artisan migrate
This creates 14 tables:
roles,permissions,permission_groups,permission_sets,permission_set_itemsrole_permissions,role_inheritsuser_roles,user_permissionsteams,team_userpermission_conditions,permission_audits,authorization_logs
Step 4: Add Trait to User Model
namespace App\Models; use HosseinHezami\PermissionManager\Traits\PermissionTrait; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use PermissionTrait; }
Step 5: (Optional) Quick Install Command
php artisan permission-manager:install --migrate
โก Quick Start (5 minutes)
1. Create a Role
use HosseinHezami\PermissionManager\Models\Role; $admin = Role::create([ 'name' => 'Administrator', 'slug' => 'admin', 'description' => 'Full system access', ]);
Or via CLI:
php artisan role:create admin "Administrator" "Full system access"
2. Create Permissions
use HosseinHezami\PermissionManager\Models\Permission; Permission::create(['route' => 'users.view']); Permission::create(['route' => 'users.edit']); Permission::create(['route' => 'users.delete']);
Or sync all your routes automatically:
php artisan permission:sync-routes
3. Assign Permissions to Role
$admin->assignPermission(['users.view', 'users.edit']); // Or with wildcard $admin->assignPermission('users.*');
4. Assign Role to User
$user = User::find(1); $user->assignRole('admin'); // Check permissions $user->hasPermissionTo('users.edit'); // true $user->hasPermissionTo('users.delete'); // false
5. Protect Your Routes
// Single permission Route::get('/users', [UserController::class, 'index']) ->middleware('pm:permission:users.view'); // Any of these permissions Route::get('/users', [UserController::class, 'index']) ->middleware('pm:permission:any:users.view,users.list'); // All of these permissions Route::post('/users', [UserController::class, 'store']) ->middleware('pm:permission:all:users.view,users.create'); // Role check Route::get('/admin', function () { /* ... */ }) ->middleware('pm:role:admin|manager');
6. Use in Blade
@role('admin') <a href="{{ route('admin.dashboard') }}">Dashboard</a> @endrole @permission('users.edit') <button>Edit User</button> @endpermission @unlesspermission('users.delete') <span class="text-muted">Delete disabled</span> @endunlesspermission
๐ฏ Core Concepts
Roles & Permissions
// Create role $editor = Role::create(['name' => 'Editor', 'slug' => 'editor']); // Create permission $permission = Permission::create(['route' => 'posts.publish']); // Assign permission to role $editor->assignPermission('posts.publish'); // Assign role to user $user->assignRole('editor'); // Check $user->hasRole('editor'); // true $user->hasPermissionTo('posts.publish'); // true
Direct Permissions
Sometimes you need to grant a permission to a specific user without creating a role:
// Give a one-time permission $user->givePermissionTo('reports.export'); // Check direct permission $user->hasDirectPermission('reports.export'); // true // Revoke $user->revokePermissionTo('reports.export');
Why it matters: Perfect for exceptional cases, temporary access, or overriding role-based permissions.
Allow / Deny System
The deny permission has higher priority than allow. This is powerful for exceptions:
// Role grants all posts.* permissions $editor->assignPermission('posts.*'); // But deny deleting posts specifically $user->denyPermissionTo('posts.delete'); // Result $user->hasPermissionTo('posts.edit'); // true (from role) $user->hasPermissionTo('posts.delete'); // false (explicit deny overrides)
Resolution Order (highest to lowest priority)
1. Super Admin bypass (if configured)
2. Explicit User DENY
3. Explicit Role DENY
4. Explicit User ALLOW
5. Role ALLOW
6. Inherited Role permissions
7. Default: DENY
Role Hierarchy
Roles can inherit permissions from other roles, creating a hierarchy:
// Create hierarchy: super-admin > admin > editor > viewer $superAdmin = Role::create(['name' => 'Super Admin', 'slug' => 'super-admin']); $admin = Role::create(['name' => 'Admin', 'slug' => 'admin']); $editor = Role::create(['name' => 'Editor', 'slug' => 'editor']); $viewer = Role::create(['name' => 'Viewer', 'slug' => 'viewer']); // Assign specific permissions $viewer->assignPermission('posts.view'); $editor->assignPermission('posts.edit'); $admin->assignPermission('posts.delete'); $superAdmin->assignPermission('system.config'); // Build hierarchy $admin->inheritFrom('editor'); // admin gets editor's permissions $editor->inheritFrom('viewer'); // editor gets viewer's permissions $superAdmin->inheritFrom('admin'); // super-admin gets admin's permissions // A user with super-admin role now has ALL permissions $user->assignRole('super-admin'); $user->hasPermissionTo('posts.view'); // โ from viewer (via editor โ admin) $user->hasPermissionTo('posts.edit'); // โ from editor (via admin) $user->hasPermissionTo('posts.delete'); // โ from admin $user->hasPermissionTo('system.config'); // โ from super-admin
Cycle Detection
The system automatically prevents circular inheritance:
$roleA->inheritFrom('roleB'); $roleB->inheritFrom('roleA'); // โ Throws CyclicRoleInheritanceException
Wildcard Permissions
Use wildcards for flexible permission matching:
// Match any route starting with 'users.' $role->assignPermission('users.*'); // Now the user has: $user->hasPermissionTo('users.view'); // true $user->hasPermissionTo('users.edit'); // true $user->hasPermissionTo('users.delete'); // true // Match routes ending with '.edit' $role->assignPermission('*.edit'); $user->hasPermissionTo('posts.edit'); // true $user->hasPermissionTo('users.edit'); // true // Global wildcard (DANGEROUS - use carefully!) $role->assignPermission('*'); // All permissions // Negation - deny specific pattern $role->assignPermission('!users.delete');
Wildcard Examples
| Pattern | Matches | Doesn't Match |
|---|---|---|
users.* |
users.edit, users.delete |
posts.edit |
*.edit |
users.edit, posts.edit |
users.view |
users.{id}.edit |
users.42.edit |
users.edit |
* |
everything | nothing |
!users.delete |
everything except users.delete |
users.delete |
๐ข Enterprise Features
Teams / Multi-Tenancy
Perfect for SaaS applications where users belong to multiple teams:
use HosseinHezami\PermissionManager\Models\Team; // Create teams $engineering = Team::createTeam(['name' => 'Engineering', 'slug' => 'engineering']); $marketing = Team::createTeam(['name' => 'Marketing', 'slug' => 'marketing']); // User joins both teams $user->joinTeam($engineering); $user->joinTeam($marketing); // Assign different roles per team $user->assignRoleForTeam('admin', $engineering); $user->assignRoleForTeam('editor', $marketing); // Check team-specific roles $user->hasRoleForTeam('admin', $engineering); // true $user->hasRoleForTeam('admin', $marketing); // false $user->hasRoleForTeam('editor', $marketing); // true
Using Team Context
Set the current team context via middleware:
// In routes/api.php Route::middleware(['pm.team:header,X-Team-Id'])->group(function () { // All permissions in this group are scoped to the team from X-Team-Id header });
Or programmatically:
use HosseinHezami\PermissionManager\Facades\PermissionManager; PermissionManager::setTeam($currentTeam); // All subsequent permission checks are scoped to this team
ABAC & Condition Engine
Attribute-Based Access Control lets you define dynamic conditions:
use HosseinHezami\PermissionManager\Models\PermissionCondition; $permission = Permission::create(['route' => 'posts.update']); // Only allow updates if user is the post owner AND status is draft PermissionCondition::create([ 'permission_id' => $permission->id, 'name' => 'owner-and-draft', 'conditions' => [ 'all' => [ ['field' => 'user.id', 'operator' => '=', 'value' => 'resource.owner_id'], ['field' => 'resource.status', 'operator' => '=', 'value' => 'draft'], ], ], ]); // Usage $user->givePermissionTo('posts.update'); $post = Post::find(1); $user->canPermission('posts.update', $post); // Returns true only if $user->id === $post->owner_id AND $post->status === 'draft'
Supported Operators
| Operator | Description | Example |
|---|---|---|
= == |
Equal | user.id = resource.owner_id |
!= !== |
Not equal | user.role != "banned" |
> >= |
Greater than | user.level >= resource.required_level |
< <= |
Less than | user.failed_attempts < 3 |
in |
In array | resource.status in ["draft", "pending"] |
not_in |
Not in array | user.id not_in [1, 2, 3] |
contains |
String contains | user.email contains "@company.com" |
starts_with |
String starts with | resource.path starts_with "admin/" |
ends_with |
String ends with | resource.mime ends_with "pdf" |
exists |
Is not null | resource.published_at exists |
not_exists |
Is null | resource.deleted_at not_exists |
Logical Operators
// AND logic (all must match) ['all' => [ ['field' => 'user.id', 'operator' => '=', 'value' => 'resource.owner_id'], ['field' => 'resource.status', 'operator' => '=', 'value' => 'draft'], ]] // OR logic (any must match) ['any' => [ ['field' => 'user.id', 'operator' => '=', 'value' => 'resource.owner_id'], ['field' => 'user.is_admin', 'operator' => '=', 'value' => true], ]] // NOT logic ['not' => [ 'field' => 'resource.status', 'operator' => '=', 'value' => 'archived', ]] // Complex nesting ['all' => [ ['field' => 'resource.status', 'operator' => '!=', 'value' => 'archived'], ['any' => [ ['field' => 'user.id', 'operator' => '=', 'value' => 'resource.owner_id'], ['field' => 'user.role', 'operator' => '=', 'value' => 'admin'], ]], ]]
Temporary Permissions
Grant time-limited access:
// Give permission that expires in 24 hours $user->givePermissionTo( 'reports.export', 'allow', now()->addDay() ); // Permission is valid until expiration $user->hasPermissionTo('reports.export'); // true now // After 24 hours: false (automatically) // Prune expired permissions (run as scheduled job) php artisan permission:prune --days=7
Audit Logging
Track who changed what and when:
use HosseinHezami\PermissionManager\Models\PermissionAudit; // Automatically logged when you: $user->assignRole('admin'); $role->assignPermission('users.delete'); $user->givePermissionTo('reports.export'); // Query audit log $recentChanges = PermissionAudit::latest() ->limit(50) ->get(); // Filter by actor (who made the change) $adminChanges = PermissionAudit::byActor($adminId)->get(); // Filter by action $grants = PermissionAudit::action('granted')->get(); // Each audit includes: // - actor_id: who made the change // - action: what happened (granted, revoked, created, etc.) // - subject_type: user, role, permission // - subject_id: the target of the action // - ip_address: where it came from // - user_agent: browser info // - metadata: extra data
Enable Audit Logging
// config/permission-manager.php 'audit' => [ 'enabled' => true, 'log_mutations' => true, ],
Authorization Audit Trail
Optionally log every permission check (useful for security audits):
// config/permission-manager.php 'authorization_logging' => [ 'enabled' => true, 'denied_only' => true, // Only log failed attempts 'sample_rate' => 0.1, // Log 10% of checks (for high-traffic apps) ],
Multi-Guard
Isolate permissions by authentication guard:
// Create web-only role Role::create([ 'name' => 'Web Admin', 'slug' => 'web-admin', 'guard_name' => 'web', ]); // Create API-only role Role::create([ 'name' => 'API Admin', 'slug' => 'api-admin', 'guard_name' => 'api', ]); // Query by guard $webRoles = Role::forGuard('web')->get(); $apiRoles = Role::forGuard('api')->get(); // Permissions are also guard-scoped Permission::forGuard('api')->get();
๐ก๏ธ Middleware DSL
Advanced middleware with a powerful DSL:
Permission Checks
// Single permission Route::get('/users', fn() => '...')->middleware('pm:permission:users.view'); // ANY of these (OR logic) Route::get('/users', fn() => '...') ->middleware('pm:permission:any:users.view,users.list,users.index'); // ALL of these (AND logic) Route::post('/users', fn() => '...') ->middleware('pm:permission:all:users.view,users.create'); // NOT this permission Route::get('/public', fn() => '...') ->middleware('pm:permission:not:admin.panel');
Role Checks
// Single role Route::get('/admin', fn() => '...')->middleware('pm:role:admin'); // ANY of these roles (OR logic) Route::get('/staff', fn() => '...') ->middleware('pm:role:any:admin,manager,editor'); // ALL of these roles (AND logic) - user must have ALL Route::get('/privileged', fn() => '...') ->middleware('pm:role:all:verified,premium');
Combined Middleware
// Multiple directives (AND logic between them) Route::get('/reports', fn() => '...') ->middleware([ 'pm:role:admin', 'pm:permission:reports.view', ]);
Dedicated Role Middleware
// Using pipe (|) for OR Route::get('/staff', fn() => '...') ->middleware('role:admin|manager|editor'); // Using comma (,) for AND Route::get('/premium', fn() => '...') ->middleware('role:verified,premium');
๐จ Blade Directives
Role Directives
@role('admin') <span>Welcome, Administrator!</span> @endrole @hasanyrole(['admin', 'editor']) <span>You can edit content</span> @endhasanyrole @hasallroles(['verified', 'premium']) <span>Premium Verified User</span> @endhasallroles @unlessrole('banned') <span>You are not banned</span> @endunlessrole
Permission Directives
@permission('users.edit') <button>Edit User</button> @endpermission @hasanypermission(['users.edit', 'users.delete']) <span>You can modify users</span> @endhasanypermission @hasallpermissions(['users.view', 'users.edit', 'users.delete']) <span>Full user management access</span> @endhasallpermissions @unlesspermission('users.delete') <span>Delete permission required</span> @endunlesspermission @cannotpermission('users.delete') <span>Cannot delete users</span> @endcannotpermission
Contextual Permission Directive
@canpermission('posts.update', $post) <a href="{{ route('posts.edit', $post) }}">Edit Post</a> @endcanpermission
Legacy Directives (Backward Compatible)
@hasRole('admin') ... @endHasRole @hasPermission('users.edit') ... @endHasPermission
๐ Laravel Gate & Policy Integration
Automatic Gate Integration
All permissions are automatically registered with Laravel's Gate:
// All these work out of the box: $user->can('users.edit'); $user->cannot('users.delete'); // In Blade @can('users.edit') <button>Edit</button> @endcan // In Controllers Gate::authorize('users.edit'); Gate::allows('users.edit'); Gate::denies('users.delete');
Custom Abilities with Policies
Define custom abilities with complex logic:
use HosseinHezami\PermissionManager\Facades\PermissionManager; PermissionManager::define('posts.update', function ($user, $post) { return $post->user_id === $user->id || $user->hasPermissionTo('posts.edit.any'); }); // Usage $user->canPermission('posts.update', $post);
Explain API
Get detailed explanations of authorization decisions:
$result = PermissionManager::explain($user, 'orders.delete'); // Returns: [ 'allowed' => false, 'ability' => 'orders.delete', 'reason' => 'explicit_user_deny', 'source' => 'direct_permission', 'metadata' => [ 'matched_pattern' => 'orders.delete', 'permission_id' => 42, ], 'user' => [ 'id' => 1, 'roles' => ['admin', 'editor'], 'direct_permissions' => ['orders.view', '!orders.delete'], ], ]
๐ป Facade API
Managing Roles
use HosseinHezami\PermissionManager\Facades\PermissionManager; // List all roles $roles = PermissionManager::roles()->list(); // Create role PermissionManager::roles()->create([ 'slug' => 'editor', 'name' => 'Editor', 'description' => 'Can edit content', ]); // Role operations via proxy PermissionManager::role('admin') ->assignPermission('users.*') ->assignPermissionSet('content-manager') ->inheritFrom('editor'); // Update, delete PermissionManager::role('editor')->update(['name' => 'Content Editor']); PermissionManager::role('editor')->delete();
Managing Permissions
// List all permissions $permissions = PermissionManager::permissions()->list(); // Create PermissionManager::permissions()->create('users.view'); PermissionManager::permissions()->create(['users.edit', 'users.delete']); // Delete PermissionManager::permissions()->delete('users.delete'); // Sync with routes PermissionManager::permissions()->sync(); // Get grouped by category $grouped = PermissionManager::permissions()->getAllGrouped(); // Permission sets PermissionManager::permissions()->createSet([ 'name' => 'Content Manager', 'slug' => 'content-manager', ]);
Managing Users
// User operations PermissionManager::user($userId) ->assignRole('admin') ->assignRole(['editor', 'manager']) ->givePermissionTo('reports.export'); // Queries $roles = PermissionManager::user($userId)->roles(); $permissions = PermissionManager::user($userId)->permissions(); $canEdit = PermissionManager::user($userId)->hasPermission('users.edit');
Team Context
// Set current team PermissionManager::setTeam($team); PermissionManager::setTeam(42); // By ID PermissionManager::setTeam('engineering'); // By slug // Clear team context PermissionManager::clearTeam(); // Get current team context $teamContext = PermissionManager::teamContext();
Cache Management
// Clear all permission cache PermissionManager::cache()->flushAll(); // Clear specific user cache PermissionManager::cache()->flushUser($userId); // Clear specific role cache PermissionManager::cache()->flushRole($roleId);
๐ฅ๏ธ Artisan Commands
Role Management
# List all roles php artisan roles:list # Create a role php artisan role:create admin "Administrator" "Full access" # Update a role php artisan role:update admin --name="Super Admin" # Delete a role php artisan role:delete admin
Permission Management
# List permissions php artisan permissions:list # Create permissions php artisan permission:create "users.edit" php artisan permission:create "users.create,users.edit,users.delete" # Delete permissions php artisan permission:delete "users.delete" # Sync routes with permissions php artisan permission:sync-routes php artisan permission:sync-routes --strategy=controller-action php artisan permission:sync-routes --prefix=admin # Generate CRUD permissions for a resource php artisan permission:generate-resource users php artisan permission:generate-resource Post --actions=view,create,update
Assignment Commands
# Assign permissions to role php artisan role:assign-permission admin "users.*" php artisan role:assign-permission admin "users.create,users.edit" # Revoke permissions php artisan role:revoke-permission admin "users.delete" # Assign roles to user php artisan user:assign-role 1 admin php artisan user:assign-role 1 "admin,editor" # Revoke roles from user php artisan user:revoke-role 1 admin
Import / Export
# Export roles to JSON php artisan role:export roles.json # Import roles from JSON php artisan role:import roles.json
Diagnostic & Debug Commands โญ
# ๐ฉบ System health check php artisan permission:doctor # Detects: # - Orphan permissions (not assigned to any role) # - Orphan roles (not assigned to any user) # - Cyclic role inheritance # - Duplicate roles/permissions # - Expired permissions # ๐ณ View role hierarchy tree php artisan permission:tree php artisan permission:tree --role=admin # โ Why was access denied? php artisan permission:why 42 users.delete # Output: # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # โ Permission Decision Explanation โ # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ # User: 42 # Ability: users.delete # โ DENIED # Reason: explicit_user_deny # Source: direct_permission # Details: # - matched_pattern: users.delete # - permission_id: 15 # ๐ Explain permission check (JSON output) php artisan permission:explain 42 users.edit --json # โ Validate configuration php artisan permission:validate # โ๏ธ Prune expired permissions php artisan permission:prune --days=30 php artisan permission:prune --dry-run # โก Check single permission php artisan permission:check 42 users.edit
Cache Commands
# Warm up cache (preload all permissions) php artisan permission:cache:warm # Clear cache php artisan permission:cache:clear php artisan permission:cache:clear --user=42 php artisan permission:cache:clear --role=5
๐งช Testing Helpers
The package provides powerful testing helpers for your application tests:
Setup
use HosseinHezami\PermissionManager\Testing\InteractsWithPermissions; use HosseinHezami\PermissionManager\Testing\PermissionAssertions; class PostControllerTest extends TestCase { use InteractsWithPermissions; use PermissionAssertions; // ... }
Creating Test Data
// Create user with roles $admin = $this->createUserWithRoles(['admin', 'editor']); // Create role with permissions $role = $this->createRoleWithPermissions('editor', [ 'posts.view', 'posts.edit', 'posts.publish', ]); // Act as a user with specific roles $user = $this->actingAsRole(['admin']); // Act as a user with specific permissions $user = $this->actingAsWithPermissions(['posts.view', 'posts.edit']);
Testing Permissions
// Grant / deny permissions in tests $this->grantPermission($user, 'posts.edit'); $this->denyPermission($user, 'posts.delete'); // Clear cache $this->clearPermissionCache();
Assertions
public function test_admin_can_edit_posts() { $admin = $this->createUserWithRoles(['admin']); $this->assertHasRole($admin, 'admin'); $this->assertHasPermission($admin, 'posts.edit'); $this->assertDoesNotHavePermission($admin, 'system.config'); $this->assertHasAnyRole($admin, ['admin', 'editor']); $this->assertHasAllRoles($admin, ['admin']); $this->assertHasAnyPermission($admin, ['posts.edit', 'posts.view']); $this->assertHasAllPermissions($admin, ['posts.edit']); $this->assertIsNotSuperAdmin($admin); // Test contextual permissions $post = Post::factory()->create(['user_id' => $admin->id]); $this->assertCanPermission($admin, 'posts.update', $post); }
Testing HTTP Routes
public function test_unauthorized_user_gets_403() { $user = $this->createUser(); $this->actingAs($user) ->get('/admin/users') ->assertStatus(403); } public function test_authorized_user_gets_access() { $user = $this->actingAsRole(['admin']); $this->get('/admin/users') ->assertStatus(200); }
โ๏ธ Configuration
Full configuration file (config/permission-manager.php):
return [ // Model classes 'models' => [ 'role' => \HosseinHezami\PermissionManager\Models\Role::class, 'permission' => \HosseinHezami\PermissionManager\Models\Permission::class, 'user' => config('auth.providers.users.model'), 'team' => \HosseinHezami\PermissionManager\Models\Team::class, ], // Table names 'tables' => [ 'roles' => 'roles', 'permissions' => 'permissions', 'permission_groups' => 'permission_groups', 'permission_sets' => 'permission_sets', 'permission_set_items' => 'permission_set_items', 'role_permissions' => 'role_permissions', 'role_inherits' => 'role_inherits', 'user_roles' => 'user_roles', 'user_permissions' => 'user_permissions', 'teams' => 'teams', 'team_user' => 'team_user', 'permission_conditions' => 'permission_conditions', 'permission_audits' => 'permission_audits', 'authorization_logs' => 'authorization_logs', ], // Cache settings 'cache_duration' => 60, // minutes 'cache' => [ 'enabled' => true, 'prefix' => 'pm', 'use_tags' => false, // Enable for Redis/Memcached 'ttl' => 60, ], // Feature toggles 'wildcards' => true, 'log_denials' => false, 'direct_permissions' => ['enabled' => true], 'teams' => [ 'enabled' => true, 'team_foreign_key' => 'team_id', ], // Super Admin 'super_admin' => [ 'enabled' => true, 'role_slug' => 'super-admin', 'bypass_all' => true, ], // Audit 'audit' => [ 'enabled' => true, 'log_mutations' => true, ], // Authorization logging (use with caution in production) 'authorization_logging' => [ 'enabled' => false, 'denied_only' => true, 'sample_rate' => 1.0, ], // ABAC 'conditions' => ['enabled' => true], // Route sync 'route_sync' => [ 'enabled' => true, 'strategy' => 'route-name', // route-name | controller-action | resource-action ], ];
๐ Comparison with Spatie
| Feature | Laravel Permission Manager | Spatie Permission | Winner |
|---|---|---|---|
| RBAC | โ | โ | Tie |
| Direct Permissions | โ | โ | Tie |
| Wildcard Permissions | โ (advanced) | โ | LPM |
| Role Hierarchy | โ Multi-level | โ | LPM |
| Explicit Deny | โ | โ | LPM |
| Temporary Permissions | โ | โ | LPM |
| Teams / Multi-Tenancy | โ | โ | Tie |
| Multi-Guard | โ Real isolation | โ | Tie |
| ABAC (Condition Engine) | โ | โ | LPM |
| Audit Logging | โ Built-in | โ | LPM |
| Authorization Audit Trail | โ | โ | LPM |
| Route Sync | โ | โ | LPM |
| Resource Generator | โ | โ | LPM |
| Explain API | โ | โ | LPM |
| CLI Doctor | โ | โ | LPM |
| Permission Tree | โ | โ | LPM |
| Gate Integration | โ Native | โ | Tie |
| Middleware DSL | โ Advanced | โ | Tie |
| Cache Tags | โ | โ | Tie |
| Admin UI | ๐ Roadmap | โ | Tie |
๐บ๏ธ Roadmap
โ v2.0 (Current - Released)
- Core Authorization Engine
- Direct Permissions + Allow/Deny
- Role Hierarchy with Cycle Detection
- Teams / Multi-Tenancy
- ABAC Condition Engine
- Audit Logging
- Multi-Guard Support
- Advanced Middleware DSL
- Blade Directives
- Gate/Policy Integration
- Smart Cache Engine
- CLI Diagnostic Tools
- Testing Helpers
- 141 passing tests
๐ฏ v2.1 (Planned)
- Permission Aliases / Bundles
- Time-based Permissions (business hours)
- Sanctum / Passport Token Abilities integration
- Field-Level Permissions
- Advanced Export formats (Excel, YAML)
- Laravel Octane support
- Performance optimizations
๐ฎ v3.0 (Vision)
- Admin Panel UI (Livewire + Filament)
- Permission Graph visualization
- Permission recommendation engine (AI)
- OAuth scope mapping
- WebSocket-based permission events
- GraphQL directives
๐๏ธ Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your App โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ Blade โ โMiddlewareโ โ Controllers โ โ
โ โโโโโโฌโโโโโโ โโโโโโฌโโโโโโ โโโโโโโโโฌโโโโโโโโโ โ
โ โ โ โ โ
โโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AuthorizationManager (Core Engine) โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ Wildcard โ โ Condition โ โ Policy โ โ
โ โ Matcher โ โ Evaluator โ โ Resolver โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โ โ Role โ โ Permission โ โ Context โ โ
โ โ Resolver โ โ Resolver โ โ Resolver โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Data Layer โ
โ โโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโ โ
โ โ Roles โ โPermissionsโ โ Teams โ โ Audits โ โ
โ โโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ค Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details.
Development Setup
# Clone repository git clone https://github.com/hosseinhezami/laravel-permission-manager.git cd laravel-permission-manager # Install dependencies composer install # Run tests composer test # Run specific test suites composer test-unit composer test-feature composer test-integration
Running Tests
# All tests composer test # With coverage composer test-coverage
๐ License
The MIT License (MIT). Please see License File for more information.
๐ Support
- ๐ง Email: hossein.hezami@gmail.com
- ๐ Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
โญ Show Your Support
If this package helps you, please:
- โญ Star the repository on GitHub
- ๐ข Share it with your colleagues
- ๐ Report bugs
- ๐ก Suggest new features
- ๐ค Submit pull requests
Made with โค๏ธ by Hossein Hezami
๐ Complete API Reference (click to expand)
User Methods (via PermissionTrait)
// Roles $user->roles(); $user->assignRole($role); $user->revokeRole($role); $user->hasRole($role); $user->hasAnyRole($roles); $user->hasAllRoles($roles); $user->lacksRole($role); // Permissions $user->permissions($directOnly = false); $user->hasPermissionTo($permission, $requireAll = true); $user->hasAnyPermission($permissions); $user->hasAllPermissions($permissions); $user->lacksPermissionTo($permission); // Direct Permissions $user->directPermissions(); $user->givePermissionTo($permission, $effect = 'allow', $expiresAt = null); $user->denyPermissionTo($permission); $user->revokePermissionTo($permission); $user->hasDirectPermission($ability); // Contextual $user->canPermission($ability, $resource = null); $user->authorizePermission($ability, $arguments = null); // Teams $user->teams(); $user->joinTeam($team); $user->leaveTeam($team); $user->belongsToTeam($team); $user->assignRoleForTeam($roles, $team); $user->revokeRoleForTeam($roles, $team); $user->hasRoleForTeam($role, $team); // Super Admin $user->isSuperAdmin(); // Snapshot & Cache $user->getPermissionSnapshot(); $user->forgetCachedPermissions();
Role Methods
// Create & Find Role::create($data); Role::findBySlug($slug); // Permissions $role->permissions(); $role->assignPermission($routes); $role->revokePermission($routes); $role->hasPermissionTo($route); $role->getAllPermissions(); $role->getAllPermissionModels(); // Hierarchy $role->inherits(); $role->children(); $role->inheritFrom($role); $role->removeInheritance($role); // Users $role->users(); // Scopes Role::forGuard($guard); // Cache $role->forgetCachedPermissions();
Permission Methods
// Create & Find Permission::create($data); Permission::findByRoute($route); // Groups $permission->group(); $permission->assignToGroup($group); $permission->removeFromGroup(); // Conditions $permission->conditions(); $permission->activeConditions(); $permission->hasConditions(); // Roles $permission->roles(); // Scopes Permission::forGuard($guard); Permission::inGroup($group);
PermissionManager Facade
// Roles PermissionManager::roles()->list(); PermissionManager::roles()->create($data); PermissionManager::role($slug)->assignPermission($permission); PermissionManager::role($slug)->revokePermission($permission); PermissionManager::role($slug)->update($params); PermissionManager::role($slug)->delete(); PermissionManager::role($slug)->inheritFrom($role); PermissionManager::role($slug)->assignPermissionSet($set); // Permissions PermissionManager::permissions()->list(); PermissionManager::permissions()->create($routes); PermissionManager::permissions()->delete($routes); PermissionManager::permissions()->sync(); PermissionManager::permissions()->syncRoutesWithOptions($options); PermissionManager::permissions()->groups(); PermissionManager::permissions()->sets(); PermissionManager::permissions()->createSet($data); PermissionManager::permissions()->generateResource($resource, $actions); // Users PermissionManager::user($userId)->assignRole($roles); PermissionManager::user($userId)->revokeRole($roles); PermissionManager::user($userId)->roles(); PermissionManager::user($userId)->permissions(); PermissionManager::user($userId)->hasRole($role); PermissionManager::user($userId)->hasPermission($permission); PermissionManager::user($userId)->givePermissionTo($permission); PermissionManager::user($userId)->denyPermissionTo($permission); PermissionManager::user($userId)->joinTeam($team); PermissionManager::user($userId)->leaveTeam($team); PermissionManager::user($userId)->canPermission($ability, $resource); // Teams PermissionManager::setTeam($team); PermissionManager::clearTeam(); PermissionManager::teamContext(); // Authorization PermissionManager::check($user, $ability, $resource); PermissionManager::explain($user, $ability, $resource); PermissionManager::snapshot($user); PermissionManager::define($ability, $callback); // Cache PermissionManager::cache()->flushAll(); PermissionManager::cache()->flushUser($userId); PermissionManager::cache()->flushRole($roleId);
๐ Usage Examples (click to expand)
E-commerce Platform
// Setup roles $customer = Role::create(['name' => 'Customer', 'slug' => 'customer']); $seller = Role::create(['name' => 'Seller', 'slug' => 'seller']); $admin = Role::create(['name' => 'Admin', 'slug' => 'admin']); // Hierarchy: admin > seller > customer $seller->inheritFrom('customer'); $admin->inheritFrom('seller'); // Customer permissions $customer->assignPermission([ 'products.view', 'orders.create', 'orders.view.own', ]); // Seller gets customer permissions + more $seller->assignPermission([ 'products.manage.own', 'orders.view.all', 'analytics.view.own', ]); // Conditional permission: sellers can only edit their own products PermissionCondition::create([ 'permission_id' => Permission::findByRoute('products.edit')->id, 'conditions' => [ 'field' => 'user.id', 'operator' => '=', 'value' => 'resource.seller_id', ], ]);
Multi-tenant SaaS
// Each tenant is a team $companyA = Team::createTeam(['name' => 'Company A']); $companyB = Team::createTeam(['name' => 'Company B']); $user->joinTeam($companyA); $user->joinTeam($companyB); // Different roles per tenant $user->assignRoleForTeam('admin', $companyA); $user->assignRoleForTeam('viewer', $companyB); // In controllers public function index(Request $request) { PermissionManager::setTeam($request->user()->currentTeam); if (auth()->user()->hasPermissionTo('reports.view')) { // Can view reports in this tenant } }
Time-limited Contractor Access
$contractor = User::find($contractorId); // Give access for 30 days $contractor->givePermissionTo( 'projects.access', 'allow', now()->addDays(30) ); // Automatically expires - no cron job needed for checks // Run pruning weekly: php artisan permission:prune --days=7
Approval Workflow with Conditions
// Define: managers can approve expenses under $1000 // Directors can approve any amount Permission::create(['route' => 'expenses.approve']); // Manager condition PermissionCondition::create([ 'permission_id' => Permission::findByRoute('expenses.approve')->id, 'name' => 'manager-limit', 'conditions' => [ 'all' => [ ['field' => 'user.role', 'operator' => '=', 'value' => 'manager'], ['field' => 'resource.amount', 'operator' => '<=', 'value' => 1000], ], ], ]);
If this package saved you time, consider giving it a โญ on GitHub!