Skip to Content
Course content

226: Laravel Policies and Gates for Authorization

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably been there: you've spent an hour polishing a feature, you hit the route in your browser, and you're greeted with a cold, hard 403 Forbidden. You know you're logged in as the administrator, so you're staring at the screen wondering why Laravel is locking you out of your own app.

Here is a snippet of a PostPolicy I saw a junior dev struggle with recently. They were trying to ensure only the owner of a blog post could edit it.

// PostPolicy.php
public function update(User $user, Post $post)
{
    return $user->id === $post->user_id;
}

In the controller, they were doing everything "by the book":

public function update(Request $request, Post $post)
{
    $this->authorize('update', $post);
    // ... update logic
}

The "Admin Lockdown" Problem

The code above is logically sound for a standard user. If I own the post, I can edit it. If I don't, I can't. But here's the bug: the site owner—the person with the is_admin flag set to true—is also getting a 403. Why? Because the update method in the Policy doesn't care if you're an admin; it only cares if your ID matches the user_id on the post.

The "naive" fix is to add if ($user->is_admin) return true; to every single method in every single policy you write. I'm telling you right now: don't do that. You'll end up with a maintenance nightmare where you're repeating the same admin check in twenty different files.

Using Gate::before for Global Overrides

The elegant way to handle this is by using Gate::before. This allows you to define a "master rule" that runs before any other policy check. If this closure returns a non-null value, that value is used as the result for the authorization check, and the actual policy method is never even called.

Head over to your AuthServiceProvider.php (or AppServiceProvider.php in Laravel 11) and add this to the boot method:

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::before(function ($user, $ability) {
        if ($user->is_admin) {
            return true;
        }
    });
}

Now, when $this->authorize('update', $post) is called, Laravel first checks the before gate. Since the admin returns true, they get in immediately. If the user isn't an admin, the gate returns null, and Laravel falls back to the specific PostPolicy@update logic. It's clean, centralized, and prevents you from forgetting the admin check in a new policy six months from now.

When to use Gates versus Policies

I often see developers get confused about which one to use. I like to think of it as "Model-based" vs "Action-based".

  • Policies: Use these when the authorization is tied to a specific Eloquent model. "Can this user edit this specific Post?" "Can this user delete this specific Comment?" If you have a model, you probably need a policy.
  • Gates: Use these for general actions that aren't tied to a record in the database. "Can this user access the admin dashboard?" "Can this user switch the site to maintenance mode?" These are essentially closures that act as quick checkpoints.

For a Gate, you define it in the provider and check it like this:

// Definition
Gate::define('access-admin-panel', function (User $user) {
    return $user->role === 'admin';
});

// Usage in Controller
if (Gate::allows('access-admin-panel')) {
    // Let them in
}


One last pro tip: stop putting authorization logic in your controllers if you can avoid it. You can use the can middleware directly in your routes file. It keeps your controllers lean and ensures the check happens before the request even hits your method.

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');



📋 Practical Task

Implement a Project Manager Override for Task Management

You are building a project management tool. There are Tasks and Projects. Each task belongs to a project, and each project has one ProjectManager (a User).

Your goal: Create an authorization system where:

  1. A regular user can only update a task if they are the creator_id of that task.
  2. A user can update any task in a project if they are the ProjectManager of the project that task belongs to.

Requirements:

  • Generate a TaskPolicy.
  • Implement the update method in the policy to handle both the creator check and the Project Manager check (hint: you'll need to traverse the relationship from the Task to the Project).
  • Apply this authorization in a mock controller method using $this->authorize()` or the Gate facade.
  • Ensure you are not using a global Gate::before for this, as Project Managers should only have power over their projects, not every project in the system.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.