Zend_Acl part 3: creating and storing dynamic ACLs

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