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

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.