Image Blog Zend Framework Access Control Lists
July 5, 2018

Zend Framework: ACLs for Users With Multiple Roles

PHP Development
Zend Framework

(The following is a guest post by Doug Bierer, one of our instructors for the Zend Framework training courses)

After covering the essentials of the Zend\Permissions\Acl component (Access Control unit, Cross Cutting Concerns module, in the 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.

Back to top

Background on Access Control Lists

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 Access Control List (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:

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');
    }
}

 

Back to top

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:

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');
    }
}
Back to top

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 Access Control List, 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.

Back to top

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:

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:

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. To learn more about our Zend Framework and other training courses, visit our website.

Additional Resources

Back to top