Zend_Acl part 2: different roles and resources, more on access

February 11, 2009 – 4:04 pm Tags: ,

First a quick announcement: please let me know if the blog seems slow/sluggish. I have been experiencing some slowness, and I recently installed WP Super Cache which seems to have helped, but let me know if you encounter anything!

Now, the post: This time, we’ll look at Zend_Acl a bit deeper after the first part

Applications often have different resources: For example, you might have pages, some user generated content like comments, and an admin area. You might also have files, or even real-life objects like a coffee machine.

In the context of Zend_Acl, access to resources is given to roles: A role might be a user’s name, a group a user belongs to, or just roles, which have been assigned to a user from the admin panel.

Since Zend_Acl only defines an “abstract” role, resource and privilege, how do we deal with all of these using it? Read more to find out! I’ll also be addressing some more ways to deal with allowing and denying access.

Dealing with individual users and usergroups

Let’s say we want to be able to limit access to specific resources based on a user, or a usergroup. How do we accomplish this?

Since each role in Zend_Acl is identified by a string, we can come up with our own role-scheme: For users, the role id will be user-username, and for usergroups, the role id will be group-groupname.

$acl->addRole(new Zend_Acl_Role('user-john'));
$acl->addRole(new Zend_Acl_Role('group-accounting'));

To check against the ACL in this kind of a scenario, we could have something like this:

//We'll assume $user is some kind of a User object
$name = $user->getName();
$groups = $user->getGroups();
 
$roles = array('user-' . $name);
foreach($groups as $g) 
  $roles[] = 'group-' . $g->getName();
 
foreach($roles as $r) {
  if($acl->isAllowed($r, 'someresource', 'someprivilege') {
    //allowed, do something.
  }
}

Improving the user/group example by using Zend_Acl_Role_Interface

In our above example with the strings, there’s a small problem: The strings.

We use user-something and group-something. What if we need to use it in some other place too? We may easily end up with code duplication, and it can become difficult to keep track of changes. In the case we add even more possible role types, it may become an even bigger mess if we introduce some more code which converts the new role type into a string!

Luckily, there’s a quite simple way out of this: by using Zend_Acl_Role_Interface with our objects.

As the user and groups in the example are classes, we could have them implement the role interface. That way we can simply pass the objects to the ACL instead of having to turn them into strings.

class User implements Zend_Acl_Role_Interface {
  //contents of the class here
 
  //this method is for the role interface:
  public function getRoleId() {
    return 'user-' . $this->_name;
  }
}

So we add a method called getRoleId to the class, which returns an identifier for the object. We’ll also add that to our Group class, but with the difference that it returns an id with group- prefix. The earlier code which checks the ACL now looks like this:

//We simply put the user and the groups in an array and be done with it
$roles = array($user) + $user->getGroups();
foreach($roles as $r) {
  if($acl->isAllowed($r, 'someresource', 'someprivilege') {
    //allowed, do something.
  }
}

Different resource types

Just like roles, resources are strings. Resources can also be represented by objects which implement an interface – Zend_Acl_Resource_Interface. You can apply the code shown for roles in this.

Let’s take a quick example with files. Remember our plugin from the first in the series? Here’s it again:

class My_Plugin_Acl extends Zend_Controller_Plugin_Abstract {
  private $_acl = null;
 
  public function __construct(Zend_Acl $acl) {
    $this->_acl = $acl;
  }
 
  public function preDispatch(Zend_Controller_Request_Abstract $request) {
    //As in the earlier example, authed users will have the role user
    $role = (Zend_Auth::getInstance()->hasIdentity())
          ? 'user'
          : 'guest';
 
    //For this example, we will use the controller as the resource:
    $resource = $request->getControllerName();
 
    if(!$this->_acl->isAllowed($role, $resource, 'view')) {
      //If the user has no access we send him elsewhere by changing the request
      $request->setModuleName('auth')
              ->setControllerName('auth')
              ->setActionName('login');
    }
  }
}

Let’s modify this so we can also use files with our ACL.

Creating our resources

We will first create a resource type for controllers, as it isn’t very simple to just create them on the fly for the sake of ACL testing, and then we will look at the File resource.

class Resource_Controller implements Zend_Acl_Resource_Interface {
  public function __construct($id) {
    $this->_id = $id;
  }
 
  public function getResourceId() {
    return 'controller-' . $this->_id; 
  }
}

We create this resource class for the controllers because it would be a bit tricky to create the controller in the ACL just so we could check against it. This class will make the job easier on us.

Now, you probably should have a model class which represents a file, so you’ll just need to make the class implement the Zend_Acl_Resource_Interface

class File implements Zend_Acl_Resource_Interface {
  /* we can have some methods related to the file model here */
 
  public function getResourceId() {
    return 'file-' . $this->_name; 
  }
}

So this assumes that your File model has a property $_name. The name is then used with the resource id.

Modifying the plugin

Now, we need to first modify the ACL plugin to use the controller resource. We’ll also modify it to use the earlier introduced User class as the role:

public function preDispatch(Zend_Controller_Request_Abstract $request) {
  //Let's assume that the identity is a User object this time
  $user = Zend_Auth::getInstance()->getIdentity();
 
  //We need to create a new controller resource using the name:
  $resource = new Resource_Controller($request->getControllerName());
 
  if(!$this->_acl->isAllowed($user, $resource, 'view')) {
    //If the user has no access we send him elsewhere by changing the request
    $request->setModuleName('auth')
            ->setControllerName('auth')
            ->setActionName('login');
  }
}

Okay, so as the user object already implements the role interface, we can pass it to isAllowed. Same with the resource.

Important: Remember that you need to use the Resource_Controller and User classes when creating the ACL! Otherwise this will not work!

//assume $someUser is an user object
$this->addRole($someUser);
 
//assume $someController is a Resource_Controller object
$this->add($someController);
 
$this->allow($someUser, $someController, 'view');

You may notice that this approach doesn’t work very well with static ACLs which are written in code. Next week, we will look at how to create dynamic ACLs, where this kind of approach works better! But for now, let’s continue.

Creating a new plugin

We could add the code for checking access to files to our earlier plugin, but if we create a separate plugin for it, it will be easier to reuse with other code. Let’s assume that we have some code which serves files to our users. When a user wants to download a file, the request will have the parameter file, which will contain the file’s name.

This will be a very similar plugin to the first:

class My_Plugin_Acl_File extends Zend_Controller_Plugin_Abstract {
  private $_acl = null;
 
  public function __construct(Zend_Acl $acl) {
    $this->_acl = $acl;
  }
 
  public function preDispatch(Zend_Controller_Request_Abstract $request) {
    //This too will use the user object from zend_auth
    $user = Zend_Auth::getInstance()->getIdentity();    
 
    //Instead of using the controllers name, we'll use the parameter
    $filename = $request->getParam('file');
 
    //If the request is not for a file we can just exit here without doing anything
    if(empty($filename))
      return;
 
    $resource = new File();
    $resource->setName($filename);
 
    if(!$this->_acl->isAllowed($user, $resource, 'view')) {
      $request->setModuleName('auth')
              ->setControllerName('auth')
              ->setActionName('login');
    }
  }
}

So this is basically the same as the earlier, but instead of checking for the controller, we check for a file.

Note: instead of creating a new File(), it may be a good idea to use some class to fetch a file model, and to check if there even is a file with the required name.

More on allowing and denying access

As we have seen a few times, we can allow access to ACL resources with the allow method. However, there are some additional things I haven’t mentioned yet.

If you allow access to resource null, but with some privilege, it effectively means the role will have that privilege for all resources, unless explicitly denied:

//myrole will get dosomething privilege on *all* resources
$acl->allow('myrole', null, 'dosomething');

Similarily, allowing on null privilege will give every privilege on a resource:

//myrole will get all privileges on someresource (privilege defaults to null)
$acl->allow('myrole', 'someresource');

The isAllowed method has similar features. If you don’t define the privilege while checking for access to a resource, it will require that the role has all privileges on the resource.

You can also give access to all resources and all privileges by leaving out both the resource and privilege when calling allow. Or, you can omit the role too, which will allow everyone access to everything – essentially requiring you to explicitly deny access if you don’t want certain roles accessing certain resources. This can be useful for creating blacklist-style behavior.

In closing

Even though we have gone through lots of ways to use Zend_Acl, there’s still a much to cover. Next week we will look at how to build “dynamic” ACLs from database or files, and we will again improve our ACL code and plugins.

You can read the third post of the series here

Notes: it may be a good idea to modify the plugins introduced in this post to actually check if there is an identity before proceeding. If there is no identity, it’s very likely that isAllow will throw an exception, or behave in an unexpected way. You also should check if the resource/role exist in the ACL, or you will likely get an exception.

Much of the examples in this post will work better when the ACL is created dynamically from a database. This is because models and such require database access to begin with, and creating them on the fly as seen in the examples may not be a good idea. However, it’s important to understand these things before moving on.

These things will be addressed in future posts in the series.

Share this:
  1. 19 Responses to “Zend_Acl part 2: different roles and resources, more on access”

  2. Guess, I am the first one to comment.

    Read both posts but feel that one needs to implement this stuff to get a hang of it. I am new to Zend read the docs was looking for some practical implementation and thank you for giving a good example now going to go through this and implement this on my own.

    Nice work, though i would be nice to add privilige collision when multiple parents are there that define the permission for the role, the first parent class from the right get’s the privilege decision.

    any ways looking forward to ur next post if i can still come to ur site.

    By Adeel Shahid on Feb 11, 2009

  3. True, by implementing this kind of stuff yourself you will learn better than by just reading. It’s the same with anything. I’m hoping these posts will help you understand how Zend_Acl works, and therefore making it easier to implement your own.

    To get privilege collision, I think you’d need to modify the Zend_Acl classes, though personally I think it kind of makes sense that the child classes “override” the rules.

    By Jani Hartikainen on Feb 11, 2009

  4. Superb, now +1000^1000 for showing how to deal with acl storage/management

    By iongion on Feb 11, 2009

  5. Just a note about roles: isAllowed() checks the last role in an array of roles first. For this reason, you should usually go from less-specific to more-specific roles when defining the array of roles to which a user belongs. For instance, you would likely want your user-* role to be last added to the array,so that it is checked _first_, as roles specific to a user should take precedence.

    By Matthew Weier O'Phinney on Feb 11, 2009

  6. Good point Matthew, though I think that would depend much on the way you want the ACL to work, or on how you defined which roles the user has.

    If you define the user as a role, and have the role inherit from every role the user should have, then you would want to have the more specific roles last.

    You could alternatively (as in the first example) define the roles as a part of a user object, and check each role’s access separately, in which case the order shouldn’t matter, unless you want to check if a more precise role has been specifically denied access.

    By Jani Hartikainen on Feb 11, 2009

  7. I’ve read this and was thinking about it a bit in line with Django. There the resources are your models with the edit, create etc privileges.

    So then your User and Group classes would implement both Zend_Acl_Role_Interface and Zend_Acl_Resource_Interface.

    And maybe a property defining the available privileges. So you could automatically load all the models, get the privileges and the resources. and link them to roles.

    This way you could even create instance specific resources. (A specific blog post for example can only be edited by users in a specific group)

    By Harro on Feb 12, 2009

  8. Hmm maybe the instance specific rights are better checked by also implementing the Zend_Acl_Assert_Interface

    By Harro on Feb 12, 2009

  9. next up, how to implement all this in a database MVC enviroment :P

    Great article Jani!

    By ranza on Feb 12, 2009

  10. Great explanation. Really looking forward to the next one in the series!

    By Fozzy on Feb 12, 2009

  11. good article,
    it’ll be useful if you post some examples how to use Zend_Acl with Zend_Db.

    By unixvps on Mar 2, 2009

  12. Thansk!
    It is a very useful guide.

    By coiby on Jul 10, 2010

  13. Great explanation. It’s very useful

    By vijay on May 4, 2012

  14. The article is very good. The only thing I am confused is where all those classes are written in file. And where those file resides. It could have been more clear if the full path of the file was attached just above the code.

    I successfully complete the first part but having trouble going through this tutorial as I am confused with file naming conventions. Even If I try Zend conventions I couldn’t make it work. So please mention the file naming conventions as well. Thank You for the great article.

    By AlexShr on May 9, 2012

  15. If you have trouble with the files, an easy way to get it working is simply using require_once to include the files.

    You can usually get it working pretty easily by putting them in library/Example/ and prefix each class name with Example_ (or whatever prefix you want). Then just make sure the autoloader is configured correctly.

    By Jani Hartikainen on May 10, 2012

  16. Thank you so much !!! Using prefixing classname and using autoloader worked fine.

    By AlexShr on May 10, 2012

  1. 4 Trackback(s)

  2. Feb 11, 2009: Zend_Acl part 1: Misconceptions and simple ACLs | CodeUtopia
  3. Feb 19, 2009: Zend_Acl part 3: creating and storing dynamic ACLs | CodeUtopia
  4. May 26, 2009: Tims Blog » Blog Archive » In Training
  5. Nov 9, 2011: Zend_Navigation with ACL Array instead of String

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)