Advanced Laravel
Authorization: Policies & Gates Deep Dive
Authorization: Policies & Gates Deep Dive
Master Laravel's authorization system with advanced policy patterns, gate definitions, before/after callbacks, resource authorization, and authorization for guest users.
Understanding Gates vs Policies
Gates and policies are Laravel's two authorization mechanisms:
// Gates: Simple closures for quick authorization checks
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
// Policies: Classes that organize authorization logic around models
class PostPolicy
{
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
Pro Tip: Use gates for simple, model-independent checks. Use policies for model-specific authorization that groups related logic together.
Advanced Policy Patterns
Create comprehensive policies with advanced patterns:
// Generate policy
php artisan make:policy PostPolicy --model=Post
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\User;
use App\Models\Post;
class PostPolicy
{
// Executed before all other methods
public function before(User $user, string $ability)
{
// Super admins can do anything
if ($user->isSuperAdmin()) {
return true;
}
// Return null to continue to other methods
return null;
}
// View any posts (index)
public function viewAny(User $user)
{
return $user->hasPermission('view-posts');
}
// View single post
public function view(?User $user, Post $post)
{
// Guest users can view published posts
if ($post->is_published) {
return true;
}
// Authenticated users can view their own drafts
return $user && $user->id === $post->user_id;
}
// Create new post
public function create(User $user)
{
return $user->hasPermission('create-posts')
&& !$user->isBanned();
}
// Update existing post
public function update(User $user, Post $post)
{
// Owner can update
if ($user->id === $post->user_id) {
return true;
}
// Editors can update any post
if ($user->hasRole('editor')) {
return true;
}
return false;
}
// Delete post
public function delete(User $user, Post $post)
{
// Only owner can delete
if ($user->id === $post->user_id) {
return true;
}
return false;
}
// Force delete (permanent)
public function forceDelete(User $user, Post $post)
{
return $user->hasRole('admin');
}
// Restore soft-deleted post
public function restore(User $user, Post $post)
{
return $user->hasRole(['admin', 'editor']);
}
// Custom ability: publish post
public function publish(User $user, Post $post)
{
return $user->hasRole(['admin', 'editor'])
|| ($user->id === $post->user_id && $user->isVerified());
}
}
Registering Policies
Register policies in AuthServiceProvider:
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Post::class => PostPolicy::class,
];
public function boot()
{
$this->registerPolicies();
// Auto-discover policies
Gate::guessPolicyNamesUsing(function ($modelClass) {
return 'App\\Policies\\' . class_basename($modelClass) . 'Policy';
});
}
}
Using Authorization in Controllers
Apply authorization checks in various ways:
namespace App\Http\Controllers;
class PostController extends Controller
{
// Method 1: authorize() helper
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
// If we reach here, user is authorized
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
// Method 2: Gate facade
public function destroy(Post $post)
{
if (Gate::denies('delete', $post)) {
abort(403, 'You cannot delete this post.');
}
$post->delete();
return redirect()->route('posts.index');
}
// Method 3: User can() method
public function publish(Post $post)
{
if (!auth()->user()->can('publish', $post)) {
return back()->with('error', 'You cannot publish this post.');
}
$post->update(['is_published' => true]);
return back()->with('success', 'Post published!');
}
// Method 4: Middleware
public function __construct()
{
$this->middleware('can:create,App\Models\Post')->only(['create', 'store']);
$this->middleware('can:update,post')->only(['edit', 'update']);
$this->middleware('can:delete,post')->only(['destroy']);
}
// Method 5: authorizeResource (automatic)
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
}
Note: The
authorizeResource() method automatically maps controller methods to policy methods, reducing boilerplate authorization code.
Advanced Gate Definitions
Define complex gates with parameters and logic:
// app/Providers/AuthServiceProvider.php
public function boot()
{
$this->registerPolicies();
// Simple gate
Gate::define('view-dashboard', function (User $user) {
return $user->hasRole(['admin', 'manager']);
});
// Gate with resource
Gate::define('assign-role', function (User $user, User $target, string $role) {
// Can't assign role to yourself
if ($user->id === $target->id) {
return false;
}
// Can only assign roles lower than your own
return $user->role_level > Role::where('name', $role)->first()->level;
});
// Gate with multiple resources
Gate::define('transfer-post', function (User $user, Post $post, User $newOwner) {
return $user->id === $post->user_id
&& $newOwner->hasPermission('own-posts');
});
// Gate that allows guest users
Gate::define('read-post', function (?User $user, Post $post) {
if ($post->is_public) {
return true;
}
return $user && $user->id === $post->user_id;
});
// Intercepting authorization checks
Gate::before(function (User $user, string $ability) {
// Super admins bypass all checks
if ($user->hasRole('super-admin')) {
return true;
}
// Return null to continue normal authorization
return null;
});
Gate::after(function (User $user, string $ability, bool|null $result) {
// Log authorization failures
if ($result === false) {
Log::info("Authorization denied: {$ability}", [
'user_id' => $user->id,
'ability' => $ability,
]);
}
// Return null to preserve original result
return null;
});
}
Policy Responses with Messages
Return detailed responses from policies:
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function update(User $user, Post $post)
{
if ($user->id === $post->user_id) {
return Response::allow();
}
if ($post->is_locked) {
return Response::deny('This post is locked and cannot be edited.');
}
if ($user->isBanned()) {
return Response::deny('Your account has been banned.', 403);
}
return Response::deny('You do not have permission to edit this post.');
}
public function delete(User $user, Post $post)
{
if ($post->hasComments()) {
return Response::denyWithStatus(
422,
'Cannot delete post with comments. Archive it instead.'
);
}
return $user->id === $post->user_id
? Response::allow()
: Response::deny('Only the author can delete this post.');
}
}
// Handling in controller
public function update(Request $request, Post $post)
{
$response = Gate::inspect('update', $post);
if ($response->denied()) {
return back()->with('error', $response->message());
}
// Continue with update
}
Warning: Always check authorization before performing sensitive operations, even if the UI hides unauthorized actions. Client-side restrictions are not security measures.
Authorization in Blade Templates
Control UI elements based on authorization:
{{-- Check single ability --}}
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
{{-- Check with else --}}
@can('delete', $post)
<button type="submit">Delete</button>
@else
<span class="text-muted">Cannot delete</span>
@endcan
{{-- Check cannot --}}
@cannot('create', App\Models\Post::class)
<p>You don't have permission to create posts.</p>
@endcannot
{{-- Check multiple abilities (OR) --}}
@canany(['update', 'delete'], $post)
<div class="post-actions">
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@can('delete', $post)
<button>Delete</button>
@endcan
</div>
@endcanany
{{-- Check gate without resource --}}
@can('view-dashboard')
<a href="{{ route('dashboard') }}">Dashboard</a>
@endcan
{{-- Guest authorization --}}
@guest
@can('read-post', $post)
<p>{{ $post->content }}</p>
@endcan
@endguest
Authorization for API Resources
Apply authorization in API contexts:
namespace App\Http\Controllers\Api;
class PostController extends Controller
{
public function index(Request $request)
{
$this->authorize('viewAny', Post::class);
return PostResource::collection(
Post::paginate()
);
}
public function store(Request $request)
{
$this->authorize('create', Post::class);
$post = Post::create($request->validated());
return new PostResource($post);
}
public function update(Request $request, Post $post)
{
$response = Gate::inspect('update', $post);
if ($response->denied()) {
return response()->json([
'message' => $response->message(),
], $response->status() ?? 403);
}
$post->update($request->validated());
return new PostResource($post);
}
// Bulk authorization
public function bulkDelete(Request $request)
{
$postIds = $request->input('post_ids');
$posts = Post::findMany($postIds);
// Check authorization for each post
foreach ($posts as $post) {
if (Gate::denies('delete', $post)) {
return response()->json([
'message' => "Cannot delete post {$post->id}",
], 403);
}
}
// All authorized, proceed with deletion
Post::destroy($postIds);
return response()->json([
'message' => 'Posts deleted successfully'
]);
}
}
Dynamic Policy Registration
Register policies programmatically based on application state:
// app/Providers/AuthServiceProvider.php
public function boot()
{
$this->registerPolicies();
// Register policies from database
$customPolicies = PolicyConfiguration::all();
foreach ($customPolicies as $config) {
Gate::define($config->name, function (User $user) use ($config) {
return $user->hasPermission($config->required_permission);
});
}
// Register policies from plugins
$plugins = app(PluginManager::class)->getActivePlugins();
foreach ($plugins as $plugin) {
foreach ($plugin->getPolicies() as $modelClass => $policyClass) {
Gate::policy($modelClass, $policyClass);
}
}
}
Testing Authorization
Write tests for authorization logic:
// tests/Feature/PostAuthorizationTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
class PostAuthorizationTest extends TestCase
{
public function test_user_can_update_own_post()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->assertTrue($user->can('update', $post));
}
public function test_user_cannot_update_others_post()
{
$user = User::factory()->create();
$post = Post::factory()->create();
$this->assertFalse($user->can('update', $post));
}
public function test_admin_can_delete_any_post()
{
$admin = User::factory()->admin()->create();
$post = Post::factory()->create();
$this->assertTrue($admin->can('delete', $post));
}
public function test_unauthorized_update_returns_403()
{
$user = User::factory()->create();
$post = Post::factory()->create();
$response = $this->actingAs($user)
->put(route('posts.update', $post), [
'title' => 'New Title'
]);
$response->assertForbidden();
}
}
Exercise 1: Create a comprehensive authorization system for a blog with the following rules:
1. Users can view all published posts, but only their own drafts
2. Users can edit their own posts within 24 hours of creation
3. Editors can edit any post, but not delete them
4. Admins can do everything
5. Banned users cannot create or edit posts
Implement with policies, test all scenarios, and add Blade directives.
1. Users can view all published posts, but only their own drafts
2. Users can edit their own posts within 24 hours of creation
3. Editors can edit any post, but not delete them
4. Admins can do everything
5. Banned users cannot create or edit posts
Implement with policies, test all scenarios, and add Blade directives.
Exercise 2: Build a document sharing system where:
1. Document owners can view, edit, delete, and share
2. Collaborators (editor role) can view and edit
3. Viewers can only view
4. Implement a "transfer ownership" feature with proper authorization
5. Add folder-level permissions that cascade to documents
Use policies with custom abilities and response messages.
1. Document owners can view, edit, delete, and share
2. Collaborators (editor role) can view and edit
3. Viewers can only view
4. Implement a "transfer ownership" feature with proper authorization
5. Add folder-level permissions that cascade to documents
Use policies with custom abilities and response messages.
Exercise 3: Implement a multi-level approval system:
1. Regular users submit requests
2. Managers can approve/reject up to $10,000
3. Directors can approve/reject up to $50,000
4. Executives can approve any amount
5. Users can view their own requests, managers see team requests
Use gates with multiple parameters and custom before/after hooks to log all authorization checks.
1. Regular users submit requests
2. Managers can approve/reject up to $10,000
3. Directors can approve/reject up to $50,000
4. Executives can approve any amount
5. Users can view their own requests, managers see team requests
Use gates with multiple parameters and custom before/after hooks to log all authorization checks.