patrickhanna / laravel-warden
Schema-based authorization and permissions for Laravel, with database-scoped ability checks.
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
README
Laravel Warrant
Row-level permissions & authorization for Laravel — one rule, compiled straight to SQL.
Read the documentation → laravel-warrant.dev
Warning
Laravel Warrant is in beta and still being tested. It's usable, but the API may change between releases — pin your version and check the changelog before upgrading. Please report any issues you run into.
Note
Not an official Laravel package. Laravel Warrant is an independent, open-source project. It is not affiliated with, maintained by, or endorsed by the Laravel team.
Schema-based authorization for Laravel that compiles a small, human-readable rule language directly into SQL — so "what can this user do?" and "which rows can this user touch?" are answered by the database in a single query, not by loading records into memory.
if is_self or (is_manager and same_department)
they can view, update
they cannot delete
That block is a real, complete Warrant rule. Warrant turns it into a WHERE
clause.
Installation
composer require patrickhanna/laravel-warrant
The service provider auto-registers. Publish the config to edit it in place:
php artisan vendor:publish --tag=warrant-config
Requirements: PHP 8.2+, Laravel 11 or 12. The SQL Warrant generates is supported on PostgreSQL, MySQL/MariaDB, and SQLite.
Documentation
Full docs live at laravel-warrant.dev:
- Quick start · Core concepts · vs. spatie/laravel-permission
- Schemas · Conditions · Check-time context
- The rule language · Building rules · Providing rules
- Checking access · Reachability · Route middleware · Denial messages
- How it compiles to SQL · Testing
- API reference: Checking · Schema · Rule building · Middleware · Errors · Cheat sheet
Why Warrant
- One source of truth. The same rule set answers a single check, filters a list, and reports the per-row ability list — no permission logic duplicated between a policy and a query.
- Compiles to SQL. Rules never run in PHP or pull your models into memory;
they become a
WHEREclause the database evaluates. - Rules are data. Store them in the database, generate them from a GUI, or hard-code them — you decide where they come from and change them without touching app code.
- Readable language. Rules are written in a small
if … they can/cannot …language that non-authors can follow. - Integrates with Laravel's Gate.
$user->can(),@can,Gate::authorize, and thecan:route middleware resolve Warrant abilities, while abilities no schema declares fall through to your existing policies.
See Why Warrant for the full rationale and a comparison with the policy/query approach.
A quick example
1. The model uses the trait and names its schema:
use Illuminate\Database\Eloquent\Model; use Warrant\HasWarrantSchema; class Timesheet extends Model { use HasWarrantSchema; public function warrantSchema(): string { return \App\Warrant\TimesheetSchema::class; } }
2. The schema declares the vocabulary — abilities and conditions:
namespace App\Warrant; use App\Models\Timesheet; use Illuminate\Contracts\Database\Query\Builder; use Warrant\Ability; use Warrant\GlobalCondition; use Warrant\Schema\Conditions\GlobalConditionContext; use Warrant\Schema\Conditions\TargetedConditionContext; use Warrant\Schema\WarrantSchema; use Warrant\TargetedCondition; class TimesheetSchema extends WarrantSchema { public const model = Timesheet::class; #[Ability] public const VIEW = 'view'; #[Ability] public const UPDATE = 'update'; #[Ability] public const DELETE = 'delete'; #[Ability] public const APPROVE = 'approve'; // Targeted: narrows WHICH timesheet rows the user matches. #[TargetedCondition] public function isSelf(TargetedConditionContext $c): Builder { return $c->query->whereRaw('timesheets.user_id = ?', [$c->user->getAuthIdentifier()]); } #[TargetedCondition] public function inDepartment(TargetedConditionContext $c): Builder { return $c->query->whereIn('timesheets.department_id', $c->arguments); } // Global: a plain yes/no about the user, independent of any row. #[GlobalCondition] public function isAdmin(GlobalConditionContext $c): bool { return (bool) $c->user->is_admin; } }
3. The resolver hands rules (as data) to Warrant for the current user:
namespace App\Warrant; use Warrant\RuleResolutionContext; use Warrant\RuleResolver; use Warrant\RuleSyntaxTree\WarrantRuleSet; class DatabaseRuleResolver implements RuleResolver { public function resolve(RuleResolutionContext $context): WarrantRuleSet { // Load the raw rule string + any binding values for this user/resource. [$syntax, $bindings] = MyRuleStore::for( user: $context->user, resource: $context->schemaKey, // 'timesheets' ); return WarrantRuleSet::fromSyntax($context->schemaKey, $syntax, $bindings); } }
The rules themselves are just text:
if is_self they can view, update, delete
if in_department(?, ?) they can view, approve
if is_admin they can *
4. Register the resolver and schema in config/warrant.php:
'rule_resolver' => App\Warrant\DatabaseRuleResolver::class, 'schemas' => [App\Warrant\TimesheetSchema::class],
5. Check access — every call is a single SQL query:
// Which timesheets can the current user update? $editable = Timesheet::query()->userHasAbility('update')->get(); // Can this user approve this specific timesheet? if (Timesheet::userHasAbilities('approve', $timesheet)) { /* ... */ } // Attach the per-row ability list, e.g. for rendering buttons. $rows = Timesheet::query()->selectUserAbilities()->get(); $rows->first()->abilities; // e.g. ['view', 'update'] // Or go through Laravel's Gate — Warrant resolves these too: $user->can('approve', $timesheet);
The Quick start walks through this end to end.
License
Laravel Warrant is open-source software licensed under the MIT license.
