Chapter 4 said abilities decide what a token may do, and Policies decide which records it may do it to. Consumers are the first resource in this API where the second question is real.
The rule: an operator, a caller whose token has the consumers:manage ability, may do anything. An ordinary consumer may read its own record and nothing else.
Most of that rule is about an ability, and Chapter 4 already has the tool for abilities:
// app/Http/Controllers/ConsumerController.php
#[Middleware('ability:consumers:manage', except: ['show'])]
class ConsumerController
{
#[Authorize('view', 'consumer')]
public function show(Consumer $consumer): ConsumerResource
{
return ConsumerResource::make($consumer);
}
// ... index, store, update, destroy
}
Every action except show needs the operator’s ability, and so will any action added later. Only show asks a question about a record: is this consumer the caller? That question belongs in a Policy.
php artisan make:policy ConsumerPolicy --model=Consumer
// app/Policies/ConsumerPolicy.php
class ConsumerPolicy
{
public function view(
Consumer $caller,
Consumer $consumer,
): Response {
$allowed = $caller->is($consumer)
|| $caller->tokenCan('consumers:manage');
return $allowed
? Response::allow()
: Response::denyAsNotFound();
}
}
$caller->is($consumer) is true when both are the same row. When they aren’t, and the caller is no operator, the answer is a 404 and not a 403. A 403 would confirm that consumer 7 exists, and to a caller who has no business knowing, that is already information.
One name needs a word. In a Policy, Response is Illuminate\Auth\Access\Response, the Policy’s answer, and not the HTTP response the word means everywhere else in this book.
Laravel finds the Policy by its name: ConsumerPolicy in app/Policies belongs to Consumer in app/Models, with no registration. In the attribute, the string 'consumer' names the route parameter, so the Policy receives the model that route model binding already loaded. A request that fails is refused before the method runs. (Laravel documentation: Authorization › Creating Policies and Controllers › Authorization Attributes.)
One more line makes the two checks agree about what they give away. Laravel loads the {consumer} record before a route’s own middleware runs. A caller without the ability would therefore get a 404 for a consumer that doesn’t exist and a 403 for one that does, and could map your IDs by the difference. Tell Laravel to check abilities first:
// bootstrap/app.php, inside withMiddleware()
$middleware->prependToPriorityList(
before: SubstituteBindings::class,
prepend: CheckForAnyAbility::class,
);
Now a token without the ability gets a 403 whatever the ID, before the record is looked up. If you ever use Sanctum’s other middleware, CheckAbilities, which demands every ability listed, move it the same way. (Laravel documentation: Middleware › Sorting Middleware.)
Why a Policy and not an if in the controller? Because of the next endpoint. When someone adds GET /consumers/{consumer}/usage six months from now, the question “whose record is this?” has one place it is answered, and a test file that already covers it.