Stop ACTA

All CakePHP ACL permissions for your views

Posted in CakePHP on 01.03.2009.

I've started intensifying my work on NeutrinoCMS again, and this is a sneak peek at the new ACL implementation. Or, a story of how I kicked ACL's ass.

I've been messing with ACL for a while now. All the basic things were somewhat easy to set up, but when it came to disabling or hiding the not-authorised links on pages, I got stuck. I did ask for help on the mailing list a long time ago (right here) but everyone was fairly clueless on the issue.

The reason I got stuck was the fact that there was no easy way to get all ACL permissions for a user in one go (although I see some progress has been made on that field too). After my brief visit to #cakephp on Freenode, I got some clues from gwoo. Well actually, all he said was "Acl uses the tree behavior" but that was enough. :-)

First, a brief overview of the ACL structure for Neutrino. The new release will have the following ARO structure:

* Everyone
    * root
    * Administrators
        * admin 1
    * Registered users
        * user 1
        * user 2
        * ...
        * Limited users
            * etc..

Note that "Everyone" and "root" nodes will be read only, while everything else is optional. Root is read-only administrative account i.e. the owner of the site (in the current - beta - release, it's the only user).

This is the ACO structure:

* Everything
    * Articles
    * Downloads
    * Comments
    * Users
    * etc..

Briefly, Everything contains all controllers.

Since I'm using Auth/Acl in controller mode, finding permissions for the current action is a piece of cake. However, when I need to render a bunch of links in a view, I call Houston and inform them I have a problem. How are we supposed to know all the permissions for all those links in a view?

To get all the current permissions for a user, I use several tools:

  • find('threaded')
  • getpath()
  • Set utility class

If you do a find('threaded') for your user, like this:

$perm = $this->Acl->Aro->find
    (
        'threaded',
        array
        (
            'conditions' => array
            (
                'Aro.foreign_key' => $this->_user['id']
            )
        )
    );

You will get all the permissions for current user ($this->_user is a shortcut in Neutrino and it contains all the data from $this->Auth->user()). However, you get only those permissions set explicitly for that particular user. If a user belongs to a group of AROs called "Registered users", you need their permissions too, right? This is where getpath() comes into play.

What we need to do is find out where does our user belong. And this is exactly what getpath is for. Let's say our user belongs to the group "Registered users". That means our path would be "/Everyone/Registered users/John Smith". In order to get the permissions for each one of those, we only need to loop and do a find('threaded') for each one of them.

Or to turn that load of crap into code, we do this:

// somewhere....in AppController::beforeRender()..far, far away...
if (!empty($this->_user) &&
    !empty($this->Acl) &&
    !$this->Session->read('Auth.Permissions'))
{
    $perm = $this->Acl->Aro->find
        (
            'threaded',
            array
            (
                'conditions' => array
                (
                    'Aro.foreign_key' => $this->_user['id']
                )
            )
        );

    $perms[] = $perm[0];
    $aroId = Set::extract('/Aro/id', $perms);

    $userPath = $this->Acl->Aro->getpath($aroId[0]);
    $userPath = Set::extract('/Aro[alias=/^.+$/]/alias', $userPath);
    $userPath = array_reverse($userPath);

    foreach ($userPath as $pathNode)
    {
        $perm = $this->Acl->Aro->find
            (
                'threaded',
                array
                (
                    'conditions' => array
                    (
                        'Aro.alias' => $pathNode
                    )
                )
            );

        $perms[] = $perm[0];
    }

    $this->Session->write('Auth.Permissions', $perms);
}

OK, now we have all those permissions ordered by their significance and stored in session under Auth.Permissions. All we need now is a helper with a clever way of using them.

I'm going to post a snippet of the new "AuthHelper" for clarity, as it is somewhat too big for an article. This is the important bit: we loop through the nodes in reverse order (in our example, first the user, then the "Registered users" and lastly "Everyone") and check for explicitly set permissions. If they are not set in any of those nodes - access is denied. In Neutrino, the node "Everyone" will have "Everything" denied, so it will return false before that.

foreach ($permissions as $aroNode)
{
    // check for permissions on specific controller
    $access = Set::extract
        (
            sprintf
            (
                '/Aco[alias=/%1$s/]/Permission[_%2$s!=0]/_%2$s',
                $controller,
                $action
            ),
            $aroNode
        );

    if (!empty($access))
    {
        if ($access[0] == 1)
        {
            return true;
        }
        else if ($access[0] == -1)
        {
            return false;
        }
    }

    // check for permissions on Everything
    $access = Set::extract
        (
            sprintf
            (
                '/Aco[alias=/Everything/]/Permission[_%1$s!=0]/_%1$s',
                $action
            ),
            $aroNode
        );

    if (!empty($access))
    {
        if ($access[0] == 1)
        {
            return true;
        }
        else if ($access[0] == -1)
        {
            return false;
        }
    }
} // foreach ($permissions as $aroNode)

The results of this whole search are cached, for now only for one request (still a very embryonic implementation), but I hope I'll speed up the process before releasing the next version. Currently the entire _loadAclPermissions() method lasts approximately 20ms, with less than 0.01ms per check in AuthHelper (longest I got was 0.007980). Keep in mind that the checks are cached (per page request) and the _loadAclPermissions() is executed only once in a session.

Hope that will help someone out there at least a bit (until the next Neutrino release:)). As usual, leave your comments, suggestions and criticisms...

Happy baking!

Article comments — View · Add


Page 1 of 1

Neil Crookes :: 04.03.2009 04:23:40
Nice approach, probably more robust than mine
lecterror :: 04.03.2009 05:53:22
Thanks..and we'll see.. :-)
Daniel Lorenz :: 20.11.2009 02:52:00
Is there an article, where I can read how to intergrate ACL in an own helper and using it in my views? This article only shows a little peace of code, but my tails failed. I can't find an example integration in NeutrinoCMS (beta).