hosseinhezami/laravel-permission-manager
| Install | |
|---|---|
composer require hosseinhezami/laravel-permission-manager |
|
| Latest Version: | v2.0.1 |
| PHP: | ^8.2 |
| License: | MIT |
| Last Updated: | Aug 21, 2026 |
| Links: | GitHub · Packagist |
🛡️ Laravel Permission Manager
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
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);
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!
Related Packages
An authorization library that supports access control models like ACL, RBAC, ABA...
An authorization library that supports access control models like ACL, RBAC, ABA...
This package provides a flexible way to add Role-based Permissions to Laravel
User management package with ACL for managing Users / Roles / Permissions in Lar...