Polítiques i Gates
Com implementar autorització a Laravel amb Gates i Policies per controlar l'accés als recursos.
Autorització a Laravel#
L'autorització determina si un usuari autenticat té permís per realitzar una acció concreta. Per exemple: pot aquest usuari editar aquest article? Pot eliminar aquest comentari? Pot accedir al panell d'administració? Laravel proporciona dues eines per gestionar l'autorització: gates i policies.
Els gates són closures que defineixen permisos generals, mentre que les policies organitzen els permisos per model. La majoria d'aplicacions utilitzen una combinació d'ambdós: gates per a accions generals (accedir al panell d'admin, veure estadístiques) i policies per a accions sobre models específics (editar un article, eliminar un comentari).
Gates#
Un gate és una closure que determina si un usuari pot fer una acció determinada. Es defineixen al mètode boot del AppServiceProvider i reben sempre l'usuari autenticat com a primer paràmetre:
// app/Providers/AppServiceProvider.php
use App\Models\User;
use App\Models\Article;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('admin', function (User $user) {
return $user->role === 'admin';
});
Gate::define('update-article', function (User $user, Article $article) {
return $user->id === $article->user_id;
});
Gate::define('view-stats', function (User $user) {
return in_array($user->role, ['admin', 'editor']);
});
}Utilitzar gates#
Hi ha diverses maneres de comprovar un gate. La més directa és amb els mètodes allows i denies:
use Illuminate\Support\Facades\Gate;
if (Gate::allows('update-article', $article)) {
// L'usuari pot editar l'article
}
if (Gate::denies('update-article', $article)) {
// L'usuari NO pot editar l'article
}El mètode authorize és més concís: si l'usuari no té permís, llança automàticament una excepció AuthorizationException que Laravel converteix en una resposta 403:
public function update(Request $request, Article $article)
{
Gate::authorize('update-article', $article);
// Si arribem aquí, l'usuari té permís
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}Pots comprovar múltiples gates alhora amb any i none:
// L'usuari pot fer ALMENYS UNA de les accions
if (Gate::any(['update-article', 'delete-article'], $article)) {
// ...
}
// L'usuari NO pot fer CAP de les accions
if (Gate::none(['update-article', 'delete-article'], $article)) {
// ...
}Comprovar gates per a un usuari específic#
Per defecte, els gates utilitzen l'usuari autenticat. Si necessites comprovar permisos per a un usuari diferent, utilitza forUser:
$user = User::find(5);
if (Gate::forUser($user)->allows('update-article', $article)) {
// L'usuari amb ID 5 pot editar l'article
}Gate before i after#
Pots definir accions que s'executen abans o després de qualsevol comprovació d'autorització. Això és útil per donar accés total als administradors sense haver de comprovar cada gate individualment:
Gate::before(function (User $user, string $ability) {
if ($user->role === 'super-admin') {
return true; // Permet tot
}
// Retornar null per continuar amb la comprovació normal
});
Gate::after(function (User $user, string $ability, bool|null $result) {
if ($result === false) {
// Registrar intents d'accés denegats
Log::warning("Accés denegat: {$user->email} va intentar {$ability}");
}
});Si before retorna true o false, el resultat és definitiu i no s'executa el gate corresponent. Si retorna null, es continua amb la comprovació normal.
Respostes de gates#
En lloc de retornar simplement true o false, un gate pot retornar una resposta amb un missatge personalitzat:
use Illuminate\Auth\Access\Response;
Gate::define('update-article', function (User $user, Article $article) {
if ($user->id !== $article->user_id) {
return Response::deny('No ets l\'autor d\'aquest article.');
}
if ($article->published_at && $article->published_at->lt(now()->subDays(30))) {
return Response::deny('No pots editar articles publicats fa més de 30 dies.');
}
return Response::allow();
});Per obtenir la resposta completa (incloent el missatge) sense que es llanci una excepció automàticament:
$response = Gate::inspect('update-article', $article);
if ($response->allowed()) {
// Continuar
} else {
echo $response->message(); // "No ets l'autor d'aquest article."
}Policies#
Les policies organitzen la lògica d'autorització al voltant d'un model concret. Cada policy és una classe amb mètodes que corresponen a accions: view, create, update, delete, etc. Són la manera recomanada de gestionar l'autorització quan les accions estan lligades a models.
Crear una policy#
php artisan make:policy ArticlePolicy --model=ArticleAixò genera una classe amb els mètodes estàndard a app/Policies/ArticlePolicy.php:
namespace App\Policies;
use App\Models\Article;
use App\Models\User;
class ArticlePolicy
{
public function viewAny(User $user): bool
{
return true; // Qualsevol usuari autenticat pot veure la llista
}
public function view(User $user, Article $article): bool
{
// Articles publicats: tothom els pot veure
// Articles esborrany: només l'autor
return $article->is_published || $user->id === $article->user_id;
}
public function create(User $user): bool
{
return in_array($user->role, ['admin', 'editor', 'author']);
}
public function update(User $user, Article $article): bool
{
return $user->id === $article->user_id;
}
public function delete(User $user, Article $article): bool
{
return $user->id === $article->user_id || $user->role === 'admin';
}
public function restore(User $user, Article $article): bool
{
return $user->role === 'admin';
}
public function forceDelete(User $user, Article $article): bool
{
return $user->role === 'admin';
}
}Registre automàtic de policies#
Laravel registra automàticament les policies que segueixen la convenció de noms: si el model és App\Models\Article, la policy ha de ser App\Policies\ArticlePolicy. No cal registrar-les manualment.
Si vols registrar-les explícitament o utilitzar noms que no segueixen la convenció:
// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::policy(Article::class, ArticlePolicy::class);
Gate::policy(Comment::class, CommentPolicy::class);
}Mètode before a les policies#
Igual que els gates, les policies poden tenir un mètode before que s'executa abans de qualsevol comprovació:
class ArticlePolicy
{
public function before(User $user, string $ability): bool|null
{
if ($user->role === 'admin') {
return true; // L'admin pot fer qualsevol cosa amb articles
}
return null; // Continuar amb la comprovació normal
}
// ...
}Usuaris opcionals#
Per defecte, les policies retornen false automàticament si no hi ha cap usuari autenticat. Si vols que algunes accions siguin accessibles per a convidats, fes que el paràmetre User sigui nullable:
public function view(?User $user, Article $article): bool
{
// Els convidats poden veure articles publicats
if ($article->is_published) {
return true;
}
// Només l'autor pot veure esborranys
return $user && $user->id === $article->user_id;
}Utilitzar policies als controladors#
Amb authorize#
El mètode authorize del controlador comprova la policy i llança una excepció 403 si l'usuari no té permís:
class ArticleController extends Controller
{
public function index()
{
$this->authorize('viewAny', Article::class);
$articles = Article::latest()->paginate(20);
return view('articles.index', compact('articles'));
}
public function show(Article $article)
{
$this->authorize('view', $article);
return view('articles.show', compact('article'));
}
public function create()
{
$this->authorize('create', Article::class);
return view('articles.create');
}
public function edit(Article $article)
{
$this->authorize('update', $article);
return view('articles.edit', compact('article'));
}
public function update(Request $request, Article $article)
{
$this->authorize('update', $article);
$article->update($request->validated());
return redirect()->route('articles.show', $article);
}
public function destroy(Article $article)
{
$this->authorize('delete', $article);
$article->delete();
return redirect()->route('articles.index');
}
}Fixa't que per a accions que no reben un model concret (index, create), passes la classe del model. Per a accions que reben un model (show, edit, update, destroy), passes la instància.
Amb authorizeResource#
Si el controlador és un resource controller i els mètodes de la policy segueixen les convencions, pots autoritzar totes les accions de cop al constructor:
class ArticleController extends Controller
{
public function __construct()
{
$this->authorizeResource(Article::class, 'article');
}
// Tots els mètodes queden autoritzats automàticament:
// index → viewAny
// show → view
// create → create
// store → create
// edit → update
// update → update
// destroy → delete
}Amb middleware#
Pots aplicar l'autorització com a middleware a les rutes:
Route::put('/articles/{article}', [ArticleController::class, 'update'])
->middleware('can:update,article');
Route::post('/articles', [ArticleController::class, 'store'])
->middleware('can:create,App\Models\Article');Amb Form Requests#
Els Form Requests tenen un mètode authorize que pot utilitzar policies:
class UpdateArticleRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('update', $this->route('article'));
}
public function rules(): array
{
return [
'title' => 'required|max:255',
'body' => 'required',
];
}
}Autorització a les vistes Blade#
Laravel proporciona directives Blade per mostrar o amagar elements segons els permisos de l'usuari.
Directives @can i @cannot#
@can('update', $article)
<a href="{{ route('articles.edit', $article) }}" class="text-blue-600 hover:underline">
Editar
</a>
@endcan
@cannot('delete', $article)
<p class="text-gray-500 text-sm">No tens permís per eliminar aquest article.</p>
@endcannotDirectiva @canany#
Per comprovar si l'usuari pot fer almenys una d'un conjunt d'accions:
@canany(['update', 'delete'], $article)
<div class="flex gap-2">
@can('update', $article)
<a href="{{ route('articles.edit', $article) }}">Editar</a>
@endcan
@can('delete', $article)
<form method="POST" action="{{ route('articles.destroy', $article) }}">
@csrf
@method('DELETE')
<button type="submit">Eliminar</button>
</form>
@endcan
</div>
@endcananyAccions sense model#
Per a accions que no necessiten un model concret (com create), passa la classe:
@can('create', App\Models\Article::class)
<a href="{{ route('articles.create') }}">Nou article</a>
@endcanAutorització amb l'usuari#
L'usuari autenticat pot comprovar permisos directament amb els mètodes can i cannot:
// Al controlador o a qualsevol lloc amb accés a l'usuari
if ($user->can('update', $article)) {
// ...
}
if ($user->cannot('delete', $article)) {
// ...
}
// Per a accions sense model
if ($user->can('create', Article::class)) {
// ...
}Respostes de policies#
Igual que els gates, les policies poden retornar respostes amb missatges:
use Illuminate\Auth\Access\Response;
public function update(User $user, Article $article): Response
{
if ($user->id !== $article->user_id) {
return Response::deny('No ets l\'autor d\'aquest article.');
}
if ($article->is_locked) {
return Response::deny('Aquest article està bloquejat per un administrador.');
}
return Response::allow();
}Quan la policy retorna una resposta deny, el missatge s'inclou a la resposta 403 si l'aplicació retorna JSON (com en una API). A les aplicacions web, el missatge es pot recuperar del MessageBag de la sessió.
Exemple complet#
Un cas d'ús comú és un blog on els autors poden gestionar els seus articles i els editors poden revisar i publicar:
// app/Policies/ArticlePolicy.php
class ArticlePolicy
{
public function before(User $user, string $ability): bool|null
{
if ($user->role === 'admin') {
return true;
}
return null;
}
public function viewAny(?User $user): bool
{
return true;
}
public function view(?User $user, Article $article): bool
{
if ($article->is_published) {
return true;
}
return $user && (
$user->id === $article->user_id ||
$user->role === 'editor'
);
}
public function create(User $user): bool
{
return in_array($user->role, ['author', 'editor']);
}
public function update(User $user, Article $article): bool
{
return $user->id === $article->user_id || $user->role === 'editor';
}
public function delete(User $user, Article $article): bool
{
return $user->id === $article->user_id;
}
public function publish(User $user, Article $article): bool
{
return $user->role === 'editor';
}
}Fixa't que pots crear mètodes personalitzats com publish que no corresponen als mètodes estàndard d'un CRUD. Simplement els cridaràs amb $this->authorize('publish', $article).