TL;DR: Authorization tutorials show you theory. Production apps need recipes. This cookbook gives you 12 copy-paste-ready patterns โ from classic RBAC to owner-only ABAC rules, team-scoped admins, expiring contractor access, and audit trails โ all built on Laravel Permission Manager. Every recipe includes the exact code you'd ship.
๐ GitHub Repository ยท ๐ฆ Packagist
๐ Table of Contents
- Prep Your Kitchen (Installation)
- Recipe 1: Classic RBAC with Wildcards
- Recipe 2: The "One Exception" User
- Recipe 3: The "Everything Except" Role
- Recipe 4: Your Org Chart, as Code
- Recipe 5: Owner-Only Editing (ABAC)
- Recipe 6: Department-Scoped Approvals
- Recipe 7: The Multi-Tenant Admin
- Recipe 8: The 30-Day Contractor
- Recipe 9: Bulletproof Route Protection
- Recipe 10: Conditional UI Without the Mess
- Recipe 11: The Compliance Trail
- Recipe 12: The 2 AM Fix
- Bonus Recipe: Testing Authorization
- Comparison with Spatie
- Final Thoughts
๐ช Prep Your Kitchen (Installation)
Every recipe below assumes this setup (takes ~60 seconds):
composer require hosseinhezami/laravel-permission-manager
php artisan vendor:publish --provider="HosseinHezami\PermissionManager\PermissionManagerServiceProvider" --tag="migrations"
php artisan migrate
// app/Models/User.php
use HosseinHezami\PermissionManager\Traits\PermissionTrait;
class User extends Authenticatable
{
use PermissionTrait;
}
Now let's cook. ๐จโ๐ณ
๐ณ Recipe 1: Classic RBAC with Wildcards
Scenario: "Editors can do everything with posts."
Instead of listing ten permissions, use a wildcard:
use HosseinHezami\PermissionManager\Models\Role;
$editor = Role::create(['name' => 'Editor', 'slug' => 'editor']);
// One wildcard instead of ten lines
$editor->assignPermission('posts.*');
$user->assignRole('editor');
$user->hasPermissionTo('posts.view'); // โ
true
$user->hasPermissionTo('posts.publish'); // โ
true
$user->hasPermissionTo('users.delete'); // โ false
Chef's note: Wildcards support posts.*, *.edit, admin.*, and even * โ matching happens in a dedicated engine, not scattered regex in your controllers.
๐ณ Recipe 2: The "One Exception" User
Scenario: "Marketing needs report export, but only for Sarah this quarter."
Don't create a role for one person. Grant a direct permission:
$sarah->givePermissionTo('reports.export');
$sarah->hasDirectPermission('reports.export'); // โ
true
$sarah->hasPermissionTo('reports.export'); // โ
true (direct counts)
And when the quarter ends:
$sarah->revokePermissionTo('reports.export');
Chef's note: Direct permissions are checked alongside role permissions in the resolution chain โ no special-casing in your code.
๐ณ Recipe 3: The "Everything Except" Role
Scenario: "Interns get all post permissions, except delete."
This is where most RBAC systems collapse into role explosion. Here it's two lines, thanks to explicit deny:
$intern = Role::create(['name' => 'Intern', 'slug' => 'intern']);
$intern->assignPermission('posts.*'); // grant everything...
$intern->denyPermission('posts.delete'); // ...except this
$user->assignRole('intern');
$user->hasPermissionTo('posts.edit'); // โ
true
$user->hasPermissionTo('posts.delete'); // โ false โ deny always wins
Chef's note: The resolution order guarantees deny beats allow, at both user and role level. One rule eliminates dozens of "exception roles."
๐ณ Recipe 4: Your Org Chart, as Code
Scenario: "Admins inherit everything editors can do; editors inherit everything viewers can do."
Model the hierarchy once โ permissions cascade forever:
$viewer->assignPermission('posts.view');
$editor->assignPermission('posts.edit');
$admin->assignPermission('users.manage');
$editor->inheritFrom('viewer');
$admin->inheritFrom('editor');
// An admin now holds ALL of these:
$user->assignRole('admin');
$user->hasPermissionTo('posts.view'); // โ
from viewer (2 levels down)
$user->hasPermissionTo('posts.edit'); // โ
from editor (1 level down)
$user->hasPermissionTo('users.manage'); // โ
direct
Visualize it anytime:
php artisan permission:tree
Chef's note: Cycles (A โ B โ A) throw a CyclicRoleInheritanceException instead of hanging your app. Safety is built in.
๐ณ Recipe 5: Owner-Only Editing (ABAC)
Scenario: "Users can edit posts โ but only their own."
Roles can't express "their own." Attribute-Based Access Control can:
use HosseinHezami\PermissionManager\Models\Permission;
use HosseinHezami\PermissionManager\Models\PermissionCondition;
PermissionCondition::create([
'permission_id' => Permission::findByRoute('posts.update')->id,
'name' => 'owner-only',
'conditions' => [
'field' => 'user.id',
'operator' => '=',
'value' => 'resource.owner_id',
],
]);
Now the check is resource-aware:
$user->canPermission('posts.update', $myPost); // โ
owns it
$user->canPermission('posts.update', $someoneElses); // โ not the owner
Chef's note: The condition engine is whitelist-based JSON โ no eval(), no code injection. Safe by design.
๐ณ Recipe 6: Department-Scoped Approvals
Scenario: "Managers approve expenses โ but only for their own department, and only under $1,000."
Combine logical operators (all / any / not) and comparison operators:
PermissionCondition::create([
'permission_id' => Permission::findByRoute('expenses.approve')->id,
'name' => 'dept-manager-limit',
'conditions' => [
'all' => [
['field' => 'user.department_id', 'operator' => '=', 'value' => 'resource.department_id'],
['field' => 'resource.amount', 'operator' => '<=', 'value' => 1000],
],
],
]);
$manager->canPermission('expenses.approve', $teamExpense); // โ
same dept, $500
$manager->canPermission('expenses.approve', $otherDeptExpense); // โ different dept
$manager->canPermission('expenses.approve', $bigExpense); // โ $5,000
Chef's note: Available operators: =, !=, >, >=, <, <=, in, not_in, contains, starts_with, ends_with, exists, not_exists.
๐ณ Recipe 7: The Multi-Tenant Admin
Scenario: "Ana is an admin at Acme, but just a viewer at Globex."
Team-scoped roles make one user's permissions different per tenant:
use HosseinHezami\PermissionManager\Models\Team;
$acme = Team::createTeam(['name' => 'Acme Corp', 'slug' => 'acme']);
$globex = Team::createTeam(['name' => 'Globex Inc', 'slug' => 'globex']);
$ana->joinTeam($acme);
$ana->joinTeam($globex);
$ana->assignRoleForTeam('admin', $acme);
$ana->assignRoleForTeam('viewer', $globex);
$ana->hasRoleForTeam('admin', $acme); // โ
$ana->hasRoleForTeam('admin', $globex); // โ
Scope every check in a request via middleware:
Route::middleware(['auth', 'pm.team:header,X-Team-Id'])->group(function () {
// All permission checks inside respect the active team
});
๐ณ Recipe 8: The 30-Day Contractor
Scenario: "The contractor needs billing access for one month. Make sure it dies on its own."
Temporary permissions auto-expire โ no cron logic in your app code:
$contractor->givePermissionTo(
'billing.view',
'allow',
now()->addDays(30) // expires automatically
);
// Today: โ
true
// Day 31: โ false โ without you touching anything
Housekeeping (schedule weekly):
php artisan permission:prune --days=7
๐ณ Recipe 9: Bulletproof Route Protection
Scenario: "Different routes need AND / OR / NOT permission logic."
The Middleware DSL expresses boolean logic declaratively:
// OR โ any of these
Route::get('/reports', [ReportController::class, 'index'])
->middleware('pm:permission:any:reports.view|reports.export');
// AND โ all of these
Route::post('/projects', [ProjectController::class, 'store'])
->middleware('pm:permission:all:projects.view|projects.create');
// NOT โ must NOT have this
Route::get('/public', fn () => '...')
->middleware('pm:permission:not:admin.panel');
// Role + permission combined
Route::delete('/projects/{project}', [ProjectController::class, 'destroy'])
->middleware(['pm:role:admin', 'pm:permission:projects.delete']);
Unauthorized users get a clean 403 automatically.
๐ณ Recipe 10: Conditional UI Without the Mess
Scenario: "Show buttons only for what the user can actually do."
Replace nested @if spaghetti with semantic directives:
@role('admin')
<a href="{{ route('admin.settings') }}">Settings</a>
@endrole
@canpermission('posts.update', $post)
<button wire:click="edit({{ $post->id }})">Edit</button>
@endcanpermission
@hasanyrole(['admin', 'editor'])
<div class="editor-toolbar">โฆ</div>
@endhasanyrole
@unlesspermission('posts.delete')
<span class="muted">Deleting is disabled for your account.</span>
@endunlesspermission
Chef's note: 12+ directives ship out of the box, including @hasallpermissions, @cannotpermission, and the legacy @hasRole / @hasPermission.
๐ณ Recipe 11: The Compliance Trail
Scenario: "The auditor asks: who granted delete rights, and when?"
Every mutation is recorded automatically (actor, action, subject, IP, user agent, metadata):
use HosseinHezami\PermissionManager\Models\PermissionAudit;
// Everything that happened recently
PermissionAudit::latest()->limit(50)->get();
// Everything one admin did
PermissionAudit::byActor($adminId)->get();
// Only grants of deny rules
PermissionAudit::action('granted')->where('effect', 'deny')->get();
Enable it in config:
'audit' => ['enabled' => true, 'log_mutations' => true],
Chef's note: There's also an optional authorization trail (authorization_logging) that logs denied checks with sampling โ perfect for spotting probing attempts without flooding logs.
๐ณ Recipe 12: The 2 AM Fix
Scenario: "User 42 can't delete posts. Why?"
Stop guessing. Ask the engine:
php artisan permission:why 42 posts.delete
User: 42
Ability: posts.delete
โ DENIED
Reason: explicit_user_deny
Source: direct_permission
Details:
- matched_pattern: posts.delete
Or programmatically:
$result = PermissionManager::explain($user, 'posts.delete');
// ['allowed' => false, 'reason' => 'explicit_user_deny', 'source' => ..., 'user' => [...]]
And for a full picture of any user's state:
$snapshot = PermissionManager::snapshot($user);
// roles, allow list, deny list, inherited roles, merged permissions
Chef's note: permission:doctor also scans the whole system for orphan roles, cycles, duplicates, and stale caches โ run it in CI.
๐ Bonus Recipe: Testing Authorization
Ship confidence with the built-in testing helpers:
use HosseinHezami\PermissionManager\Testing\InteractsWithPermissions;
use HosseinHezami\PermissionManager\Testing\PermissionAssertions;
class PostPolicyTest extends TestCase
{
use InteractsWithPermissions, PermissionAssertions;
public function test_intern_cannot_delete_posts(): void
{
$intern = $this->actingAsRole(['intern']);
$this->assertHasPermission($intern, 'posts.edit');
$this->assertDoesNotHavePermission($intern, 'posts.delete');
$this->deleteJson('/posts/1')->assertStatus(403);
}
}
The package itself is backed by 141 tests / 226 assertions on isolated SQLite.
๐ Comparison with Spatie
| Feature | Laravel Permission Manager | Spatie Permission |
|---|---|---|
| RBAC + Wildcards | โ | โ |
| Direct Permissions | โ | โ |
| Teams / Multi-Tenancy | โ | โ |
| Explicit Allow/Deny | โ | โ |
| Role Hierarchy | โ + cycle detection | โ |
| ABAC / Conditions | โ JSON engine (no eval) | โ |
| Temporary Permissions | โ Auto-expiry | โ |
| Audit Logging | โ Built-in | โ |
| Explain API / Doctor / Tree | โ | โ |
| Permission Groups & Sets | โ | โ |
| Gate Integration | โ Native | โ |
| Middleware DSL | โ any/all/not | โ |
| Blade Directives | โ 12+ | โ |
| Testing Helpers | โ Built-in | โ |
| Tests | 141 | ~300 |
| Laravel 13 Support | โ | โ ๏ธ |
| Backward Compatible | โ 100% | โ |
Bottom line: Spatie is a mature, excellent choice for standard RBAC. Laravel Permission Manager adds the production recipes above โ deny rules, hierarchy, ABAC, tenancy depth, auditing, and debugging โ in one cohesive engine.
๐ญ Final Thoughts
Authorization in production isn't one feature โ it's a dozen recurring patterns. The packages that win are the ones that turn each pattern into two lines of declarative code instead of a custom policy class.
Keep this cookbook handy:
- ๐ญ Wildcard RBAC for the norm
- ๐ฏ Direct permissions for exceptions
- ๐ซ Explicit deny for "everything except"
- ๐ณ Hierarchy for org charts
- ๐ง ABAC for ownership & context
- ๐ข Teams for tenants
- โฑ๏ธ Expiry for contractors
- ๐ก๏ธ Middleware DSL for routes
- ๐จ Blade directives for UI
- ๐ Audit for compliance
- ๐ Explain API for debugging
composer require hosseinhezami/laravel-permission-manager
If a recipe saved you time, a โญ on GitHub keeps the kitchen open. ๐
๐ GitHub Repository ยท ๐ฆ Packagist
Tags: #laravel #php #authorization #rbac #abac #multitenancy #security #webdev #tutorial #opensource #saas #backend
Top comments (0)