BreadcrumbHomeResourcesBlog Zend Framework: ACLs For Users With Multiple Roles July 5, 2018 Zend Framework: ACLs for Users With Multiple RolesPHP DevelopmentZend 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.Table of ContentsBackground on Access Control ListsDon't Forget EveryoneBut What About Bob?Introducing Mr. XTable of Contents1 - Background on Access Control Lists2 - Don't Forget Everyone3 - But What About Bob?4 - Introducing Mr. XBack to topBackground on Access Control ListsAs you may (or may not) know, the Zend\Permissions\Acl component uses three elements to define rights:RoleResourceRightsAssuming 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 topDon't Forget EveryoneSo 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 topBut 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 topIntroducing Mr. XThe 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? YESHooray, 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? YESExcellent! 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 Resources101 Guide: Developing Web Applications with PHPBlog: The Evolution of Zend Framework to the Laminas ProjectWebinar: Migrating from Zend Framework to LaminasBlog: Managing Security Risks in the PHP Engine and Web ApplicationsEnterprise Support for the Laminas PHP FrameworkLaminas Project communityBack to top