patrickhanna/laravel-warden
Warden adds role and permission management to Laravel with a simple, expressive API for defining abilities and guarding routes. Lightweight and easy to drop into existing apps.
This package is an authorization layer, not an authentication one. The fastest path to value is to wire it into Laravel's existing Gate/Policy system and then stop calling permission tables directly from controllers.
composer require patrickhanna/laravel-warden
php artisan vendor:publish --provider="PatrickHanna\\Warden\\WardenServiceProvider"
php artisan migrate
Verify the provider class name in vendor/patrickhanna/laravel-warden/composer.json before running this; the repository name (laravel-warrant) and package name (laravel-warden) differ, so the namespace may not match the package slug.User model (check the README or src/ for the exact trait name):
use PatrickHanna\Warden\Traits\HasWarrant;
class User extends Authenticatable
{
use HasWarrant;
}
$role = WarrantRole::create(['name' => 'editor']);
$role->givePermission('posts.update');
$user->assignRole('editor');
$user->can('posts.update'); // should be true
can: middleware:
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
Where to look first:
Gate, whether it registers a Gate::before, and what facade/trait names are real.roles, permissions, pivot tables, and any tenant/scope columns) and therefore what "database-scoped" actually means in this package.assignRole, givePermission, hasPermission, etc.) that you will use daily.If the package does not register itself with Laravel's Gate, use its facade directly in policies rather than scattering calls through controllers. Integrating once at the policy layer keeps the rest of the app framework-idiomatic.
Keep business rules in policies and use the package only to answer "does this user hold this ability for this scope?" This preserves Laravel's authorize(), can(), Blade @can, and form-request conventions while letting permissions be data-driven.
class PostPolicy
{
public function update(User $user, Post $post): bool
{
if ($user->id === $post->user_id) {
return true;
}
return $user->hasPermission('posts.update', $post);
}
}
Passing the model instance is the important part. String-only checks ($user->can('posts.update')) test global permission; model-aware checks test the database-scoped ability the package advertises.
Prefer middleware and form requests over inline checks:
Route::middleware('can:update,post')->put('/posts/{post}', ...);
public function authorize(): bool
{
return $this->user()->can('update', $this->route('post'));
}
This keeps authorization consistent across HTTP, API, and console entry points.
Use roles for job functions (editor, moderator, billing) and direct permissions only for one-off exceptions. Seeding is the main place this matters:
$role = Role::firstOrCreate(['name' => 'editor']);
$role->syncPermissions(['posts.create', 'posts.update']);
$user->assignRole('editor');
Make seeders idempotent with firstOrCreate/syncPermissions; otherwise repeated deploys create duplicate pivots or fail on unique constraints.
The key workflow is "can this user do X to this specific record?" Keep a consistent call shape across the codebase:
$user->can('update', $post);
$user->can('delete', $comment);
If the package exposes a scope/tenant concept, resolve the tenant before the check and cache the resolved permission set per request. Avoid resolving scopes inside loops.
Use the framework directives so the package's Gate integration does the work:
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
For APIs, expose abilities explicitly rather than leaking the permission schema:
'can' => [
'update' => $request->user()->can('update', $post),
'delete' => $request->user()->can('delete', $post),
],
Because the package integrates with Gate, tests stay familiar:
$this->actingAs($editor)
->put("/posts/{$post->id}")
->assertOk();
Test the negative path too: a user with posts.update globally but not for another tenant's post should receive 403. That is the check most likely to regress.
composer.json, read src/ before adopting, and keep an exit path. If the package is abandoned, your policies should still be the place where authorization logic lives so you can swap the permission backend.laravel-warden but the repository is laravel-warrant. Provider namespaces, facade names, config keys, and trait names may not match the README. Confirm them in composer.json and src/ before copying examples.Gate::before for super-admins, it can silently bypass database-scoped checks. Search the service provider for Gate::before and Gate::after; if present, make the bypass explicit and test it.PostPolicy for Post. If you use non-standard namespaces, register policies explicitly in AuthServiceProvider; otherwise can('update', $post) may fall through to a global ability check and return the wrong result.auth must run before can: middleware. A can: route on a guest produces a confusing 403 rather than a redirect to login.$user->can('posts.update') and $user->can('posts.update', $post) are not interchangeable. Standardize on the model-aware form anywhere a record is involved.Gate::forUser($user)->inspect('update', $post) to see which policy or ability resolved the result, then compare against the package's stored permissions for that user and scope.How can I help you explore Laravel packages today?