Laravel action/access control with policies

Mar 30, 2022 by Thibault Debatty | 507 views

Laravel PHP

https://cylab.be/blog/210/laravel-actionaccess-control-with-policies

When developing a web application with Laravel, you will usually have to deal with different users, that have different permissions. If the application is quite simple, with only 2 types of users (administrators that are almighty and regular users that have no permission) you can use a middleware to protect your administrator's area. In this blog post we look at Laravel policies, that allow fine-grain control of user actions.

Concepts

In your Laravel app, you probably have multiple controllers and actions, like PostController@update for example. If your application supports multiple users, you must (probably) ensure that a user can only update his own Post.

A quick (and dirty) to implement this control is in the Controller itself:

public function update(Request $request, Post $post)
{
    if ($post->user_id !== Auth::id) {
        abort(403); // Unauthorized
    }
    // rest of processing
}

As time goes and the application grows, this will clutter the code and cause (security) bugs.

The other approach is to factor the access control code out of your controller, into a dedicated Policy class.

The PostPolicy class will contain the access control code:

class PostPolicy
{
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

And the PostController contains a single line to perform the verification:

public function update(Request $request, Post $post)
{
  // check that the current user is authorized to update this post
  $this->authorize('update', $post);

  // rest of processing
}

Moreover, as you might guess from the example above, Laravel automatically determines the appropriate Policy class to call: the line $this->authorize('update', $post); will eventually call the Policy method PostPolicy@update.

This will make your code much easier to read and maintain!

However, there is one catch: in Laravel Policies are mapped to Models, not to Controllers. We will see the consequences of this in the example below...

Creating policies

The easiest way to create a Policy is using artisan:

php artisan make:policy PostPolicy

Your new Policy class will be stored in /App/Policies.

Registering policies

As mentioned above, in Laravel the Policy class is automatically determined, based on the Model that the user acts on. This mapping must be defined in /App/Providers/AuthServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as 
    ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     */
    protected $policies = [
        \App\Post::class => \App\Policies\PostPolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     */
    public function boot()
    {
        $this->registerPolicies();
    }
}

Implementing access control logic

Now that the Policy is registered, we may add the methods that will check the actions that a user is allowed to execute (or not). Each method takes an instance of User as first argument and must return a boolean. For example, in /App/Policies/PostPolicy.php:

<?php

namespace App\Policies;

use App\Post;
use App\User;

class PostPolicy
{

    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

Calling Policy methods

In your code, the easiest way to call a Policy method is using authorize. The first argument is the name of the Policy method, and the second is the Model object to validate:

public function update(Request $request, Post $post)
{
    // check that the current user is authorized to update this post
    $this->authorize('update', $post);

    // rest of processing
}

Obviously, you can use the same Policy method at multiple places in your code:

/**
 * Show the form for editing the specified Post.
 */
public function edit(Post $post)
{
    $this->authorize('update', $post);
    return view("post.edit", ["post" => $post]);
}

Policy methods without model

In some cases, the controller action has no instance of a Model to act on:

/**
 * Show the form for creating a new Post.
 */
public function create()
{
    return view("post.create");
}

As you may remember, Laravel uses the Model class to find the appropriate Policy class. In those cases, the second parameter of the authorize method must be the related class:

/**
 * Show the form for creating a new Post.
 */
public function create()
{
    // check that this user is authorized to create a Post
    $this->authorize("create", Post::class);
    return view("post.create");
}

In the PostPolicy class (in /App/Policies/PostPolicy.php), the corresponding method takes a single User argument:

public function create(User $user)
{
    return $user->isAuthor();
}

Going further

Policies are described with much more details on the documentation page of Laravel:

https://laravel.com/docs/8.x/authorization#creating-policies