Web^ID
💬 Contact 💬 Contact

07 03 2023

retour à la liste des articles

Laravel Policies

• écrit par [Web^ID]

visuel code laravel policies

Les Policies de Laravel sont un moyen parmi d'autres de contrôler qu'un utilisateur est autorisé ou non à accéder à un contenu ou à déclencher une action. Avant de nous focaliser sur ces Policies, voyons ce que nous avons à disposition pour faire ces vérifications.

Contexte fictif

Imaginons un projet Laravel sur lequel nous avons des utilisateurs avec différents rôles :

  • Administrateur : peut évidemment tout faire
  • Ecrivain : peut créer des contenus de type articles
  • Vendeur : peut créer des contenus de type produits

 

Imaginons une route en POST sur laquelle nous pouvons publier un article


// routes/web.php
Route::post('publier-article', [PostController::class, 'store']);

Et la méthode de contrôleur qui va avec, qui reçoit une Form Request et injecte ses données validées dans un Repository qui se chargera d'enregistrer les données.

// Controllers/PostController.php
    public function store(storePostRequest $request): RedirectResponse
    {
        $this->postRepository->create($request->validated());

        return redirect(route('posts.all'))->with('success', 'Article publié avec succès');
    }

Il manque à cet exemple les vérifications sur les permissions car un visiteur non authentifié ou un utilisateur de type Vendeur n'a pas le droit de créer un article. C'est ce que nous allons voir immédiatement.

Solution 1 : Vérifier le rôle de l'utilisateur dans le contrôleur

 public function store(storePostRequest $request): RedirectResponse
    {
        // On exclu les visiteurs
        if (!auth()->check()) {
            throw new AuthenticationException();
        }

        $user = auth()->user();

        // On vérifie explicitement le rôle de l'utilisateur
        if ($user->role !== Roles::Administrator && $user->role !== Roles::Editor) {
            throw new AuthenticationException();
        }

        $this->postRepository->create($request->validated());

        return redirect(route('posts.all'))->with('success', 'Article publié avec succès');
    }

On pollue le happy path du contrôleur avec des vérifications à rallonge que l'on devra copier coller un peu partout. Ces vérifications devraient être déportées à un endroit plus adéquat dont c'est le métier.

Solution 2 : Utiliser les Middleware

// web.php
Route::middleware('userCanStorePosts')
    ->post('publier-article', [PostController::class, 'store']);
// Middleware/userCanStorePosts.php
public function handle(Request $request, Closure $next): Response
{
    // On exclu les visiteurs
    if (!auth()->check()) {
        abort(403)
    }

    $user = auth()->user();

    // On vérifie explicitement le rôle de l'utilisateur
    if ($user->role !== Roles::Administrator && $user->role !== Roles::Editor) {
        abort(403)
    }

    return $next($request);
}

La logique de vérification n'empiète plus sur le contrôleur

Fastidieux s'il faut créer un middleware par action

Est-ce réellement le rôle d'un middleware ?

Solution 3 : Utiliser les Policies

Il existe évidemment un nombre assez conséquent d'autres solutions mais nous allons nous intéresser aujourd'hui de près aux Policies de Laravel.

Ce sont en quelques mots de classes dont l'unique but est de gérer les autorisations en lecture comme en écriture des modèles.

 

Reprenons notre exemple de création d'article. Nous allons dans un premier temps générer une classe liée à notre modèle Article (Post)

php artisan make:policy PostPolicy

Nous obtenons une classe vide dans le dossier Policies. C'est cette classe qui va être en charge de mettre des stops là où il y a franchissement d'une ligne rouge.

<?php
// Policies/PostPolicy.php
namespace App\Policies;

use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }
}

Nous allons y intégrer une méthode pour l'enregistrement d'un article qui va renvoyer un booléen en fonction du rôle de l'utilisateur que nous récupérons par magie en paramètre.

// Policies/PostPolicy.php
    public function store(User $user): bool
    {
        return $user->role === Roles::Administrator || $user->role === Roles::Editor;
    }

Pour charger la classe et la lier au modèle Article, nous allons enrichir la variable $policies du fichier AuthServiceProvider. C'est d'ici que vient la magie du paragraphe précédent.

// Providers/AppServiceProvider.php
protected $policies = [
  Post::class => PostPolicy::class
];

Enfin, dans la déclaration de la route, nous allons utiliser le Middleware can qui se base sur les Policies.

// web.php
Route::middleware('can:store,' . Post::class)
    ->post('publier-article', [PostController::class, 'store']);

En faisant cela, le code de notre route est facilement compréhensible ("middleware can store post"), notre contrôleur ne s'occupe que du happy path et notre classe de Policies gères toutes les conditions d'accès.

 

En cas de tentative frauduleuse, une 403 sera envoyée à l'utilisateur. Pour plus de précision dans les messages d'erreur, nous pouvons au lieu de renvoyer un booléen dans les méthodes des Policies envoyer des \Illuminate\Auth\Access\Response avec un message personnalisé. 

// Policies/PostPolicy.php
public function store(User $user): bool
{
    return $user->role === Roles::Administrator || $user->role === Roles::Editor
        ? Response::allow()
        : Response::deny('You are not allowed to store posts');
}

Évidemment pour l'exemple nous restons sur quelque chose de relativement simple mais nous pouvons enrichir notre class PostPolicies d'autant de méthodes qu'il y a d'actions à réaliser sur le modèle Post. Nous créerons ensuite bien évidemment autant de classe de Policies que de modèles à manipuler.

 

Dans le cas de la mise à jour d'un contenu existant, celui-ci sera passé automatiquement en paramètre des méthodes de Policies

// Policies/PostPolicy.php
public function update(User $user, Post $post): bool
{
    // Intedire ma mise à jour d'un article archivé
    if ($post->status === Status::Archived) {
        return false;
    }
    return $this->store($user);
}

Si pour une raison les conditions d'accès à un modèle doivent être testées dans un contrôleur ou dans un service, les Policies se marient très bien avec le système de Gates de Laravel. Les Gates sont une série d'outils qui peuvent être utilisés hors du cadre des Policies pour vérifier si une situation spécifique est autorisée.

 

Ci-dessous par exemple, on vérifie si l'utilisateur connecté peut mettre à jour un article via la méthode inspect. On adapte ensuite le code en fonction de la réponse.

// Controllers/PostsController.php
$response = Gate::inspect('update', $post);

if ($response->allowed()) {
    // Faire une action seulement si l'utilisateur peut modifier cet article
} else {
    // Faire une autre action à la place
}

Les Gates sont également utilisables avec les méthodes can et cannot du modèle User.

// Controllers/PostsController.php
if ($request->user()->cannot('update', $post)) {
  // Proposer une alternative aux utilisateur n'ayant pas la possibilité de...
}

Ou encore avec Blade

@can('update', $post)
    Afficher le formulaire de modification seulement si autorisé
@endcan

On peut aussi les voir comme des barrières qui se chargent de stopper l'application si l'utilisateur ne possède pas les droits nécessaires.

Gate::authorize('update', $post);

// Cette partie ne s'exécutera seulement si l'utilisateur a le droit de mettre à jour l'article

 

Nous utilisons régulièrement les Policies chez Web^ID car c'est de notre point de vue la meilleure organisation qu'il soit pour développer un projet conséquent qui reste lisible et maintenable par tous.

 

Vous trouverez évidemment beaucoup plus de détails sur la documentation officielle des Policies tels que l'auto-discovery, les filters, ou les resource controllers.

Vous souhaitez démarrer rapidement ?
Besoin d'accompagnement ?

Vous voulez en savoir plus ou vous souhaitez nous parler de votre projet ?
Nous sommes à votre disposition !