DEV Community

Chiemela Chinedum
Chiemela Chinedum

Posted on

An Introduction to Laravel's Gates and Policies

Whereas Authentication allows us to verify the identity of an entity accessing a resource, Authorization allows us to determine whether the entity has certain privileges over the said resource.

Authorization is the process of specifying whether an authenticated entity has the rights to access a certain resource on the server. One takeaway from this is that we need to know the entity (authentication) before we can enforce authorization.

Laravel provides two mechanisms for implementing authorization - Gates, and Policies.

What we are going to build

For the purpose of this tutorial, we will work on a simple forum - with just threads 🤓. We will have two types of users - moderators and, well, users. Moderators can create and update threads, users cannot. Only the thread creator can update the thread.

Prerequisites

To follow along with this tutorial, you should have a basic understanding of Laravel and its authentication system. We will attempt to adopt a TDD (Test-Driven Development) system to allow us test our code very quickly. You can learn more about TDD here.

To get the starter code, clone this repository and checkout to the starter branch. Run composer install to pull in the dependencies. You'll notice that the migrations for the User and Thread models have already been set up and the necessary relationships have been included in the models. You also notice that the factories for the User and Thread models have been set up and the phpunit.xml file has been updated to use SQLite and store data in memory.

Finally, open the .env file, fill out your database parameters and run php artisan migrate and you should be set.

So let's dive right in.

Gates

Gates are basically closures (and closures are just anonymous functions) that determine if a user can perform a given action on a resource.

Defining Gates

We define gates using the Gate facade.


use Gate;

// ...

Gate::define("action", function($user) {
    // Authorization logic
});

Gate definition closures will always receive the currently authenticated user as their first argument. The second argument is our closure, where we write our authorization logic for the specified action. This should resolve to a boolean.

We can also define gates using the class@method style - same as we use for defining routes like so:

Gate::define('action', 'ClassName@methodName');

But, more on this later.

Ok. Let's write some code.

Add the following to the boot method of the AuthServiceProvider.php class in the app/Providers folder.


use Gate;

// ...

public function boot()
{
    $this->registerPolicies();

    Gate::define('create-thread', function($user) {
       return $user->role === 'moderator';
    });
}

We are basically saying that a user should only be allowed to create-thread (the action) if they have a role property value of moderator.

Using Gates

Ok. We have our gate defined. How do we make use of them to authorize actions?

We will need to do some setup before we can do this.

First, we will need to create our ThreadsController.


php artisan make:controller ThreadsController

Add a store method to it like so:

public function store(Request $request) 
{

}

Next, set up a route for this action in the web.php file like so:


Route::post('/threads', 'ThreadsController@store')->name('threads.store');

Finally, let's write a test for this feature.

Create a test like so:


php artisan make:test CreatesThreadTest

This will create a CreatesThreadTest.php file in the tests/Feature folder. Add two tests to it as shown below:


<?php

namespace Tests\Feature;

use App\Models\Thread;
use App\Models\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class CreatesThreadTest extends TestCase
{
    use RefreshDatabase;

    / @test */
    public function a_moderator_can_create_a_thread()
    {
        $moderatorUser = factory(User::class)->create(['role' => 'moderator']);

        $thread = factory(Thread::class)->make(['user_id' => null])->toArray();

        $this->actingAs($moderatorUser);

        $this->post('/threads', $thread)
            ->assertStatus(200);

        $this->assertDatabaseHas('threads', array_merge($thread, ['user_id' => $moderatorUser->id]));
    }

    / @test */
    public function a_normal_user_cannot_create_a_thread()
    {
        $normalUser = factory(User::class)->create(['role' => 'user']);

        $thread = factory(Thread::class)->make(['user_id' => null])->toArray();

        $this->actingAs($normalUser);

        $this->post('/threads', $thread)
            ->assertStatus(403);

        $this->assertDatabaseMissing('threads', array_merge($thread, ['user_id' => $normalUser->id]));
    }
}

For the first test, a_moderator_can_create_a_thread, we create a user with a role of moderator, we generate some thread data and then post this data to our post /threads route. We expect a 200 success status response and also expect that the database will have the create data we just generated.

For the second test, a_normal_user_cannot_create_a_thread we use a normal user and we expect that the thread creation fails with a 403 Unauthorized status code. We also make sure the database does not contain the generated thread data.

Type the following into the terminal to run this test:

vendor/bin/phpunit --filter CreatesThreadTest

And of course, we expect our tests to fail. So let's fix this 🧩.

Update the store method of the ThreadsController like so:


public function store(Request $request)
{
    $thread = auth()->user()
                    ->threads()
                    ->create($request->all());

    return response()->json(['thread' => $thread], 200);
}

Here, we use the threads relationship of the user to create a new thread using the request payload.

Note that ideally, you should add some validation for your request but since this tutorial is not about validation, I have skipped that bit.

Run your tests again.

Boom! One passed. But one failed. This is because, at this point, all our users are allowed to create a thread. This is where Gate Authorization comes in. Now update your store method to look like the snippet below. Do not forget to use Gate;.


public function store(Request $request)
{

    if (Gate::allows('create-thread')) {
        $thread = auth()->user()
                        ->threads()
                        ->create($request->all());

        return response()->json(['thread' => $thread], 200);
    }

    return response()->json(['message' => 'Unauthorized Action'], 403);
}

Run those tests again. Now, they should all pass.

We added a conditional using the Gate::allows() method. This method receives the name of the action and uses the authorization logic we defined for it to determine whether the currently authenticated user is authorized to proceed with that action.

There's also the Gate::denies() method which determines if a user is NOT allowed to perform the action specified in its first argument. Also, for cases where we want to perform authorization checks on a user different from the currently authenticated user, we have Gate::forUser($user)->allows() and
Gate::forUser($user)->denies().

Policies

Policies are classes that organize authorization logic around a particular model or resource. This means that with policies, we try to group all the logic that controls the ability to perform actions on a model in one class. The policy class contains methods that define authorization logic for different actions.

Generating policies

We can create policies using artisan like so:

php artisan make:policy PolicyName

The naming convention is to append the word Policy to the corresponding model name. So the policy for our Thread model will be called ThreadPolicy. The command above will create a PolicyName.php file in the app/Policies folder of our project.

Just like Service Providers, policies need to be registered. We do this in the AuthServiceProvider (again 😌). We use the $policies protected class variable, which is an associative array that maps our models to their corresponding policies.


protected $policies = [
    ModelName::class => ModelPolicyName::class
];

Note that Laravel also supports auto-discovery for policies as long as naming conventions for both models and policies are followed and the model and policy are located in the right directories.

Now, we understand the basics, let's write our own policy.

Step 1: We create it

php artisan make:policy ThreadPolicy

Step 2: We register it.

In the AuthServiceProvider.php file, update the $policies variable to look like so

protected $policies = [
    App\Models\Thread::class => App\Policies\ThreadPolicy::class
];

Step 3: Add a method for authorization logic to the policy.

It's time to write some authorization logic. From our Software Spec 😇, we mentioned that only the moderator that created a thread can update it. We can also add a second clause that this user MUST be a moderator. This means that if a moderator ceases to be one, they can no longer update their thread. We will do this in an update method in the ThreadPolicy class.

public function update(User $user, Thread $thread)
{
    return $user->role === "moderator" && $thread->user_id === $user->id;
}

Here we return a boolean that determines if the user is both a moderator and the creator of the thread.

For some databases, even though the thread user_id field was specified as an int, the value will be returned as a string and the triple equality check will not work. We need to explicitly cast the field to an integer and we do that in the app/Models/Thread.php file like so:

protected $casts = ['user_id' => 'integer'];

Step 4: Add an update method in our ThreadsController

public function update(Request $request, Thread $thread)
{

}

As shown above, we use route model binding to inject an instance of the Thread to be updated into our controller method.

Step 5: Add a route for thread update

// web.php

Route::post('/threads/{thread}', 'ThreadsController@update')->name('threads.update');

Step 6: Add some tests for thread update

As promised, we will go the TDD route. Run php artisan make:test UpdatesThreadTest and update the test file create to look like so:


<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
use App\Models\Thread;

class UpdatesThreadTest extends TestCase
{
    use RefreshDatabase;

    / @test */
    public function a_moderator_can_update_his_thread()
    {
        $moderatorUser = factory(User::class)->create(['role' => 'moderator']);

        $thread = factory(Thread::class)->create(['user_id' => $moderatorUser->id]);

        $update = [
            'title' => 'Updated Title'
        ];

        $this->actingAs($moderatorUser);

        $this->post("/threads/{$thread->id}", $update)
            ->assertStatus(200);

        $this->assertDatabaseHas('threads', array_merge($update, [
            'user_id' => $moderatorUser->id,
            'description' => $thread->description
        ]));
    }

    / @test /
    public function a_moderator_cannot_update_another_moderators_thread()
    {
        $moderatorUser = factory(User::class)->create(['role' => 'moderator']);

        $thread = factory(Thread::class)->create();

        $update = [
            'title' => 'Updated Title'
        ];

        $this->actingAs($moderatorUser);

        $this->post("/threads/{$thread->id}", $update)
            ->assertStatus(403);

        $this->assertDatabaseMissing('threads', array_merge($update, [
            'user_id' => $moderatorUser->id,
            'description' => $thread->description
        ]));
    }

    /** @test /
    public function a_former_moderator_cannot_update_his_thread()
    {
        $formerModerator = factory(User::class)->create(['role' => 'user']);

        $thread = factory(Thread::class)->create(['user_id' => $formerModerator->id]);

        $update = [
            'title' => 'Updated Title'
        ];

        $this->actingAs($formerModerator);


        $this->post("/threads/{$thread->id}", $update)
            ->assertStatus(403);


        $this->assertDatabaseMissing('threads', array_merge($update, [
            'user_id' => $formerModerator->id,
            'description' => $thread->description
        ]));
    }
}

We have three tests:

  1. a_moderator_can_update_his_thread ensures moderators can update their thread
  2. a_moderator_cannot_update_another_moderators_thread ensures a moderator cannot update another moderator's thread
  3. a_former_moderator_cannot_update_his_thread ensures normal users cannot update a thread even if the thread was created by them.

Of course, when we run these tests with vendor/bin/phpunit --filter UpdatesThreadTest, all three tests fail 😡. Let's fix that.

First, we will need to update our controller update method with logic to update a thread.


// app/Http/Controllers/ThreadsController.php

public function update(Request $request, Thread $thread)
{
     $thread->update($request->all());

     return response()->json(['thread' => $thread->fresh()], 200);
}

Run our tests again and we get just one passing. The last two fail. This is because we are yet to employ authorization.

An instance of an authenticated user has the can and cannot methods that can be used to check if a user can or cannot perform an action on a resource or model. We will make use of the can method to perform authorization based on the login contained in our policy update method.


public function update(Request $request, Thread $thread)
{
    if ($request->user()->can('update', $thread)) {

        $thread->update($request->all());

        return response()->json(['thread' => $thread->fresh()], 200);
    }

    return response()->json(['message' => 'Unauthorized Action'], 403);
}

We add a conditional that checks if a user can update a thread and now, our tests should be passing.

The can method takes the action as its first argument. The second argument will be the instance of the model we are performing the action on. There are, however, some actions that do not require a model instance like the create action for example. In that case, we will pass the name of the model class like this: can('create','App\Models\Thread').

ASIDE: Remember when I mentioned earlier that we can define gates using the ClassName@method style we use for routing, well this can be done like so:

Gate::define('update-post', 'App\Policies\ThreadPolicy@update');

I will also like to mention that we can define resource gates using the Gate::resource() method. This will map a gate to a Policy class with the view, create, update, delete actions specified. For example, update will be mapped to the ResourcePolicyClass@update.

Gate::resource('threads', 'App\Policies\ThreadPolicy');

Final Considerations

We have done a lot, but then this is like a small subset of all you can do with gates and policies. You can use them in route middlewares, in controllers and even in views.

You can learn a lot more about Laravel Authorization, gates and policies to be specific, from the Laravel official documentation here.

Conclusion

In this tutorial, we have learned what authorization is and how it differs from authentication. We have also seen how to implement basic authorization in Laravel using Policies and Gates. We have also seen a little bit about Test-Driven Development and trust me, we've also learned how to spec out a small project ... or not. 😇

Go forth and authorize. Happy coding 🙂

Discussion (0)