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.

Database Layout

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: , , , ,

23 Responses to “Database driven ACL with Zend Framework”

  1. ddelrio1986 Says:

    Jasone that post was excellent. TYSM! Reading this answered so many questions and opened many doors in my head. :)

  2. 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.

  3. 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 ..

  4. 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.

  5. 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! :)

  6. 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.

  7. 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 ).

  8. 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.

  9. 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 ..

  10. 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!

  11. 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.

  12. 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.

  13. 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

  14. 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())) {

  15. Hi,
    woww, very cool!

    btw. what software did you use to create the database structure?

    Thanks

  16. 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.

  17. 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?

  18. Having the same problem as KullDox. Could anyone post the database table structure ?

  19. favorited this one, brother

  20. i am gonna show this to my friend, dude

  21. 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.

  22. I too am getting the same errors as : KullDox and Chris, are there any suggestions as to what we are doing wrong?

    Thanks.

  23. 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.

Leave a Reply