Laravel route model binding turns a route identifier into a model instance. That removes lookup boilerplate, but the bound model is only a lookup result. A matching case ID does not prove the signed-in reviewer may see it. A policy expresses an action against a specific model and caller; the controller or route must invoke that policy before returning private fields. For a private resource, a policy can deliberately make a denied case appear missing so a guessed ID does not reveal existence. The same authorization rule must cover exports and state changes. A successful GET authorization is not a durable grant for a later POST after assignments or case status change.
Laravel Route Binding, Policy, and Private Resource Scope
Working case
Reviewer 47 can inspect permit 62. Reviewer 81 sees another queue, guesses 62 in the address bar, and receives a bound PermitCase because the route accepts any existing primary key. A detail endpoint that serializes the whole model then exposes internal district notes. Adding an auth middleware stops anonymous callers, but reviewer 81 remains signed in. The repaired route invokes a view policy on the bound case and returns only a deliberate resource projection. Export metadata uses the same policy. Approval calls a current-state service that checks the reviewer's assignment inside the write transaction, because a previously authorized screen may be stale.
Implementation boundary
<?php
use App\Http\Resources\PermitResource;
use App\Models\PermitCase;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->get('/permits/{permit}', function (PermitCase $permit) {
Gate::authorize('view', $permit);
return new PermitResource($permit);
});Keep authentication middleware on private routes and register the policy for the PermitCase model. Make the view rule inspect the current reviewer-to-case mapping and return a denial shaped by the product's existence-disclosure rule. Invoke authorization before constructing a response resource or starting an export. Do not rely on a controller's menu filtering or client-hidden button. For nested routes, scoped bindings can constrain children to their parent relationship, but they do not replace a user policy. In the mutation service, re-read the case under a lock and verify the caller and requested transition again. Return a narrow resource rather than every database column. Record a request identifier for support without including hidden titles in client errors.
Cost and boundaries
Authentication, model binding, and a policy lookup each can perform database work. A policy that walks a lazy relationship on every list row creates repeated queries; list endpoints should instead apply a permission-scoped base query and avoid authorizing hundreds of hydrated models one by one when the policy can be represented in SQL. A targeted detail lookup usually needs only one indexed assignment check. Denying private resources as missing may make client diagnostics less obvious, so capture a safe internal reason and request ID. Measure query count for allowed, forbidden, and missing IDs, plus the time spent in policy evaluation under large reviewer assignments.
Failure trace
Call the bound route anonymously, as reviewer 47, as reviewer 81, and with an unknown ID. The forbidden and unknown responses should follow the same chosen disclosure policy. Add an export route and verify it cannot bypass the view rule. Remove the policy invocation in a negative test and observe the guessed-ID leak, then restore it. Revoke reviewer 47's assignment after a detail GET and attempt approval; the service must reject the now-stale permission. Return a model with private notes from a negative test and verify the resource projection excludes them. Watch for extra relationship queries when policies run across a queue.
Verification
- A bound model is still checked against the current reviewer.
- Export and mutation entry points repeat their required action checks.
- Public resources exclude internal case fields.
Practice drill
Implement detail, export metadata, and approval routes for a permit workflow. Seed cases 62 and 81 with disjoint reviewers. Write the PermitCase view and approve policy rules, choosing whether a forbidden detail should be 403 or private 404. Use route binding for the detail page, then add an explicit policy check and a response resource. Implement approval in a service that reevaluates current permission under a transaction. Add tests for guessed IDs, revoked assignment, and a list of 47 cases. Record query counts and prove that private model attributes never enter the public response.
Decision note
Let binding locate the case; let a policy and current-state service decide whether that reviewer may use it.
Common Mistakes
- Treating route model binding as permission.
- Checking only whether a reviewer is signed in.
- Returning the entire model after a successful policy check.
Related lessons
Laravel Policy, Query, and Queue Boundaries; Laravel Eloquent Loading and Cursor Page Cost; Laravel Form Requests, Transactions, and Approval Replay; Laravel After-Commit Jobs and Outbox Recovery; Authorization and Tenant Boundaries; Django Middleware, Sessions, and Object Permissions.
Apply and check
Build Project: Laravel permit review workflow and review Web Development: Laravel policy, query, and queue quiz.
