-
Notifications
You must be signed in to change notification settings - Fork 0
Home
The most advanced, enterprise-grade permission management system for Laravel applications.
RBAC + ABAC + Role Hierarchy + Multi-Tenancy + Audit Logging + Condition Engine
- ✨ 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
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');- ✅ 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
- 🏢 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
- 🎨 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
composer require hosseinhezami/laravel-permission-managerphp artisan vendor:publish --provider="HosseinHezami\PermissionManager\PermissionManagerServiceProvider" --tag="config"
php artisan vendor:publish --provider="HosseinHezami\PermissionManager\PermissionManagerServiceProvider" --tag="migrations"php artisan migrateThis creates 14 tables:
-
roles,permissions,permission_groups,permission_sets,permission_set_items -
role_permissions,role_inherits -
user_roles,user_permissions -
teams,team_user -
permission_conditions,permission_audits,authorization_logs
namespace App\Models;
use HosseinHezami\PermissionManager\Traits\PermissionTrait;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use PermissionTrait;
}php artisan permission-manager:install --migrateuse 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"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$admin->assignPermission(['users.view', 'users.edit']);
// Or with wildcard
$admin->assignPermission('users.*');$user = User::find(1);
$user->assignRole('admin');
// Check permissions
$user->hasPermissionTo('users.edit'); // true
$user->hasPermissionTo('users.delete'); // false// 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');@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// 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'); // trueSometimes 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.
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)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
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-adminThe system automatically prevents circular inheritance:
$roleA->inheritFrom('roleB');
$roleB->inheritFrom('roleA'); // ❌ Throws CyclicRoleInheritanceExceptionUse 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');| 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 |
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); // trueSet 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 teamAttribute-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'| 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 |
// 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'],
]],
]]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=7Track 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// config/permission-manager.php
'audit' => [
'enabled' => true,
'log_mutations' => true,
],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)
],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();Advanced middleware with a powerful DSL:
// 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');// 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');// Multiple directives (AND logic between them)
Route::get('/reports', fn() => '...')
->middleware([
'pm:role:admin',
'pm:permission:reports.view',
]);// 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');@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('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@canpermission('posts.update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcanpermission@hasRole('admin') ... @endHasRole
@hasPermission('users.edit') ... @endHasPermissionAll 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');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);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'],
],
]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();// 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',
]);// 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');// 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();// Clear all permission cache
PermissionManager::cache()->flushAll();
// Clear specific user cache
PermissionManager::cache()->flushUser($userId);
// Clear specific role cache
PermissionManager::cache()->flushRole($roleId);# 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# 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# 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# Export roles to JSON
php artisan role:export roles.json
# Import roles from JSON
php artisan role:import roles.json# 🩺 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# 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=5The package provides powerful testing helpers for your application tests:
use HosseinHezami\PermissionManager\Testing\InteractsWithPermissions;
use HosseinHezami\PermissionManager\Testing\PermissionAssertions;
class PostControllerTest extends TestCase
{
use InteractsWithPermissions;
use PermissionAssertions;
// ...
}// 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']);// Grant / deny permissions in tests
$this->grantPermission($user, 'posts.edit');
$this->denyPermission($user, 'posts.delete');
// Clear cache
$this->clearPermissionCache();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);
}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);
}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
],
];| 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 |
- 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
- 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
- Admin Panel UI (Livewire + Filament)
- Permission Graph visualization
- Permission recommendation engine (AI)
- OAuth scope mapping
- WebSocket-based permission events
- GraphQL directives
┌─────────────────────────────────────────────────────┐
│ 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 │ │
│ └────────┘ └───────────┘ └─────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
Contributions are welcome! Please read CONTRIBUTING.md for details.
# 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# All tests
composer test
# With coverage
composer test-coverageThe MIT License (MIT). Please see License File for more information.
- 📧 Email: hossein.hezami@gmail.com
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
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)
// 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();// 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();// 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);// 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)
// 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',
],
]);// 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
}
}$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// 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!