Skip to content

Zend\Permissions\Acl and Mr. X

Doug Bierer edited this page Jun 29, 2018 · 1 revision

Zend\Permissions\Acl and Mr. "X"

After covering the essentials of the Zend\Permissions\Acl component (Access Control unit, Cross Cutting Concerns module, in the new Zend Framework Advanced course) many students have approached me to ask, "what happens if a user has multiple roles?"

In this article I discuss the "traditional" way of handling a user who has multiple roles, and then lay out an easy approach which I simply call Mr. X.

Background

As you may (or may not) know, the Zend\Permissions\Acl component uses three elements to define rights:

  • Role
  • Resource
  • Rights

Assuming we use controllers as resources, and actions as rights, here is a typical ACL for a company having departments of Sales, Marketing, Support. In this case we can use the department as the role, so the ACL might look something like this:

<?php
namespace Application\Acl;

use Zend\Permissions\Acl\Acl as ZendAcl;

class Acl extends ZendAcl
{
    protected $roles = [
        'sales',
        'marketing',
        'support',
    ];
    protected $resources = [
        'index-controller',
        'mktg-controller',
        'sales-controller',
        'support-controller',
    ];
    public function __construct()
    {
        // configured roles
        foreach ($this->roles as $role) $this->addRole($role);
        // configured resources
        foreach ($this->resources as $res) $this->addResource($res);
        // basic assignments
        $this->allow('sales', 'sales-controller');
        $this->allow('support', 'support-controller');
        $this->allow('marketing', 'mktg-controller');
    }
}

Don't Forget Everyone

So far so good! Oops ... right away we have a problem: what about website visitors who we want to allow access to the home page? Also, what about login? OK, let's assume, for the sake of illustration, that the IndexController::indexAction produces the home page, and the loginAction takes care of login. One possibility would be to define a constant which represents a new role everyone, and to have all roles inherit from that. So we make these modifications to the example above:

const ROLE_EVERYONE = 'everyone';

and

// hard-coded role
$this->addRole(self::ROLE_EVERYONE);
// configured roles: all roles inherit from "everyone"
foreach ($this->roles as $role) $this->addRole($role, self::ROLE_EVERYONE);

So now our class looks like this:

<?php
namespace Application\Acl;

use Zend\Permissions\Acl\Acl as ZendAcl;

class Acl extends ZendAcl
{
    const ROLE_EVERYONE = 'everyone';
    protected $roles = [
        'sales',
        'marketing',
        'support',
    ];
    protected $resources = [
        'index-controller',
        'mktg-controller',
        'sales-controller',
        'support-controller',
    ];
    public function __construct()
    {
        // hard-coded role
        $this->addRole(self::ROLE_EVERYONE);
        // configured roles: all roles inherit from "everyone"
        foreach ($this->roles as $role) $this->addRole($role, self::ROLE_EVERYONE);
        // configured resources
        foreach ($this->resources as $res) $this->addResource($res);
        // "everyone: assignment
        $this->allow(self::ROLE_EVERYONE, 'index-controller', ['index','login']);
        // basic assignments
        $this->allow('sales', 'sales-controller');
        $this->allow('support', 'support-controller');
        $this->allow('marketing', 'mktg-controller');
    }
}

But What About Bob?

But wait ... we forgot about Josie, who is in both Sales and Marketing. And then there's Bob, who is in Support and Sales.

We could create a new role SalesMktg which inherits from both Sales and Marketing ... but then we'd have to add an if statement which checks to see if, after authentication, that user belongs to both departments. Likewise, we could add a new role SalesSupport which inherits from both Sales and Support ... but this means another if statement, and so on and so forth.

Another option would be to create a method multiCheck($roles, $resource, $right) in our ACL class which loops through all the departments, and checks to see if that role has rights or not. Maybe something like this:

public function multiCheck($roles, $resource, $right)
{
    $allowed = FALSE;
    foreach ($roles as $role) {
        if ($this->isAllowed($role, $resource, $right)) {
            $allowed = TRUE;
            break;
        }
    }
    return $allowed;
}

But that kind of ruins the simplicity of the ACL, and further, fails to take advantage of its already built-in multi-inheritance. In short, things can start to get messy very quickly in this situation.

Introducing Mr. X

The concept of Mr. X is astonishingly simple. The idea is that anybody who visits the website, no matter who they are, assumes the role of MR_X. We then check the results of Zend\Authentication\AuthenticationService::getIdentity() where (presumably) we have stored a field department (to follow this example: otherwise the field could be called groups, or even just roles). We will further assume that this field is in the form of an array, even if the user only belongs to one department.

In this scenario, we don't worry about having other roles inherit from everyone: we will automatically add it as a parent from which MR_X inherits. Here is what we add to the ACL to support MR_X:

// add "everyone" to $roles
if (!in_array(self::ROLE_EVERYONE, $roles)) $roles[] = self::ROLE_EVERYONE;
$this->addRole(self::ROLE_MR_X, $roles);

Here is how our finished ACL appears:

<?php
namespace Application\Acl;

use Zend\Permissions\Acl\Acl as ZendAcl;

class Acl extends ZendAcl
{
    const ROLE_EVERYONE = 'everyone';
    const ROLE_MR_X     = 'MR_X';
    protected $roles = [
        'sales',
        'marketing',
        'support',
    ];
    protected $resources = [
        'index-controller',
        'mktg-controller',
        'sales-controller',
        'support-controller',
    ];
    public function __construct(array $roles = [])
    {
        // hard-coded role
        $this->addRole(self::ROLE_EVERYONE);
        // configure roles
        foreach ($this->roles as $role) $this->addRole($role);
        // configured resources
        foreach ($this->resources as $res) $this->addResource($res);
        // "everyone: assignment
        $this->allow(self::ROLE_EVERYONE, 'index-controller', ['index','login']);
        // basic assignments
        $this->allow('sales', 'sales-controller');
        $this->allow('support', 'support-controller');
        $this->allow('marketing', 'mktg-controller');
        // add "everyone" to $roles
        if (!in_array(self::ROLE_EVERYONE, $roles)) $roles[] = self::ROLE_EVERYONE;
        $this->addRole(self::ROLE_MR_X, $roles);
    }
}

We're now ready to Rock N Roll! First let's slap together a little stand-alone test program:

<?php
// demonstrates the "Mr. X" technique re: Zend\Permissions\Acl

// set up infrastructure
include __DIR__ . '/vendor/autoload.php';
use Application\Acl\Acl;

function testAcl($roles, $resource, $rights)
{
    $acl = new Acl($roles);
    $output = 'Mr X has this role(s): ' . implode(',', $roles) . PHP_EOL
            . 'Is Mr X is allowed to use the '
            . $resource . ' and '
            . $rights . ' action? ';
    $output .= ($acl->isAllowed(Acl::ROLE_MR_X, $resource, $rights)) ? 'YES' : 'NO';
    return $output . PHP_EOL;
}

We can test for non-authenticated users, to see if they can hit the home page:

echo testAcl([], 'index-controller', 'index');
// output:
// Mr X has this role(s):
// Is Mr X is allowed to use the index-controller and index action? YES

Hooray, it works! But can somebody in Sales access the home page?

echo testAcl(['sales'], 'index-controller', 'index');
// output:
// Mr X has this role(s): sales
// Is Mr X is allowed to use the index-controller and index action? YES

Excellent! Now let's run some other tests:

echo testAcl(['sales'], 'mktg-controller', 'index');
echo testAcl(['sales','marketing'], 'mktg-controller', 'index');
echo testAcl(['sales','marketing'], 'sales-controller', 'index');
echo testAcl(['sales','marketing'], 'support-controller', 'index');
// output:
/*
Mr X has this role(s): sales
Is Mr X is allowed to use the mktg-controller and index action? NO
Mr X has this role(s): sales,marketing
Is Mr X is allowed to use the mktg-controller and index action? YES
Mr X has this role(s): sales,marketing
Is Mr X is allowed to use the sales-controller and index action? YES
Mr X has this role(s): sales,marketing
Is Mr X is allowed to use the support-controller and index action? NO
*/

And there you have it. From now on, anywhere in your application, all you need to do is to do an Acl::isAllowed() against MR_X and you're good to go.

Clone this wiki locally
You can’t perform that action at this time.