Simple Access Control for CakePHP3

The newest version of CakePHP doesn’t ship with built in ACL, which means you need to write your own. Personally I think this is a smart move, having looked at the one-size-fits-all solutions for previous versions of the framework and knowing that every system has different requirements, this version has good hooks and documentation on how to add something that works for your application. I thought I’d share what worked for mine.

The application has about 50 users, it’s a small, back-office application. Users are in the users table and they can have one or more roles; the relationships between the two are in users_roles.

Do The Initial Setup

To begin with, I baked the models for the users and the roles. I introduced the linking table by adding the relationship into the \Model\Table\UsersTable::initialize() method. There are some great docs on doing this, but for this example I just needed:

        $this->belongsToMany('Roles', [
            'foreignKey' => 'user_id',
            'targetForeignKey' => 'role_id',
            'joinTable' => 'users_roles'
        ]);

Then I went ahead and baked the controllers and templates. Since I’ll be putting the names of the roles into my access control code, I disabled the ability to add and delete roles, or change their names, through the web interface. To keep those changes in step with the code that relates to them, we’ll make these changes using a database patch. A minor point, but one that might be handy if you’re using a similar approach to me.

This approach doesn’t do anything special with authentication as it uses the standard approaches for logging people in (some good examples in the CakePHP tutorials). However authorization is what controls the access to individual controllers or actions, and this is where it gets interesting.

Build The Authorization Piece

To work out which roles have access to which controller actions, CakePHP will call the authorize() method of the class that I configure. This call includes the currently logged in user, and the request object, so we can use these two pieces of information together and decide who can see what. When the user is logged in, I’m storing their record with the roles hydrated into the object. This means that we’re not hitting the database on every web request to look up what roles the user has all the time (I’d also like to use this same method at some point to work out if I should be displaying navigation to a given user, so it becomes potentially SEVERAL database hits at that point, rather than just one as it is in this example).

First, I configure the Auth component in the Controller\AppController::initalize() method by setting up something like this (you probably want the Flash component as well while you’re there):

        $this->loadComponent('Auth', [
            'authenticate' => [
                'Form' => [
                    'fields' => [
                        'username' => 'email',
                        'password' => 'password'
                    ],
                ]
            ],
            'loginAction' => [
                'controller' => 'Users',
                'action' => 'login'
            ],
            'authorize' => ['Example'],
            'unauthorizedRedirect' => '/users/login',
        ]);

With this in place, I have a login form where the user logs in with their email and password. It’s important to set the loginAction when configuring the Auth component so that CakePHP knows that unauthenticated users should be able to see that page … it’s really hard to log in if you don’t have access to the login form!

The authorize setting here means that CakePHP will call Auth\ExampleAuthorize::authorize() before allowing users access to anything. All we need our function to do is return true or false – in fact a good way to get started is to do the configuration, create the class, and get the method to return true. This lets you know that your configuration is correct and you can start working on the actual logic!

The documentation covers everything you could need but sometimes real code is easier to look at. Here’s my actual auth class:

<?php

namespace App\Auth;

use Cake\Auth\BaseAuthorize;
use Cake\Network\Request;
use App\Model\Entity\User;

class ExampleAuthorize extends BaseAuthorize
{
    public function authorize($user, Request $request)
    {
        $this->_user = $user;
        // assume false
        $authorized = false;

        // admins see everything, return immediately
        if ($this->userHasRole('admin')) {
            return true;
        }

        switch($request->params['controller']) {
            case 'Users':
                // check the action param to control for a specific controller action
                if ($request->params['action'] == 'logout') {
                    $authorized = true; // everyone can log out
                }
                break;
            case 'Money':
                // you need the finance role to see this entire controller/section
                if ($this->userHasRole('finance')) {
                    return true;
                }
            default: // by default, all logged in users have access to everything
                if (!empty($user)) {
                    $authorized = true;
                }
                break;
        }

        return $authorized;
    }

    protected function userHasRole($role) {
        if (isset($this->_user['roles']) && in_array($role, $this->_user['roles'])) {
            return true;
        }
        return false;
    }
}

There are a few things to look at here. For simple starters, look at the userHasRole helper method – this is just to let me quickly look up if this user has this role. By separating it out, the flow of the actual logic is a bit more readable – and if we ever change how roles work, it only needs to change in one place!

The main method starts by assuming that the user does NOT have access, and by storing the user in to a property (to be used by the helper method). If you’re an admin, you always have access so we can really quickly return true if that’s the case. If not, then I’ve tried to include examples of limiting access by whole controller, and by specific action (everyone should be able to log out, if only to avoid error messages when someone tries to click on “log out” after their session has expired). In this system, we want most things to be accessible to everyone so that’s the default; there are just a few specific instances where a particular role will be needed for specific sections. Notice the defensive approach. You don’t have access unless the logic finds a reason to give it to you!

Going Further

This works well for my application, particularly because users can have multiple roles and the admins themselves can manage who has what. Since we have very simple requirements, the logic is just held in code; it’s easy to follow and understand, but it means that only the developers of the system can change what each role can access, and therefore as discussed, roles are managed by database patch so that the roles in the database will match the ones the code expects. A more complex system would probably need per-role, per-action permissions stored in the database to determine who has what. This would have the advantage of being maintainable without a code change, if that’s important in your situation.

I also mentioned that I’d like to use the permissions system to check if a navigation link should be displayed. CakePHP doesn’t offer this by default but I think it’s something I’d like to add to my own application over time.

Hopefully this example serves as a basis for someone implementing ACL in CakePHP3, I found that there aren’t a lot of examples so here’s at least one that we can refer to – I had a lot of great support from the #cakephp IRC channel on freenode as well, so that’s a good place to go if you still have questions.

4 thoughts on “Simple Access Control for CakePHP3

    • Oops! I just migrated to a new server and apparently pygments wasn’t installed. Fixed, thanks for letting me know :)

Leave a Reply

Please use [code] and [/code] around any source code you wish to share.

This site uses Akismet to reduce spam. Learn how your comment data is processed.