Laravel Gates & Policies

·

·

Authorization decides what an authenticated user may do. Laravel separates authentication (who) from authorization (what) using gates for simple checks and policies for model-centric rules.

Policies for Model Actions

Generate policies with php artisan make:policy PostPolicy –model=Post. Methods map to actions: viewAny, view, create, update, delete.

Register policies in AuthServiceProvider or rely on auto-discovery when naming conventions match.

Step 1: Define policy methods

public function update(User $user, Post $post): bool { return $user->id === $post->user_id; } returns true only for authors.

Return false or throw AuthorizationException for denials. Use viewAny for index authorization before querying lists.

public function delete(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

Step 2: Authorize in controllers

$this->authorize('update', $post) calls the policy before update logic runs. authorizeResource in constructor maps REST actions automatically.

Form requests can implement authorize() returning $this->user()->can('update', $this->route('post')) for single entry points.

$this->authorize('update', $post);

Step 3: Blade @can directives

@can('update', $post) shows edit buttons only for permitted users. @cannot and @else provide alternative markup.

Avoid expensive policy queries inside tight loops—precompute permissions or use gates with cached roles.

@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

Step 4: Middleware can and authorize

Route::put('/posts/{post}', …)->middleware('can:update,post') resolves policy from route parameter. Use for API routes without controller authorize calls.

can middleware accepts gate names or policy abilities with model route parameters.

->middleware('can:update,post')

Gates for General Abilities

Gates define closures in AuthServiceProvider boot() for abilities not tied to one model, like access-admin-dashboard.

Gates can return Response objects with messages instead of boolean false for richer denial feedback.

Step 1: Define gates in boot

Gate::define('access-admin', fn (User $user) => $user->is_admin); centralizes admin checks.

Use Gate::before to grant super-admin all abilities without listing every policy method.

Gate::define('access-admin', fn (User $user) => $user->role === 'admin');

Step 2: Check gates in code

Gate::allows('access-admin') or $user->can('access-admin') returns boolean. @can('access-admin') works in Blade without a model.

abort_unless(Gate::allows('access-admin'), 403) stops unauthorized access early in controller methods.

abort_unless(auth()->user()->can('access-admin'), 403);

Step 3: Gate responses with messages

Return Response::deny('Subscription required') from gates to expose reasons to JSON clients or Inertia pages.

Policies may also return Response::denyWithStatus(402) for payment-required style APIs.

return Response::deny('You need an active plan.');

Step 4: Inline gates for prototypes

Gate::define('edit-settings', fn ($user) => …) is fine early on, but move stable rules into policies as models mature.

Document abilities in a central enum or constants class so frontend teams know which permissions exist.

Gate::define('manage-billing', fn ($user) => $user->subscribed());

Roles, Teams, and Testing Authorization

Many apps layer roles on users via packages like spatie/laravel-permission or custom role tables checked inside policies.

Test policies with actingAs and assert true/false from gate results to prevent authorization regressions.

Step 1: Role checks inside policies

return $user->hasRole('editor') || $user->id === $post->user_id; combines role-based and resource ownership rules.

Keep role names in config or database seeds. Avoid hard-coding magic strings across dozens of policies.

return $user->hasRole('admin') || $user->id === $post->user_id;

Step 2: Policy before hook

public function before(User $user, string $ability) { if ($user->isSuperAdmin()) return true; } skips other checks for admins.

Returning null continues to other methods. Returning false denies immediately.

public function before(User $user) {
    return $user->is_admin ? true : null;
}

Step 3: Testing with Pest or PHPUnit

$user = User::factory()->create(); $this->actingAs($user)->get(route('posts.edit', $post))->assertForbidden(); proves policy enforcement.

Use Policy::fake() sparingly; prefer integration tests hitting real policies.

$this->assertTrue($user->can('update', $post));

Step 4: Authorize resource controllers

$this->authorizeResource(Post::class, 'post'); in constructor maps index to viewAny and destroy to delete automatically.

Ensure route parameter name matches the second argument so Laravel resolves models correctly.

$this->authorizeResource(Post::class, 'post');

Aadyanex designs Laravel authorization with explicit policies, audited admin actions, and least-privilege roles for regulated industries.