El Capítulo 4 dijo que las habilidades deciden lo que un token puede hacer, y las Policies deciden a qué registros puede hacérselo. Los consumidores son el primer recurso de esta API en el que la segunda pregunta es real.
La regla: un operador, es decir, alguien cuyo token tiene la habilidad consumers:manage, puede hacer cualquier cosa. Un consumidor corriente puede leer su propio registro y nada más.
La mayor parte de esa regla trata de una habilidad, y el Capítulo 4 ya tiene la herramienta para las habilidades:
// 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
}
Todas las acciones salvo show necesitan la habilidad del operador, y la necesitará cualquier acción que se añada después. Solo show hace una pregunta sobre un registro: ¿es este consumidor quien llama? Esa pregunta pertenece a una 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) es verdadero cuando ambos son la misma fila. Cuando no lo son, y quien llama no es operador, la respuesta es un 404 y no un 403. Un 403 confirmaría que el consumidor 7 existe, y para alguien que no tiene por qué saberlo, eso ya es información.
Un nombre necesita una aclaración. En una Policy, Response es Illuminate\Auth\Access\Response, la respuesta de la Policy, y no la respuesta HTTP que la palabra significa en el resto de este libro.
Laravel encuentra la Policy por su nombre: ConsumerPolicy en app/Policies pertenece a Consumer en app/Models, sin registrar nada. En el atributo, la cadena 'consumer' nombra el parámetro de la ruta, de modo que la Policy recibe el modelo que el route model binding ya cargó. Una petición que no pasa se rechaza antes de que el método se ejecute. (Documentación de Laravel: Authorization › Creating Policies y Controllers › Authorization Attributes.)
Una línea más hace que las dos comprobaciones coincidan en lo que revelan. Laravel carga el registro {consumer} antes de que se ejecuten los middleware propios de una ruta. Quien llamara sin la habilidad recibiría entonces un 404 para un consumidor que no existe y un 403 para uno que sí, y podría cartografiar tus ID por la diferencia. Dile a Laravel que compruebe primero las habilidades:
// bootstrap/app.php, dentro de withMiddleware()
$middleware->prependToPriorityList(
before: SubstituteBindings::class,
prepend: CheckForAnyAbility::class,
);
Ahora un token sin la habilidad recibe un 403 sea cual sea el ID, antes de que se busque el registro. Si alguna vez usas el otro middleware de Sanctum, CheckAbilities, que exige todas las habilidades listadas, muévelo de la misma manera. (Documentación de Laravel: Middleware › Sorting Middleware.)
¿Por qué una Policy y no un if en el controlador? Por el siguiente endpoint. Cuando alguien añada GET /consumers/{consumer}/usage dentro de seis meses, la pregunta «¿de quién es este registro?» tendrá un solo lugar donde se responde, y un archivo de tests que ya la cubre.