Zend_Acl part 3: creating and storing dynamic ACLs

February 18, 2009 – 12:15 pm Tags: , ,

In this third post of the series, I’ll talk about using dynamic ACLs: How to store an ACL in a database, and construct it from there when needed. This post builds on the things introduced in part 1 and part 2.

We will first look at a simple example with users and pages, and then we’ll have a more complex example, involving building a much more complex ACL with inheritance, role types and other stuff.

The idea behind dynamic ACLs

As we have previously looked at ACLs which are hardcoded, we will now look at building a “dynamic” ACL. Previously shown “static” ACLs are good for quick and simple sites, but when you actually require the ability for administrators to define access rights on the fly using an admin panel, they quickly lose their usefulness.

Also, when working with very large amounts of users or roles, it may not be a very good idea to always construct the full ACL. This is one of the things we’ll discuss in this part: Creating ACLs with only the parts that are required to perform a specific access check.

Let’s say you have a user, and you have to check if the user has access to a specific page. Would it make sense to load the complete ACL, with all your users, all your pages and then set up all their access rights? Nope, not really. It’s a better idea to simply create an ACL with just the single user, and the single resource we need to check against.

A simple example with users and pages

Let’s first start with a relatively simple case: We have a site with some pages, and we need to be able to define which users can access which pages. By default, nobody can access a page, unless we specifically allow it. Later in this post, we’ll look at a more complex set up.


To the right you can see a database table schema for this example. As you can see, it’s relatively simple with just three tables.

  1. Pages table holds our site’s pages
  2. Users table holds our site’s users
  3. Page_privileges table holds data on which user has been granted access to which page

In this case, it makes sense to store ACL roles and resources as numbers: Each user is a role, and each page is a resource, thus we can easily store them in the ACL as their respective ID’s from the database.

An ACL factory

Before going any further, let’s introduce a concept we’ll be using throughout this post – an ACL factory class. The factory will be used to construct our ACL instances. This will allow us to keep better track of the code that is creating the ACL, and keep everything related to its initialization in a single place. By doing this, we can also specify things like table names or such in later on the post.

This is the basic factory we’ll be using now:

class AclFactory {
  /**
   * Creates an ACL for a specific page
   * @param Page $page
   * @return Zend_Acl
   */
  public function createAcl(Page $page) {
    //Lets assume we have a model for the page_privileges with a method like this
    //which would return PagePrivilege objects with the page_id passed as the param.
    $privileges = PagePrivilege::findByPageId($page->getId());
 
    $acl = new Zend_Acl();
    $acl->add(new Zend_Acl_Resource($page->getId()));
 
    foreach($privileges as $privilege) {
      $acl->addRole(new Zend_Acl_Role($privilege->getUserId()));
      $acl->allow($privilege->getUserId(), $page->getId());
    }
 
    return $acl;
  }
}

The way the method works is it first gets all privileges for the page – every row with the page’s ID as the page_id. We then create a new Zend_Acl instance, and add a resource with the page’s ID. Then it’s simply a loop, which adds each user_id found from the table to the ACL as a role, and marks the page as allowed for the user.

But how does this work? Because we are using the user IDs as the role IDs, we can loop over each found privilege, and add the user id in it to the ACL. This way, the ACL will have all possible roles for this resource in it. Similarily, since the privilege means the user has access to the page, we also allow the user to access the page.

Using the ACL factory

Now that we have the code which can build an ACL for us, let’s see how it could be used:

//Lets say $page is a page we want to access
$factory = new AclFactory();
$acl = $factory->createAcl($page);
 
//and $user is the currently logged in user
if($acl->hasRole($user->getId()) && $acl->isAllowed($user->getId(), $page->getId()) {
  //Access OK
}

So we use the factory to build an ACL, and then we use it as normal. This time we also use the hasRole method to test if the ACL actually even has the user’s role – since we are only adding the roles that actually have access to the page, the user may not be in the ACL, and if the role does not exist when you call isAllowed, you will get an exception.

Complex example: A file directory

Now that we’ve covered the basics, let’s dive in a bit deeper: How to implement an ACL system for a system which can be used to serve files to users.

The file system’s access will be defined like this:

  • It can have multiple categories, which can contain one or more files
  • Access to categories or files will be inherited from the parent(s)
  • Users can belong to one or more groups
  • A user or a group can have access defined to specific files or categories

So in terms of Zend_Acl, we can talk of categories and files as resources. Files will inherit permissions from the category they belong to, and categories will inherit from any parent categories they may have.

Since we won’t go into much details about the database and all that, you can take a look at the database diagram presented on the right. Full source code for a ZF file system application based on this is also available towards the end of this post, which contains SQL for creating the tables, and all the code required.

Initial set up

We’ll assume few things on the system: we will have a predefined group in the database: guests

A user who isn’t logged in will be assigned an identity which belongs only to the group guests. Thus we can limit access to guests using the ACL if we want.

We’ll also have a model class representing each of the tables with getters and setters, in a similar fashion as the NewsPost example model shown in the practical uses for reflection post. In addition, we will have some classes which can be used to fetch instances of the models from the database, or store them.

Starting out

Most of the big things will be going on in the ACL factory class. We will modify the one introduced earlier to suit this more complex example. It will have two public methods: createFileAcl and createCategoryAcl. We’ll take a look at the latter first:

public function createCategoryAcl(Category $category, User $user) {
  $acl = new Zend_Acl();
  $this->_createRoles($acl, $user);
 
  $categories = $category->getParents() + array($category);
 
  //Top category has no parent, so it's null at first
  $parent = null;
  foreach($categories as $c) {
    $acl->add($c, $parent);
    //next will have this as the parent
    $parent = $c;
  }
 
  $privileges = Privilege::findByCategories($categories);
  $this->_setPrivileges($acl, $privileges);
  return $acl;
}

So basically the method takes a Category object and a User object as its parameters. We will look at the _createRoles and _setPrivileges methods soon.

The Category class implements the Zend_Acl_Resource_Interface as shown previously, so the code in the factory can simply pass the objects to add() when creating the ACL.

This also shows how resource inheritance can be done: We simply take all the parents of the category and make them inherit from each other by passing them as arguments to add.

The getParents method will return all parents of a Category, as the farthest parent at 0, and closer at 1, 2 and so on. As the top level category will not have a parent, we first make it null. As we process the array, we change the parent category to the processed one, so the next category gets it as its parent.

The Privilege class’ method findByCategories will return all privilege objects for certain categories. Refer to the database diagram to see what it represents.

Private methods

In the category acl method, the code calls _createRoles. The job of this function is to create the role entries for the user object in question.

private function _createRoles(Zend_Acl $acl, User $user) {
  //add groups first so user can inherit them
  foreach($user->getGroups() as $group) {
    $acl->addRole($group);
  }
 
  $acl->addRole($user, $user->getGroups());
}

So by calling this method, the ACL gets filled with all the user’s roles. Similar to the Category class, the User and Group classes implements Zend_Acl_Role_Interface, so we can again just pass them to addRole without having to worry about it. You can later take a look at the code at the end of the post for a better idea of these classes.

The _setPrivileges method has a bit more code: its job is to look at the Privilege objects, and see if it needs to allow or deny access based on them

private function _setPrivileges($acl, $privileges) {
  foreach($privileges as $privilege) {
    $role = $privilege->getRole();
    $resource = $privilege->getResource();
    //the user's roles should already exist so we can
    //ignore the ones that don't
    if(!$acl->hasRole($role)) {
      continue;
    }
 
    if($privilege->getMode() == 'allow')
      $acl->allow($role, $resource);
    else
      $acl->deny($role, $resource);
  }
}

The Privilege class has methods for getting the relevant role and resource classes. Depending on the type, the role returned could be a User object or a Group object, and the resource could be a Category object or a File object. Since they all implement the interfaces for their types, they can be passed to Zend_Acl right away without having to do any additional logic.

ACLs for files

Now for the other public method in the class.

public function createFileAcl(File $file, User $user) {
  $acl = null;
  if($file->getCategoryId() != null) {
    //if the file belongs to a category
    //we need to build the whole category based acl
    $category = $file->getCategory();
    $acl = $this->createCategoryAcl($category, $user);
    $acl->add($file, $category);
  }
  else {
    $acl = new Zend_Acl();
    $this->_createRoles($acl, $user);
    $acl->add($file);
  }
 
  $privileges = Privilege::findByFile($file);
  $this->_setPrivileges($acl, $privileges);
  return $acl;
}

Since a File can belong to a category, we need to build the category ACL first. This can be easily done by calling the other method. In the case the File doesn’t belong to any category, it’s assumed to be on the top level, and it won’t inherit from anything. The file itself may also have some privileges of its own, so we fetch them and call the _setPrivileges method to process them.

Final bunch of code

We have one more ACL related class to look at: The plugin, or how to use the ACL factory in this case. This one is a bit longer, but it should be easy to understand as it’s pretty typical stuff:

<?php
class App_Plugin_AccessCheck extends Zend_Controller_Plugin_Abstract {
  public function preDispatch(Zend_Controller_Request_Abstract $request) {
    if(!$this->_accessValid($request)) {
      //we throw an exception because the error controller
      //can easily handle these
      throw new App_Exception_AccessDenied('Access denied');
    }
  }
 
  private function _accessValid(Zend_Controller_Request_Abstract $request) {
    $user = $request->getParam('user', null);
 
    //the identityloader plugin should have added a user to
    //the request and if it doesn't exist something is wrong
    if($user === null)
      return false;
 
    $params = $request->getParams();
    $factory = new App_AclFactory();
    $resource = null;
 
    if(isset($params['file'])) {
      $resource = File::findById($params['file']);
      $acl = $factory->createFileAcl($resource, $user);
    }
    elseif(isset($params['category'])) {
      $resource = Category::findById($params['category']);
      $acl = $factory->createCategoryAcl($resource, $user);
    }
    else {
      //since we only care about access to files/categories,
      //we'll return true if those aren't being accessed
      return true;
    }
 
    return $acl->hasRole($user) && $acl->has($resource)
      && $acl->isAllowed($user, $resource);
  }
}

This is a plugin which will use the ACL to test access. As we mentioned earlier, by default users who haven’t logged in will automatically belong to a guests-group, so we have some other code that loads a user object to the request. We take this parameter in the accessValid method and test it against the ACL.

It also looks at some other parameters of the request to determine if the user is trying to access a file or a category, and acts accordingly.

Summing up

So this time we had a bit more complex usage of Zend_Acl. I didn’t write all about the models used, the SQL and all that to keep it at least on a some kind of manageable level for you, but fear not – all the code that I did not show is downloadable!

Click here to download the filesystem project

I have commented certain parts of the code, but I must warn you that the code is probably quite far from optimal. It does not always use safe SQL, it does lots of extra queries in certain cases, it probably does not take care of possible erroneus conditions very well… but it does what it’s supposed to: it demonstrates how you can implement a more complex database-backed dynamic ACL system!… Oh, and it doesn’t have an admin page, so you’ll have to insert all the test data to the DB yourself

I don’t have any more chapters to this series planned out, but if you have any questions, or if some part of this wasn’t particularily clear, feel free to have your say in the comments! I might write a followup post to answer your questions if there are some more complex ones on anyones mind.

You may have noticed I didn’t cover Zend_Acl_Assertions. That’s because I couldn’t think of any use for them. =)

If you liked this series, remember to subscribe to the RSS feed to get new posts automatically to your feed reader.

Further reading:
In addition to this series on Zend_Acl, I’ve written the following posts which look a bit more on the theory side of access control and authentication:
Implementing swappable authentication methods, which explains how you could make your authentication method easily changeable between, for example, LDAP and a database.
Extensible authentication and access-control, which talks more about creating a generic auth/acl system which can be easily extended so it works with different systems like database or LDAP

Share this:
  1. 27 Responses to “Zend_Acl part 3: creating and storing dynamic ACLs”

  2. Simple scenario for Zend_Acl_Assertions:
    User1 writes a comment
    User2 writes a comment

    User1 can edit his own comment but not the comment of User2

    But nice tutorial, I think the parent child model to store the roles is not the best way. Should be implement with Nested Sets for example, because of recursion.

    By Nils on Feb 18, 2009

  3. Indeed, a nested set would be better in terms of being able to fetch the nodes better. However, it’s much more complex to insert nodes into a nested set, and just more difficult to understand in general, so I didn’t want to make the database more complex than it has to be for the example.

    By Jani Hartikainen on Feb 18, 2009

  4. Nice post, i have been founded something like that but using assertions to check which user assigned to which privilege , you can find it at http://devzone.zend.com/article/1665-Zend_Acl-Zend_Auth-Example-Scenario

    and look for the last 2 comments

    thank you!

    By islam elnaggar on Feb 18, 2009

  5. Thank you for this 3 parts about Zend_Acl.
    Not sure I got it all figured out for the third part because I’m pretty new to Zend Framework, but those are great tutorials!

    Regards,

    Naunaud.

    By Naunaud on Feb 19, 2009

  6. Great tutorial, thanks

    By dragon on Feb 20, 2009

  7. Very nice article, but just to clarify something, in the last example:
    - role_type would contain “user” or “group”?
    - ressource_type would contain “category” or “file”?
    They are used in the “::findByCategories” and “::findByFile” methods of the Privilege class?

    And I gather you have to code a specific ::createRessourceTypeAcl method for each ressource type in the Acl_Factory?

    By Cadrach on Mar 6, 2009

  8. Forget my previous comment, I dug in the source code and found my answers :)

    Again a very nice article!

    By Cadrach on Mar 6, 2009

  9. Interesting series.

    I downloaded the filesystem project files and the code and sql to create the tables appear to be good.

    Can you please also post some sample data? I understand the table relationships in theory, but I can’t seem to populate the tables properly myself.

    Thanks.

    By Simon on May 29, 2009

  10. Thank you so much for this series of tutorials. It has given me a much better understanding of how to appropriately use Acls (although I’ve got a ways to go!)

    By Smcrae on Jun 3, 2009

  11. First, nice Article :-). There is one thing with your user/group management that I think is not exactly ideal: In your _createRoles() method, you add the groups a user belongs to to the user role as parents – so far so good; but if a user is in two groups, one which allows access to a resource and one which denies, it coincidently decides whether to grant access or not based on which group comes later in the array returned by the $user->getGroups() method. So a little weakness with competing rights here I think.

    By Nicolai Lang on Nov 1, 2009

  12. You’re right about that Nicolai, well spotted. In this case, the role which is applied later would override the previously applied one, so yeah, it would be a bit confusing.

    By Jani Hartikainen on Nov 1, 2009

  13. Good post! I used your example to populate my own acl which works fine for a long time.

    Your idea to load only the required resources is a very good idea to optimize the code. On the other hand, it doesn’t always work. When you use Zend_Navigation and want to combine that with ACL, all resources need to be loaded into the ACL. At this point, a complicated ACL cannot benefit from the optimized lazy loading but needs to load all resources in its list first.

    By Jurian Sluiman on Nov 2, 2009

  14. Great tutorial, just can’t figure something out.
    I want to check for several thing in my ACL system, i want to protect certain actions, but sometimes also entire controllers. How could i modify the plugin which checks the access to know when it has to check for a action, and when for a controller?

    By Raymond de Wit on Nov 3, 2009

  15. Raymond,

    I think the best approach could be to protect your actual “resources”. Your controllers and actions are probably processing something, such as articles or users, so instead of checking the controller or action, you’d check access against the specific object that the code is working on.

    That way you won’t need to write code to differentiate between specifics, as you could have the objects implement the acl resource interface.

    By Jani Hartikainen on Nov 3, 2009

  16. Jani,

    Great series, mate! I’m just trying to get my head around the ACL concept and this has helped immensely.

    Can you clarify something for me … in your example is a category (in essence) a role?

    Also how would you get around the issue of multiple roles for a user and the switching of rights as each role is loaded? Would you have a ‘precendence’ for each role which states that role A takes precedence over role B and then load them from lowest precedence to highest?

    Cheers

    By Craig on Nov 26, 2009

  17. Jani, about the category/role thing … I re-read it and a group is a role. So nevermind…

    By Craig on Nov 26, 2009

  18. Very nice, thanks!

    By umpirsky on Dec 15, 2009

  19. Can the RADICORE http://www.radicore.org/
    be integrated with Zend Framework ?

    Total newbie to php so it might be the worst question ever :P

    I am used to java and ACEGI. But for web development over the internet java looks like an overkill.

    By chanakya on Dec 17, 2009

  20. Seeing how Radicore works with PHP, the answer is maybe. Difficult to say without looking into it more.

    By Jani Hartikainen on Dec 17, 2009

  21. Nice article.

    Maybe you can use Zend_Acl_Assertion to allow or deny access to a resource during a certain hour of the day, or to check if the user’s IP is blacklisted/whitelisted.

    Perhaps i’ve missed a thing, but ‘allow’ and ‘deny’ are rules and not privileges. Examples of privileges are ‘create’, ‘view’, ‘delete’, ‘edit’, ‘add’ and each of them can be allowed or denied depending on the role and the resource.

    By ludalito on Apr 1, 2010

  22. Thanks for this advanced post!

    By jaume on Oct 19, 2010

  23. hello,
    could you please tell me if the SQL diagram you generated via some application or did you create it manually ;) ?


    Regards,
    Robert

    By Robert on Mar 18, 2011

  24. Hi Robert

    I used Microsoft Visio to create the diagrams.

    By Jani Hartikainen on Mar 20, 2011

  25. Hi Jani,
    You’ve proved having a great expertise in the Acl domain, therefore I’d like to submit to you a generic isssue that I’ve found around me and that seems to become generic. Instead of considering the obvious resources as pages I wonder if anyone has got into the database protected views as resources and manage the user access to them on a role base. To put it simple, if the app has 4 types of user roles I want to restict the access to a common table through a view associated to a role; the table is split into 4 views. If you have any knowledge about a pointer to such a reference please let me/us know. I can assure you that this is of hight interest.
    Thanks

    By Opariti on Jun 20, 2011

  26. Excellent series of posts! Material very well detailed!
    I recommend to everyone.

    By Felipe Marques on Jul 26, 2011

  1. 2 Trackback(s)

  2. Feb 19, 2009: Zend_Acl part 2: different roles and resources, more on access | CodeUtopia
  3. Feb 13, 2010: Best of 2009 « Evanwilliamsconsulting.com Blogs

Post a Comment

You can use some HTML (a, em, strong, etc.). If you want to post code, use <pre lang="PHP">code here</pre> (you can replace PHP with the language you are posting)