today i would like to write something about a dynamic loaded and user specific Zend ACL (ZF version 1.11.11). This example requires a working login and access control, just like the example of Niko Klausnitzer.
If this is working, we have to extend the db with the acl entities just as in the following picture.
![]() |
acl db model |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; | |
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; | |
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL'; | |
DROP SCHEMA IF EXISTS `mydb` ; | |
CREATE SCHEMA IF NOT EXISTS `mydb` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ; | |
USE `mydb` ; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`users` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`users` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`users` ( | |
`u_id` INT NOT NULL AUTO_INCREMENT , | |
`u_username` VARCHAR(127) NOT NULL , | |
`u_password` VARCHAR(255) NOT NULL , | |
PRIMARY KEY (`u_id`) ) | |
ENGINE = InnoDB; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`roles` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`roles` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`roles` ( | |
`r_id` INT NOT NULL AUTO_INCREMENT , | |
`r_name` VARCHAR(255) NOT NULL , | |
PRIMARY KEY (`r_id`) ) | |
ENGINE = InnoDB; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`role_inheritance` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`role_inheritance` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`role_inheritance` ( | |
`role_id` INT NOT NULL , | |
`parent_role_id` INT NOT NULL , | |
PRIMARY KEY (`role_id`, `parent_role_id`) , | |
INDEX `role` (`role_id` ASC) , | |
INDEX `parent_role` (`parent_role_id` ASC) , | |
CONSTRAINT `role` | |
FOREIGN KEY (`role_id` ) | |
REFERENCES `mydb`.`roles` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE, | |
CONSTRAINT `parent_role` | |
FOREIGN KEY (`parent_role_id` ) | |
REFERENCES `mydb`.`roles` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE) | |
ENGINE = InnoDB; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`users_roles` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`users_roles` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`users_roles` ( | |
`user_id` INT NOT NULL , | |
`role_id` INT NOT NULL , | |
PRIMARY KEY (`user_id`, `role_id`) , | |
INDEX `users` (`user_id` ASC) , | |
INDEX `roles` (`role_id` ASC) , | |
CONSTRAINT `users` | |
FOREIGN KEY (`user_id` ) | |
REFERENCES `mydb`.`users` (`u_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE, | |
CONSTRAINT `roles` | |
FOREIGN KEY (`role_id` ) | |
REFERENCES `mydb`.`roles` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE) | |
ENGINE = InnoDB; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`resources` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`resources` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`resources` ( | |
`r_id` VARCHAR(127) NOT NULL , | |
`parent_resource` VARCHAR(127) NULL , | |
PRIMARY KEY (`r_id`) , | |
INDEX `r_parent` (`parent_resource` ASC) , | |
CONSTRAINT `r_parent` | |
FOREIGN KEY (`parent_resource` ) | |
REFERENCES `mydb`.`resources` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE) | |
ENGINE = InnoDB; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`role_resource_privilege` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`role_resource_privilege` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`role_resource_privilege` ( | |
`role_id` INT NOT NULL , | |
`resource_id` VARCHAR(127) NOT NULL , | |
`privilege` VARCHAR(127) NOT NULL , | |
PRIMARY KEY (`role_id`, `resource_id`, `privilege`) , | |
INDEX `p_roles` (`role_id` ASC) , | |
INDEX `p_resources` (`resource_id` ASC) , | |
CONSTRAINT `p_roles` | |
FOREIGN KEY (`role_id` ) | |
REFERENCES `mydb`.`roles` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE, | |
CONSTRAINT `p_resources` | |
FOREIGN KEY (`resource_id` ) | |
REFERENCES `mydb`.`resources` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE) | |
ENGINE = InnoDB; | |
-- ----------------------------------------------------- | |
-- Table `mydb`.`user_resource_privilege` | |
-- ----------------------------------------------------- | |
DROP TABLE IF EXISTS `mydb`.`user_resource_privilege` ; | |
CREATE TABLE IF NOT EXISTS `mydb`.`user_resource_privilege` ( | |
`user_id` INT NOT NULL , | |
`resource_id` VARCHAR(127) NOT NULL , | |
`privilege` VARCHAR(127) NOT NULL , | |
PRIMARY KEY (`user_id`, `resource_id`, `privilege`) , | |
INDEX `u_users` (`user_id` ASC) , | |
INDEX `u_resources` (`resource_id` ASC) , | |
CONSTRAINT `u_users` | |
FOREIGN KEY (`user_id` ) | |
REFERENCES `mydb`.`users` (`u_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE, | |
CONSTRAINT `u_resources` | |
FOREIGN KEY (`resource_id` ) | |
REFERENCES `mydb`.`resources` (`r_id` ) | |
ON DELETE CASCADE | |
ON UPDATE CASCADE) | |
ENGINE = InnoDB; | |
SET SQL_MODE=@OLD_SQL_MODE; | |
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; | |
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`users` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`users` (`u_id`, `u_username`, `u_password`) VALUES (1, 'admin', 'admin'); | |
INSERT INTO `mydb`.`users` (`u_id`, `u_username`, `u_password`) VALUES (2, 'tester', 'tester'); | |
COMMIT; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`roles` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`roles` (`r_id`, `r_name`) VALUES (1, 'guest'); | |
INSERT INTO `mydb`.`roles` (`r_id`, `r_name`) VALUES (2, 'tester'); | |
INSERT INTO `mydb`.`roles` (`r_id`, `r_name`) VALUES (3, 'manager'); | |
INSERT INTO `mydb`.`roles` (`r_id`, `r_name`) VALUES (4, 'admin'); | |
COMMIT; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`role_inheritance` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`role_inheritance` (`role_id`, `parent_role_id`) VALUES (2, 1); | |
INSERT INTO `mydb`.`role_inheritance` (`role_id`, `parent_role_id`) VALUES (3, 2); | |
INSERT INTO `mydb`.`role_inheritance` (`role_id`, `parent_role_id`) VALUES (4, 3); | |
COMMIT; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`users_roles` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`users_roles` (`user_id`, `role_id`) VALUES (1, 4); | |
INSERT INTO `mydb`.`users_roles` (`user_id`, `role_id`) VALUES (2, 2); | |
COMMIT; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`resources` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`resources` (`r_id`, `parent_resource`) VALUES ('default', NULL); | |
INSERT INTO `mydb`.`resources` (`r_id`, `parent_resource`) VALUES ('default::login', 'default'); | |
INSERT INTO `mydb`.`resources` (`r_id`, `parent_resource`) VALUES ('default::logout', 'default'); | |
INSERT INTO `mydb`.`resources` (`r_id`, `parent_resource`) VALUES ('special', NULL); | |
COMMIT; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`role_resource_privilege` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`role_resource_privilege` (`role_id`, `resource_id`, `privilege`) VALUES (1, 'default::login', 'ALL'); | |
INSERT INTO `mydb`.`role_resource_privilege` (`role_id`, `resource_id`, `privilege`) VALUES (2, 'default::logout', 'ALL'); | |
INSERT INTO `mydb`.`role_resource_privilege` (`role_id`, `resource_id`, `privilege`) VALUES (4, 'default', 'ALL'); | |
COMMIT; | |
-- ----------------------------------------------------- | |
-- Data for table `mydb`.`user_resource_privilege` | |
-- ----------------------------------------------------- | |
START TRANSACTION; | |
USE `mydb`; | |
INSERT INTO `mydb`.`user_resource_privilege` (`user_id`, `resource_id`, `privilege`) VALUES (1, 'special', 'SPECIAL'); | |
COMMIT; |
This ACL works with multi role and single resource inheritance. But only with ALLOW rights, if you need DISALLOW rights, you would have to add priorities. It would be possible to generate the Zend Models out of the db model, using zend db model generator. I've used it in version 0.60. The following ACL code only uses models and no sql statements!
UPDATE: Here you could download my models:
http://goo.gl/FNojG
I have minimally adjusted the generated classes to fit my needs.
If the mappers aren't running, test to rename the folder to "Mapper" or add the string "mappers" to the autoloader.
Some last hint, you have to ensure by dynamic role creation or deletion that all role dependencies are correct (e.g. no loop).
Resources within this example look like "module", "module::controller" or "module::controller::action". The privileges are special rights e.g. "ALL" or something like "yes user you may see this button". It's really a very granular ACL! It would be also possible to use the action as privilege.
(To grant all rights to a resource in the Zend ACL is to allow NULL!)
toggle the ACL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* ACL Plugin | |
* @package Plugin | |
* @author FlorianX | |
*/ | |
class Plugin_Auth_Acl extends Zend_Acl | |
{ | |
private $_roleRecursionStop = array(); | |
private $_resourceRecursionStop = array(); | |
public function __construct () | |
{ | |
$this->initPermissions(); | |
$this->initUsers(); | |
} | |
/** | |
* | |
* Recursive function to load the roles with parents | |
* | |
* @param array Application_Model_Roles $roles the array with ALL roles | |
* @param int|null $searchId that is the parent_role_id which was not available in the ACL | |
* @param boolean $loadAll needed to run first over all array values | |
*/ | |
private function _recursiveLoadRoles ($roles, $searchId, $loadAll = false) | |
{ | |
// run through the array | |
foreach ($roles as $key => $role) { | |
if(($role->getRId() == $searchId) || $loadAll){ | |
if (($parent_roles = $role->getRoleInheritanceByRole()) != null) { | |
// check whether the parents have already been created | |
$parent_role_ids = array(); | |
foreach ($parent_roles as $k => $val) { | |
$parent_role_ids[] = $val->getParentRoleId(); | |
/* | |
* avoid infinite recursion, if the acl hasn't the role and | |
* it was already searched after it | |
*/ | |
if (! $this->hasRole($val->getParentRoleId()) && in_array($val->getParentRoleId(), $this->_roleRecursionStop)) { | |
throw new Exception("Infinity loop by roles: " . implode(", ", $this->_roleRecursionStop)); | |
}elseif (! $this->hasRole($val->getParentRoleId())) { | |
$this->_roleRecursionStop[] = $val->getParentRoleId(); | |
$this->_recursiveLoadRoles($roles, $val->getParentRoleId()); // call the recursion function | |
} | |
} | |
if (! $this->hasRole($role->getRId())) | |
$this->addRole(new Zend_Acl_Role($role->getRId()), | |
$parent_role_ids); | |
unset($parent_role_ids); | |
} else { | |
// no parents | |
if (! $this->hasRole($role->getRId())) | |
$this->addRole($role->getRId()); | |
} | |
} | |
} | |
} | |
/** | |
* | |
* Init the ACL Roles | |
*/ | |
private function initRoles () | |
{ | |
// get all roles from DB | |
$fetchModel = new Application_Model_Roles(); | |
$roles = $fetchModel->fetchAll(); | |
$this->_recursiveLoadRoles($roles, null, true); // call the recursion function | |
unset($fetchModel, $roles); | |
} | |
/** | |
* | |
* Recursive function to load the resources with parents | |
* | |
* @param array Application_Model_Resources $resources the array with ALL resources | |
* @param int|null $searchId that is the parent_resource which was not available in the ACL | |
* @param boolean $loadAll needed to run first over all array values | |
*/ | |
private function _recursiveLoadResources ($resources, $searchId, $loadAll = false) | |
{ | |
// run through the array | |
foreach ($resources as $k => $resource) { | |
if (($resource->getRId() == $searchId) || $loadAll) { | |
if (($parent_resource = $resource->getParentResource()) != null) { | |
/* | |
* avoid infinite recursion, if the acl hasn't the resource and | |
* it was already searched after it | |
*/ | |
if (! $this->has($parent_resource) && in_array($parent_resource, $this->_resourceRecursionStop)) { | |
throw new Exception("Infinity loop by resources: " . implode(", ", $this->_resourceRecursionStop)); | |
}elseif (! $this->has($parent_resource)) { | |
$this->_resourceRecursionStop[] = $parent_resource; | |
$this->_recursiveLoadResources($resources, $parent_resource); // call the recursion function | |
} else { | |
if (! $this->has($resource->getRId())) | |
$this->addResource(new Zend_Acl_Resource($resource->getRId()), | |
$parent_resource); | |
} | |
} else { | |
// no parents | |
if (! $this->has($resource->getRId())) | |
$this->addResource(new Zend_Acl_Resource($resource->getRId())); | |
} | |
} | |
} | |
} | |
/** | |
* | |
* Init the ACL Resources | |
*/ | |
private function initResources () | |
{ | |
$this->initRoles(); | |
// get all resources from DB | |
$fetchModel = new Application_Model_Resources(); | |
$resources = $fetchModel->fetchAll(); | |
$this->_recursiveLoadResources($resources, null, true); // call the recursion function | |
unset($fetchModel, $resources); | |
} | |
/** | |
* | |
* starts the resource and role init process and | |
* grants the privileges | |
*/ | |
private function initPermissions () | |
{ | |
$this->initResources(); | |
$fetchModel = new Application_Model_Roles(); | |
$roles = $fetchModel->fetchAll(); | |
foreach ($roles as $key => $role){ | |
$resourcePrivileges = $role->getRoleResourcePrivilege(); | |
if($resourcePrivileges !== null){ | |
foreach ($resourcePrivileges as $k => $resourcePrivilege){ | |
if($resourcePrivilege->getPrivilege() == 'ALL'){ | |
// ALL - to grant all privileges we have to allow null | |
$this->allow($resourcePrivilege->getRoleId(), | |
$resourcePrivilege->getResourceId(), | |
null); | |
}else{ | |
$this->allow($resourcePrivilege->getRoleId(), | |
$resourcePrivilege->getResourceId(), | |
$resourcePrivilege->getPrivilege()); | |
} | |
} | |
} | |
} | |
unset($roles, $fetchModel); | |
} | |
/** | |
* | |
* Init the user specific roles with privileges | |
*/ | |
private function initUsers(){ | |
$fetchModel = new Application_Model_Users(); | |
$users = $fetchModel->fetchAll(); | |
foreach ($users as $key => $user){ | |
/* create the user role like "u<ID>" | |
* to avoid dublicates | |
*/ | |
$userRole = 'u' . $user->getUId(); | |
// every user has the GUEST role, to access the login/logout | |
$parent_roles = array(); | |
if(($roles = $user->getUsersRoles()) != null){ | |
foreach($roles as $role){ | |
$parent_roles[] = $role->getRoleId(); | |
} | |
} | |
// create a user role with the inheritance | |
$this->addRole(new Zend_Acl_Role($userRole), $parent_roles); | |
unset($roles, $parent_roles); | |
// grant the user privileges to the user role "u<ID>" | |
$resourcePrivileges = $user->getUserResourcePrivilege(); | |
if($resourcePrivileges !== null){ | |
foreach ($resourcePrivileges as $k => $resourcePrivilege){ | |
if($resourcePrivilege->getPrivilege() == 'ALL'){ | |
// ALL - to grant all privileges we have to allow null | |
$this->allow($userRole, | |
$resourcePrivilege->getResourceId(), | |
null); | |
}else{ | |
$this->allow($userRole, | |
$resourcePrivilege->getResourceId(), | |
$resourcePrivilege->getPrivilege()); | |
} | |
} | |
} | |
} | |
} | |
} |
With this example it is possible to dynamically load roles, resources and special user rights from the DB with Zend. It also avoids infinite loops by inheritance. Now you have to load the ACL in the bootstrap and implement some caching methods.
Have Fun!