Database driven ACL with Zend Framework
April 26th, 2008 jasoneisen Posted in Zend |
The first thing we want to do is create the following database structure.

Get the SQL.
The first thing that you will recognize here that you may not have seen before in other implementations is the module table. In this demonstration we will be mapping modules, controllers, and actions to modules, resources, and privileges within our zend controllers. So if a user wanted to go to example.com/admin/user/edit, they would be utilizing the admin module, user resource, and edit privilege. For ease of use, I have set up constraints within the database so that updates and deletes cascade through the other tables.
Next, we need to integrate this with our application by extending Zend_Acl to include our database loading method. I chose to extend it into a singleton class because it keeps us from having to figure out a place to store it, like the Zend_Registry or somesuch, and keeps our application from loading the acl configuration from our database more than once.
<?php class My_Acl extends Zend_Acl { protected static $_instance = null; private function __construct() {} private function __clone() {} protected function _initialize() { $db = Zend_Db_Table::getDefaultAdapter(); $roles = $db->fetchAll("SELECT acl_role_privilege.acl_role_id, acl_module.acl_module_name, acl_resource.acl_resource_name, acl_privilege.acl_privilege_name FROM acl_role_privilege INNER JOIN acl_privilege ON acl_role_privilege.acl_privilege_id = acl_privilege.acl_privilege_id INNER JOIN acl_resource ON acl_privilege.acl_resource_id = acl_resource.acl_resource_id INNER JOIN acl_module ON acl_resource.acl_module_id = acl_module.acl_module_id"); foreach ($roles as $role) { if (!$this->has($role['acl_module_name'].'_'.$role['acl_resource_name'])) { $this->add(new Zend_Acl_Resource($role['acl_module_name'].'_'.$role['acl_resource_name'])); } if (!$this->hasRole($role['acl_role_id'])) { $this->addRole(new Zend_Acl_Role($role['acl_role_id'])); } } $this->deny(); $this->allow(null, 'default_error'); foreach ($roles as $role) { $this->allow($role['acl_role_id'], $role['acl_module_name'].'_'.$role['acl_resource_name'], $role['acl_privilege_name']); } } public static function getInstance() { if (null === self::$_instance) { self::$_instance = new self(); self::$_instance->_initialize(); } return self::$_instance; } }
Now lets go over this. The first two functions are set to private in helping keep this a singleton class. The next, _initialize(), loads up the access list from the database and loops through all of them and creates the roles and resources. After that, we deny access to everything and then allow only the default error page, because everyone should have access to this, right? Lastly, it loops again and grants privileges on each role. This deny first, allow second method increases the security of the application.
The last function, getInstance(), is what you would call to access this class. If this class has not been called before, it will initialize itself once, and any request for the class afterwards won’t have to do it again.
Please note that this is a very basic example. Zend_Db_Table models could be used in place of the sql query, the information could be serialized and stored somewhere for less database load, but this does what I want it to by showcasing it most simply and effectively.
Next, we want to make a controller plugin to automatically force restrictions on every page.
<?php class My_Controller_Plugin_Auth extends Zend_Controller_Plugin_Abstract { public function preDispatch(Zend_Controller_Request_Abstract $request) { $userRoleId = Zend_Registry::get('userRoleId') $acl = My_Acl::getInstance(); $request = $this->getRequest(); if (!$acl->hasRole($userRoleId)) { $error = "Sorry, the requested user role '".$userRoleId."' does not exist"; } if (!$acl->has($request->getModuleName().'_'.$request->getControllerName())) { $error = "Sorry, the requested controller '".$request->getControllerName()."' does not exist as an ACL resource"; } if (!$acl->isAllowed($userRoleId, $request->getModuleName().'_'.$request->getControllerName(), $request->getActionName())) { $error = "Sorry, the page you requested does not exist or you do not have access"; } if (isset($error)) { Zend_Layout::getMvcInstance()->getView()->error = $error; $request->setControllerName('error'); $request->setActionName('error'); $request->setDispatched(false); } } }
What this class basically does is check to see if you have privileges to view the current page, and if not dispatches to the error controller/action with the error message logged to the view. One key thing to note here is the concatenation of the module and controller into the resource argument of the $acl->has() query. This is because Zend_Acl does not have a way to specify modules because it isn’t highly integrated with controllers, as it shouldn’t be. If it were, it would be doing basically the same thing, only internally.
Now you can easily build a CRUD system to manage your ACL, as well as tie it in to your navigation, which I will hopefully have time to do in later posts.
Here are a few more resources which you may find useful:
Zend_Acl and storing roles and resources in a DB
Zend_Acl & MVC Integration Component Proposal
Zend_Acl dynamic loading Component Proposal
Zend_ACL and Zend_Config - Zend Framework Forum
Feel free to let me know if you have any suggestions or have some other resources to add. Remember this is just a basic implementation to demonstrate the ability to store acl information in a database.
Tags: acl, php, security, Zend, zend framework
April 26th, 2008 at 9:11 pm
Jasone that post was excellent. TYSM! Reading this answered so many questions and opened many doors in my head.
April 29th, 2008 at 6:55 am
in predispatch method you have $request parameter. In the body of the method you use $this->getRequest().
I think it`s unnecessarilly because you already have a request object.
Thanks for sharing your approach.
May 21st, 2008 at 3:48 am
found your site on del.icio.us today and really liked it.. i bookmarked it and will be back to check it out some more later ..
June 9th, 2008 at 6:01 am
Hi,That post is great.
And the thing that freaks me out is efficiency.
how about overhead of code?If there are too many roles in database,and load them every time when every page is requested.
June 14th, 2008 at 6:50 am
Hi, great article. But how would I go about integrating it in a real app? I have tables “Users” (id, username, password), “Groups”(id, name) and “User_in_groups”(user_id, group_id), how would I connect it to your “acl_role” table?
The user being in multiple groups means he/she would need to inherit from them? Do I even need the “acl_role” table in that situation? If yes, what do I store in it?
Sorry about so many questions, I’m a little lost with the whole ACL bussines but your post got me half way there, thank you very much!
June 14th, 2008 at 11:29 am
kometo:
You could store this information serialized and update it whenever it’s changed and use that.
See: http://framework.zend.com/manual/en/zend.acl.advanced.html#zend.acl.advanced.storing
Dado:
Groups would be your role table in your case. You would just need to set up the module/resource/privilege part.
This example does not try to tackle inheritance or assertions. I promise I will tackle this in an upcoming post (using Doctrine now, though) at some point.
July 3rd, 2008 at 9:51 am
Hi , Thanks for the post, i am very intersted about this Technical .
Another way of optimization is to load in the ACL only the information about the current user logined and not all roles and access rights .
for example in the _initialize function you can pass in the parameter the id of the role
protected function _initialize( userRoleId ) ;
* ( sory for my bad english too ).
July 3rd, 2008 at 7:07 pm
After converting to doctrine (I will post the code here soon) and running nearly the same query, I did some basic benchmarks with the profiler.
Time Connecting: .00348
Query Execution: .00329
Total Time: .00677
This is for retrieving 201 records. The time just connecting to the db takes more than half of the entire time spent, and even still that is massively small. 6.7ms?
As a side note, I don’t agree with pulling ACL only related to the current user role. Using inner joins, entire modules and controllers could be missing in the ACL and may need querying for other reasons. Also, that’s n more queries your db has to cache rather than just a single one that works for all roles.
July 20th, 2008 at 5:37 pm
found your site on del.icio.us today and really liked it.. i bookmarked it and will be back to check it out some more later ..
July 29th, 2008 at 9:38 pm
Great finaly a uptodate howto on ZF acl!
I’m missing something obvious. As every example out there, yours doesn’t at all address as to where insert the code you hand us.
What extending a class is i understand very well, but i wouldn’t know for god where to put the extended file, and where to include or require it!
Please address this as i’ve seen the question asked to many times and none answered yet. Thank you!
August 12th, 2008 at 9:11 am
Barryke,
Notice the class names. They would go in your library “My”. So for example, the last class in the post would go in
…library/My/Controller/Plugin/Auth.php
You simply tie this into your application by adding the plugin to your front controller in the bootstrap. The rest is automatic, provided you are using the zend autoloader.
August 28th, 2008 at 4:15 am
Hi!
I’m new to the Zend_Acl, so can you post and example of filled database tables in order to understand better how it should work?
Thanks.
August 29th, 2008 at 5:44 pm
Two things, first you don’t need to pass a request object as a parameter and get the request inside the preDispatch method
Also, you need to change the preDispatch method to dispatchLoopStartup to have the forwarding work properly.
i.e.
$request->setModuleName(’default’)
->setControllerName(’error’)
->setActionName(’index’)
->setDispatched(false);
Does nothing in a preDispatch, but works properly in dispatchLoopStartup
August 29th, 2008 at 5:47 pm
Also, if your resource isn’t in the ACL and you call isAllowed anyways, you get an Exception.
The fix is
if (!isset($error) && !$acl->isAllowed($session->roleid, $request->getModuleName().’_’.$request->getControllerName(), $request->getActionName())) {
August 30th, 2008 at 3:08 pm
Hi,
woww, very cool!
btw. what software did you use to create the database structure?
Thanks
September 1st, 2008 at 2:53 pm
Hey, guys!
Can anybody post and example of populated database? I’m getting only exceptions like “‘Zend_Acl_Exception’ with message ‘Resource ‘default_index’ not found’”.
And for sure it is because I didn’t fill the DB correctly.
Here are my sqldumb:
– ——————————————————–
–
– Table structure for table `acl_module`
–
CREATE TABLE `acl_module` (
`acl_module_id` tinyint(3) unsigned NOT NULL auto_increment COMMENT ‘Module ID’,
`acl_module_name` varchar(32) NOT NULL COMMENT ‘Module Name’,
PRIMARY KEY (`acl_module_id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT=’ACL Modules’ AUTO_INCREMENT=3 ;
–
– Dumping data for table `acl_module`
–
INSERT INTO `acl_module` VALUES (1, ‘default’);
INSERT INTO `acl_module` VALUES (2, ‘index’);
– ——————————————————–
–
– Table structure for table `acl_privilege`
–
CREATE TABLE `acl_privilege` (
`acl_privilege_id` smallint(5) unsigned NOT NULL auto_increment COMMENT ‘Privilege ID’,
`acl_resource_id` tinyint(3) unsigned NOT NULL COMMENT ‘Resource ID’,
`acl_privilege_name` varchar(32) NOT NULL COMMENT ‘Privilege Name’,
PRIMARY KEY (`acl_privilege_id`),
UNIQUE KEY `acl_resource_id_2` (`acl_resource_id`,`acl_privilege_name`),
KEY `acl_resource_id` (`acl_resource_id`),
KEY `acl_privilege_name` (`acl_privilege_name`)
) ENGINE=MyISAM AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT=’ACL Privileges’ AUTO_INCREMENT=14 ;
–
– Dumping data for table `acl_privilege`
–
INSERT INTO `acl_privilege` VALUES (1, 1, ‘index’);
INSERT INTO `acl_privilege` VALUES (2, 1, ‘add’);
INSERT INTO `acl_privilege` VALUES (3, 1, ‘delete’);
INSERT INTO `acl_privilege` VALUES (4, 1, ‘edit’);
INSERT INTO `acl_privilege` VALUES (5, 2, ‘index’);
INSERT INTO `acl_privilege` VALUES (6, 2, ‘add’);
INSERT INTO `acl_privilege` VALUES (7, 2, ‘delete’);
INSERT INTO `acl_privilege` VALUES (8, 2, ‘edit’);
INSERT INTO `acl_privilege` VALUES (9, 3, ‘index’);
INSERT INTO `acl_privilege` VALUES (10, 3, ‘add’);
INSERT INTO `acl_privilege` VALUES (11, 3, ‘delete’);
INSERT INTO `acl_privilege` VALUES (12, 3, ‘edit’);
INSERT INTO `acl_privilege` VALUES (13, 7, ‘index’);
– ——————————————————–
–
– Table structure for table `acl_resource`
–
CREATE TABLE `acl_resource` (
`acl_resource_id` tinyint(3) unsigned NOT NULL auto_increment COMMENT ‘Resource ID’,
`acl_module_id` tinyint(3) unsigned NOT NULL COMMENT ‘Module ID’,
`acl_resource_name` varchar(32) NOT NULL COMMENT ‘Resource Name’,
PRIMARY KEY (`acl_resource_id`),
KEY `acl_module_id` (`acl_module_id`)
) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT=’ACL Resources’ AUTO_INCREMENT=8 ;
–
– Dumping data for table `acl_resource`
–
INSERT INTO `acl_resource` VALUES (1, 1, ‘game’);
INSERT INTO `acl_resource` VALUES (2, 1, ‘user’);
INSERT INTO `acl_resource` VALUES (3, 1, ’stat’);
INSERT INTO `acl_resource` VALUES (4, 1, ‘auth’);
INSERT INTO `acl_resource` VALUES (5, 1, ‘index’);
INSERT INTO `acl_resource` VALUES (6, 1, ‘error’);
INSERT INTO `acl_resource` VALUES (7, 1, ‘uStats’);
– ——————————————————–
–
– Table structure for table `acl_role`
–
CREATE TABLE `acl_role` (
`acl_role_id` tinyint(3) unsigned NOT NULL auto_increment COMMENT ‘ACL Role ID’,
`acl_role_name` varchar(32) collate utf8_unicode_ci NOT NULL COMMENT ‘Role Name’,
PRIMARY KEY (`acl_role_id`),
KEY `acl_role_name` (`acl_role_name`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT=’ACL Roles’ AUTO_INCREMENT=5 ;
–
– Dumping data for table `acl_role`
–
INSERT INTO `acl_role` VALUES (1, ‘Admin’);
INSERT INTO `acl_role` VALUES (2, ‘User’);
INSERT INTO `acl_role` VALUES (3, ‘Manager’);
INSERT INTO `acl_role` VALUES (4, ‘Visitor’);
– ——————————————————–
–
– Table structure for table `acl_role_privilege`
–
CREATE TABLE `acl_role_privilege` (
`acl_role_id` tinyint(3) unsigned NOT NULL COMMENT ‘Role ID’,
`acl_privilege_id` smallint(5) unsigned NOT NULL COMMENT ‘Privilege ID’,
PRIMARY KEY (`acl_role_id`,`acl_privilege_id`),
KEY `acl_role_id` (`acl_role_id`),
KEY `acl_privilege_id` (`acl_privilege_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT=’ACL Role Privileges’;
–
– Dumping data for table `acl_role_privilege`
–
INSERT INTO `acl_role_privilege` VALUES (1, 1);
INSERT INTO `acl_role_privilege` VALUES (1, 2);
INSERT INTO `acl_role_privilege` VALUES (1, 3);
INSERT INTO `acl_role_privilege` VALUES (1, 4);
INSERT INTO `acl_role_privilege` VALUES (1, 5);
INSERT INTO `acl_role_privilege` VALUES (1, 6);
INSERT INTO `acl_role_privilege` VALUES (1, 7);
INSERT INTO `acl_role_privilege` VALUES (1, 8);
INSERT INTO `acl_role_privilege` VALUES (1, 9);
INSERT INTO `acl_role_privilege` VALUES (1, 10);
INSERT INTO `acl_role_privilege` VALUES (1, 11);
INSERT INTO `acl_role_privilege` VALUES (1, 12);
INSERT INTO `acl_role_privilege` VALUES (1, 13);
INSERT INTO `acl_role_privilege` VALUES (2, 1);
INSERT INTO `acl_role_privilege` VALUES (2, 2);
INSERT INTO `acl_role_privilege` VALUES (2, 3);
INSERT INTO `acl_role_privilege` VALUES (2, 4);
INSERT INTO `acl_role_privilege` VALUES (2, 9);
INSERT INTO `acl_role_privilege` VALUES (2, 10);
INSERT INTO `acl_role_privilege` VALUES (2, 11);
INSERT INTO `acl_role_privilege` VALUES (2, 12);
INSERT INTO `acl_role_privilege` VALUES (2, 13);
INSERT INTO `acl_role_privilege` VALUES (3, 13);
INSERT INTO `acl_role_privilege` VALUES (4, 1);
INSERT INTO `acl_role_privilege` VALUES (4, 5);
INSERT INTO `acl_role_privilege` VALUES (4, 9);
INSERT INTO `acl_role_privilege` VALUES (4, 13);
* end sql **************************************************
Thanks in advance.
September 8th, 2008 at 10:02 am
Hi jasoneisen,
that is a nice implementation and was a good inspiration for me.
But one thing is not clear for me. So when i have an admin module where the role ‘admin’ should have access to all resources and privileges in the ‘admin’-module, i have to insert every privilege and resource for ‘admin’ separately.
I think it is better to change the dependencies to
acl_role — acl_role_module — acl_module — acl_resource — acl_privilege.
How do you think about it?
September 13th, 2008 at 7:47 am
Having the same problem as KullDox. Could anyone post the database table structure ?
September 22nd, 2008 at 8:44 pm
favorited this one, brother
September 28th, 2008 at 8:08 am
i am gonna show this to my friend, dude
October 27th, 2008 at 12:02 pm
Is there any update to this?
There seems to be some suggestioins or questions such as the one by elmo on Sept 8th as to allow full controller action to cerain modules such as admin.
Mainly, I am looking for a direction on how to manage controllers, roles and priv.
Priv are technically actions/methods, correct? If i add a new priv, I need to add it to the entire acl_priv table for all roles for the controller it uses, correct?
Sounds like I should develop a dynamic controller reader to list all methods/privs within a controller and then allow choices as to who gets what.
And on June 14th you state that to use something like this when one person is going to have multiple roles, they only need to setup the ‘module/resource/privilege part.’
Then how does the acl know which role is allowed to do what?
sorry for all the questions, but want to make sure I am understanding this fully
Thanks.
October 28th, 2008 at 3:59 pm
I too am getting the same errors as : KullDox and Chris, are there any suggestions as to what we are doing wrong?
Thanks.
October 31st, 2008 at 10:56 am
this seems to have something to do with the line:
$this->allow(null, ‘default_error’);
and no value for the user who is allowed to access this. If I add a user_role_id to it, it works fine.