Authorization
๐ก๏ธ Authorization
django-guardian bolted on.
Gates: Simple, Closure-Based Checks
For an authorization rule not tied to a specific model โ "can this user view the admin dashboard at all" โ a Gate is a plain closure:
// app/Providers/AppServiceProvider.php use Illuminate\Support\Facades\Gate; public function boot(): void { Gate::define('view-admin-dashboard', function (User $user) { return $user->is_admin; }); }
if (Gate::allows('view-admin-dashboard')) { /* ... */ } if ($request->user()->can('view-admin-dashboard')) { /* ... */ }
๐ Policies: Object-Level Authorization, By Default
php artisan make:policy BookPolicy --model=Book
// app/Policies/BookPolicy.php namespace App\Policies; use App\Models\Book; use App\Models\User; class BookPolicy { public function update(User $user, Book $book): bool { return $user->id === $book->author_id; // THIS user, THIS book โ genuinely object-level } public function delete(User $user, Book $book): bool { return $user->id === $book->author_id || $user->is_admin; } }
Auto-discovered by naming convention โ BookPolicy pairs with Book automatically, no manual registration needed (in older Laravel versions, or if the naming doesn't match, it's registered explicitly in AppServiceProvider).
Django's Model-Level Default vs Laravel's Object-Level Default
Django (Built-in)
user.has_perm("catalog.change_book") โ a coarse, model-wide permission. It answers "can this user edit some book," not "can they edit this specific book." Per-object checks need django-guardian.
Laravel (Built-in)
BookPolicy::update(User $user, Book $book) receives the actual Book instance โ object-level authorization is the default shape, no additional package required.
Using Policies
// In a controller public function update(UpdateBookRequest $request, Book $book) { $this->authorize('update', $book); // throws 403 automatically if denied $book->update($request->validated()); return redirect()->route('books.show', $book); }
Notice authorize() โ the exact same method name as FormRequest's authorize(): bool hook from Chapter 1's Course 1. That hook existed for exactly this purpose all along; it's the natural place to call a Policy check instead of always returning true:
// app/Http/Requests/UpdateBookRequest.php public function authorize(): bool { return $this->user()->can('update', $this->route('book')); }
In Blade, @can/@cannot check the same policy for conditional rendering:
@can('update', $book) <a href="{{ route('books.edit', $book) }}">Edit</a> @endcan
viewAny/view/create/update/delete
The conventional method names Laravel checks for automatically when a Policy is registered โ matching, not coincidentally, the same actions Route::resource() generates.
Policies Compose With Gate::before()
A Gate::before() closure can grant a superuser blanket access before any individual Policy method even runs โ a common pattern for an is_admin bypass.
๐ป Coding Challenges
Challenge 1: Write a Policy
Generate an AuthorPolicy for the Author model with an update() method that only allows a user to edit an author record they created (assume an owner_id column), and use $this->authorize() in a controller.
Goal: Practice writing and applying an object-level authorization check.
Challenge 2: Use a Policy in a Blade View
Write a Blade template that shows an "Edit" link for a book only if the current user is authorized to update it, using the @can directive.
Goal: Practice conditional UI rendering based on the same Policy check used server-side.
Challenge 3: Add an Admin Bypass With Gate::before()
Add a Gate::before() closure in AppServiceProvider that automatically grants every permission to a user with is_admin = true, and explain why this runs before individual Policy methods rather than needing to be added to each one.
Goal: Practice a global authorization override that composes with per-model Policies.
authorize() Throws โ It Doesn't Return a Boolean$this->authorize('update', $book) throws an AuthorizationException (automatically converted to a 403 response) when denied โ it does not return false the way Gate::allows() or $user->can() do. Writing if ($this->authorize(...)) is a mistake: the code inside that if never runs on denial anyway, since the exception is thrown before authorize() would ever return. Use authorize() as a standalone statement when you want a hard stop, and can()/Gate::allows() when you need an actual boolean to branch on. Separately: if a Policy's class name doesn't match its model by convention (a custom name, or a model in a non-standard namespace), it needs explicit registration in AppServiceProvider's policy mapping โ the same "forgot to wire it up" trap this course has hit repeatedly.
๐ฏ What's Next
The final chapter of this course covers getting everything built across both courses onto a real server: Deployment โ Laravel's production configuration compared to the Django course's deployment chapter.