Sep 9, 2015 | acl, laravel, laravel 5.1, authorization

ACL (Access Control List) Authorization in Laravel 5.1

Series

This is a series of posts on New Features in Laravel 5.1.

!
Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.

The authentication that Laravel provides out-of-the-box makes it simple to get user signup, login, logout, and password resets up and running quickly and easily.

But if you needed to control access to certain sections of the site, or turn on or off particular pieces of a page for non-admins, or ensure someone can only edit their own contacts, you needed to bring in a tool like BeatSwitch Lock or hand-roll the functionality, which would be something called ACL: Access Control Lists, or basically the ability to define someone's ability to do and see certain things based on attributes of their user record.

Thankfully, Taylor and Adam Wathan wrote an ACL layer in Laravel 5.1.11 that provides this functionality without any added work.

What can it do?

The out-of-the-box Laravel ACL is called Gate (that's not a product name like "Spark", but rather the name of the classes and the façades).

Using the Gate classes (either injecting them or using the Gate façade) allows you to easily check if a user (either the currently-logged-in user or a specific user) is "allowed" to do a certain thing. Check out this syntax for a taste:

if (Gate::denies('update-contact', $contact)) {
    abort(403);
}

Drop that into your controller and it checks the currently authenticated user against a ruleset that you defined (and which you named update-contact); it takes the data of that particular contact, checks it agains the ruleset, and returns whether or not the user is authorized.

You can also check for Gate::allows, you can use it in conditionals in Blade with @can, and there's much, much more. So, let's take a look.

How does it work?

Everything with Laravel's ACL is founded on a concept called an "Ability." An Ability is a key (e.g. "update-contact") and a Closure (with optional parameters) that returns either true or false.

Defining Abilities

Let's define an Ability in the default location, the AuthServiceProvider:

...
class AuthServiceProvider extends ServiceProvider
{
    public function boot(GateContract $gate)
    {
        parent::registerPolicies($gate);

        $gate->define('update-contact', function ($user, $contact) {
            return $user->id === $contact->user_id;
        });
    }
}

As you can see, the first parameter for our Closure is the user. If there's no currently-authenticated user (and if you haven't specified one--we'll see that later), Gate will automatically return false for every Ability.

Class methods instead of Closures

Just like most other places in Laravel that accept Closures (e.g. route definition), you can pass a class name and method into the second parameter of define instead of a Closure, and it'll be resolved out of the Container:

$gate->define('update-post', 'PostACLCheckerThingie@update');

Checking with Façades

Gate allows you to check using the following methods: check, allows, or denies. Note that check is just the same as allows, and denies is exactly the opposite of allows; so it's really just allows with a clone named check and an opposite check named denies.

If you're using the façade, you won't need to pass in the user; the façade automatically passes in the currently authenticated user for you.

if (Gate::denies('update-contact', $contact)) {
    abort(403);
}
if (Gate::allows('create-contact')) {
    redirect('hooray');
}

Or, if you've defined an Ability with multiple parameters:

$gate->define('delete-interaction', function ($user, $contact, $interaction) {
    // Do stuff...
});

Just pass an array to the second parameter:

if (Gate::allows('delete-interaction', [$contact, $interaction]) {
    // Do stuff...
});

What if you want to check this ability for a specific user, instead of the currently authenticated user?

if (Gate::forUser($user)->denies('update-contact', $contact)) {
    abort(403);
}

Checking with Injected Gate

As always, you can inject the class itself instead of using the façade. The class you'll inject is the same GateContract that's injected into the AuthServiceProvider: Illuminate\Contracts\Auth\Access\Gate.

    public function somethingResolvedFromContainer(Gate $gate)
    {
        if ($gate->denies('create-team')) {
            // etc.
        }
    }

Checking on the user model

Laravel's App\User model now provides can and cannot, which mirror allows and denies on the Gate. This comes from the Authorizable trait.

So, if we have a user somewhere, we can check can() on them:

if ($user->can('update-contact', $contact)) {
    // Do stuff
}

Blade

You can also use can (optionally with else) in Blade:

<nav>
    <a href="/">Home</a>
    @can('edit-contact', $contact)
        <a href="{{ route('contacts.edit', [$contact->id]) }}">Edit This Contact</a>
    @endcan
</nav>

Intercepting checks

What if you have the idea of a superuser, or admin? Or what if you want to be able to set a temporary toggle to change the ACL logic for your users?

The before function allows you to return early, before all of your other checks, in certain exceptional circumstances.

$gate->before(function ($user, $ability) {
    if ($user->last_name === 'Stauffer') {
        return true;
    }
});

Or, more realistically:

$gate->before(function ($user, $ability) {
    if ($user->isOwner()) {
        return true;
    }
});

Policies

There's another concept that you can (optionally) use to define access logic in your applications. It's an organizational structure that'll help keep you from crudding up the AuthServiceProvider; it's almost like a controller, in that it helps you group your ACL logic based on the resource that it's controlling access to.

Generating Policies

You can generate a policy with Artisan:

php artisan make:policy ContactPolicy

Then you register it with the AuthServiceProvider in the policies property:

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Contact::class => ContactPolicy::class,
    ];

Here's what our auto-generated Policy looks like:

<?php

namespace App\Policies;

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

So, let's define the update method:

<?php

namespace App\Policies;

class ContactPolicy
{
    public function update($user, $contact)
    {
        return $user->id === $contact->user_id;
    }
}

Note: I've primarily been using the update method as an example here. There are a few situations (see below) where the method name matters, because it needs to sync with the calling method. But you'll like have a variety of method names: show, create, or even addInteraction.

Checking Policies

If there's a policy defined for a resource type, the Gate will use the first parameter key to figure out which method to check on your policy.

So, to check if you can update a Contact, just pass the contact in and check for the update Ability. This code will pass to the update method on the ContactPolicy:

if (Gate::denies('update', $contact)) {
    abort(403);
}

This also works for the User model checking and Blade checking.

Additionally, there's a policy helper that allows you to retrieve a policy class and run its methods:

if (policy($contact)->update($user, $contact)) {
    // Do stuff
}

Controller authorization

Since much of the authorization will be quitting out of a controller method if the Ability is denied, there's a shortcut for that when in a Controller (which is added via the new AuthorizesRequests trait):

    public function update($id)
    {
        $contact = Contact::findOrFail($id);

        $this->authorize('update', $contact);

        // Do stuff...
    }

Just like in our examples above, this will throw a 403 error if the authorization fails.

And finally, if your controller method name lines up with the same method name on the Policy (e.g. the update controller method and the update method on the Policy), you can skip the first parameter of authorize entirely:

    public function update($id)
    {
        $contact = Contact::findOrFail($id);

        $this->authorize($contact);

        // Do stuff...
    }

This is one of my favorites; you might not love the magic, but since this is something I do so often, I'm really excited to trim down the amount of my controller methods that are dedicated to the same ACL logic, over and over.

Concliption

That's it. As someone who has written ACLs dozens of times, I can say: This is better than anything I've built, simpler than anything I've imported from others, and does everything I need.

If you need anything like Roles, user groups, or database-defined permissions levels, you'll still need to do some of the work yourself--and you may still find yourself reaching for an external package. But for most circumstances, this is more than enough, and just as simple as it can be.


Comments? I'm @stauffermatt on Twitter


Tags: acl  •  laravel  •  laravel 5.1  •  authorization


This is part of a series of posts on New Features in Laravel 5.1:

  1. Jun 9, 2015 | laravel, laravel 5.1
  2. Jun 15, 2015 | laravel, laravel 5.1, testing, integration testing
  3. Jun 16, 2015 | laravel, laravel 5.1, testing, integration testing
  4. Jul 31, 2015 | laravel, laravel 5.1
  5. Sep 9, 2015 | acl, laravel, laravel 5.1, authorization

Subscribe

For quick links to fresh content, and for more thoughts that don't make it to the blog.