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=Article

Això 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>
@endcannot

Directiva @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>
@endcanany

Accions 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>
@endcan

Autorització 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).