Course outline

Laravel / Application boundaries and reliable workflows / Lesson 10

Policies and resource authorization

Place access decisions at the server resource boundary and keep authentication, binding, validation, and presentation distinct.

About 5 min readContent reviewed
On this page

Lesson goals

  • Name the operation and resource
  • Express the training ownership rule in a policy
  • Review bypasses, lists, and concurrent changes

Name the operation and resource

An authorization decision asks whether a particular actor may perform a particular operation on a particular resource under the current context. “The user is logged in” is insufficient for editing another author’s post. Gates suit named decisions, while policies group operations around a model or resource. State the business rule before choosing the API: for this synthetic lesson, a persisted authenticated user may create a post and only its author may update it. Real moderation roles, tenant boundaries, publication workflow, and delegated access need explicit additional rules rather than assumptions hidden in a controller branch.

Make policy registration and invocation observable

Laravel can discover policies under its documented model and policy naming conventions, and explicit Gate::policy registration is available when the structure differs or the application chooses an explicit mapping. Verify that the intended class is actually selected. A policy method existing in the repository does nothing for an endpoint that never invokes authorization. Route model binding supplies a model or a missing-model result; it does not automatically prove ownership. Keep authentication middleware at the route boundary and invoke the required resource ability before performing a state change. Tests should fail if that invocation is removed.

Express the training ownership rule in a policy

The first fragment assumes standard App\Models\User and Post classes with persisted identifiers and posts.user_id. It compares identifiers through their string representation so the rule does not accidentally reject equivalent scalar key representations. This comparison is appropriate only under the declared schema contract; a polymorphic, composite, or tenant-qualified identity may need a different rule. The create method supports the Form Request from the preceding lesson. Under standard application conventions the class is discoverable as PostPolicy; alternatively register Post::class to PostPolicy::class in a provider’s boot method. No privileged bypass is introduced by this simple example.

php Contextual Laravel 12 application fragment
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
    public function create(User $user): bool
    {
        return $user->exists;
    }
    public function update(User $user, Post $post): bool
    {
        return (string) $user->getAuthIdentifier() === (string) $post->user_id;
    }
}
Expected output
Synthetic Post user_id=7: User id=7 allowed; User id=9 denied.

Use documented discovery for these conventional model/policy names or explicitly map them in a provider. A persisted author is allowed to update their own post; another identity is denied.

Authorize the write before applying it

The second controller authorizes update on the bound Post before validating its writable fields and issuing an update. It assumes an authenticated web route, the policy mapping, the stated schema, and a posts.show route. The order makes the access boundary explicit; a Form Request can also delegate authorization when that better suits the action. The client cannot choose the author through this write shape. With a synthetic post owned by user A, authenticated user B receives a forbidden response and no title change. An allowed A update can proceed after valid input. These are intended test expectations, not observations from an executed database operation.

php Contextual Laravel 12 application fragment
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Gate;
class UpdatePostController extends Controller
{
    public function update(Request $request, Post $post): RedirectResponse
    {
        Gate::authorize("update", $post);
        $data = $request->validate([
            "title" => ["required", "string", "max:120"],
            "body" => ["required", "string", "max:5000"],
        ]);
        $post->update(["title" => $data["title"], "body" => $data["body"]]);
        return redirect()->route("posts.show", $post);
    }
}
Expected output
Authenticated non-author: 403 and unchanged post. Valid author request: update then redirect.

Requires authenticated web routing and the declared policy/schema. Gate denial occurs before mutation; the endpoint permits only title and body changes.

Review bypasses, lists, and concurrent changes

A Blade can directive or a hidden button can improve presentation but cannot enforce a direct HTTP request. Resource responses should also expose only appropriate fields, while list queries need a visibility scope because a viewAny decision alone does not filter every returned row. Before hooks and privileged overrides require careful review: their short-circuit behavior can broaden all abilities unexpectedly. If mutable ownership or publication state matters to the final write, account for concurrency in the domain transaction rather than assuming an earlier policy result reserves that state. Test guests, owners, other users, missing resources, and any real privileged role separately.

Common errors and fixes

A bound model is treated as authorized

Why it happens
Binding existence was confused with permission.
How to fix it
Invoke the ability for that actor and resource.
Prevent it next time
Test direct requests by another authenticated user.

The policy is never consulted

Why it happens
Discovery or explicit registration does not match the class layout.
How to fix it
Verify the mapping and an observable deny case.
Prevent it next time
Keep a feature test that proves policy enforcement.

An endpoint denies in the page but accepts a direct write

Why it happens
Only presentation was conditionally hidden.
How to fix it
Enforce authorization before server-side mutation.
Prevent it next time
Treat UI permissions as supplementary feedback.

Check your understanding

1. Does a successful viewAny policy result mean every row in a list is visible?

Show worked solution

No. It can authorize access to the listing operation while the query still needs the required tenant, ownership, or publication scope. Design both decisions and test that an excluded row never appears. Per-row policy checks after loading are not a substitute for a bounded scoped query.

2. Which assertions prove an unauthorized update did not partially succeed?

Show worked solution

Assert the forbidden response and reload the stored record to confirm the original title and author remain unchanged. A response assertion alone can miss a controller that writes before denying. Also include a valid-owner test so an always-deny implementation cannot satisfy the whole suite.

Key takeaways

  • Bind existence and authorize permission separately.
  • Server enforcement must precede mutation.
  • Test denial plus unchanged state.

Official references