Article

Wrapping your models snugly in ACL interfaces

May 21st, 2011

More and more we're encountering Zend Framework projects where people have written up their own model implementations. Which isn't a surprise of course, as the framework does not include ORM, and the models folder is still empty after running the "zf create project" command...

We are no exception, as we've rolled out an additional library to Zend Framework including models using the Datamapper pattern

Wait.. ORM, Datamapper.. wasn't this article supposed to be about the ACL interfaces? Yes yes, I'm getting to that. You see, when we started using our models we also wanted to add the default access control list and decided *not* to do it the Zend Framework way...

When you read the introduction to Zend_Acl you are told about two interfaces to implement if you want to utilize ACL to control access to your classes. I'm assuming that you've already discovered that it's a major pain to check privileges by doing a $logged_in_user->getRole() call and sending a string to the Acl from controllers or services or views or whatever.

So you get started on utilizing your own models in the ACL in the first class that makes sense, App_Model_User (that's how we roll, but I'm generally talking about whatever you use use as your "user" model) and implement the Zend_Acl_Role_Interface.

class App_Model_User Extends F500_Model_ModelAbstract
                Implements Zend_Acl_Role_Interface
{
    protected $_roleId;
    public function getRoleId()
    {
        return $this->_roleId;
    }
}

This is only half the picture - a role is just plain lonely without a bunch of resources - so you look at a good first resource to add to your access control list. You've just finished the first admin module to your project (the one almost every project on the planet has), where you can CRUD... that's right... Users! Hmm.. that file looks familiar.. but let's get to it:

class App_Model_User Extends F500_Model_ModelAbstract
                Implements Zend_Acl_Role_Interface, Zend_Acl_Resource_Interface
{
    protected $_roleId;
    protected $_resourceId;

    public function getRoleId()
    {
        return $this->_roleId;
    }

    public function getResourceId();
    {
        return $this->_resourceId;
    }
}

This is getting ugly! Plus, if you didn't do this from the start, you'll have to (re)visit all the models in the project to add the implement - or add it to your base class and potentially give too many classes a ResourceId method. Especially when adding ACL to an existing project this can become quite a painful refactor.

So how do we go about doing this differently without losing the ability to send the *actual* models to the ACL? [ in case you're wondering, sending the actual objects to the ACL is absolutely necessary to use Assertions. You know, things like "User A can edit Object B, but only IF he created it.. or IF he was awarded ownership.. or IF the planet Pluto has regained planet status ].

To still send the models to the ACL we, well, like the title of the article says.. we wrap. Like TacoBell.

Anywhere in your application, you'll have -for example- an App_Model_User instance, and an App_Model_Planet instance. You grab the ACL from wherever you like, in this case the registry, and try to check some privilege:

$acl = Zend_Registry::get('Acl');
$canOrbit = $acl->isAllowed( $user, $planet, 'orbit' );

All we ever do with the Acl class after setting the proper privileges is calling the 'isAllowed' method. Overriding this method by extending the Zend_Acl class gives us the opportunity to check if we are actually passing something NOT implementing Zend_Acl_Role/Resource_Interface and swiftly wrapping it. I'll get to the wrappers in a minute, but here's the new isAllowed method:

/**
 * @param  mixed|null  $role
 * @param  mixed|null  $resource
 * @param  string|null $privilege
 * @return bool
 */
public function isAllowed( $role = null, $resource = null, $privilege = null )
{
    if( !is_null( $role ) and !is_string( $role ) and !( $role instanceof Zend_Acl_Role_Interface )) {
        $class = $this->_defaultRoleClass;
        $role  = new $class( $role );
    }

    if( !is_null( $resource ) and !is_string( $resource ) and !( $resource instanceof Zend_Acl_Resource_Interface )) {
        $class    = $this->_defaultResourceClass;
        $resource = new $class( $resource );
    }

    return parent::isAllowed( $role, $resource, $privilege );
}

Of course we leave the default behavior intact, sending a string or an actual Zend_Acl_Role_Interface, but if it's anything else, we wrap the $role in $_defaultRoleClass and the $resource in $_defaultResourceClass (the model is put in the wrapper via the constructor). These classes can be anything you like, and they can be as smart as you like, but in general all they need to know is how to get a RoleId and a ResourceId from the incoming models. Oh yeah, and they need a way of returning the model for usage in the planetary Assertions I was talking about before... how else will we find out who owns the moon??

class F500_Acl_Role_Model extends Zend_Acl_Role
{
    protected $_model = null;

    public function __construct( $model )
    {
        $this->_model  = $model;
        $this->_roleId = $model->getShortName() . 'Model';
    }

    public function getModel()
    {
        return $this->_model;
    }

}

class F500_Acl_Resource_Model extends Zend_Acl_Resource
{
    protected $_model = null;

    public function __construct(  $model )
    {
        $this->_model      = $model;
        $this->_resourceId = $model->getShortName() . 'Model';
    }

    public function getModel()
    {
        return $this->_model;
    }
}

These are our wrappers.. roles are always project specific so we extend the F500_Acl_Role_Model class ( Role-Model grin) in the application, but this is basically the $user->getRole() again. The getShortName method returns the classname without "App_Model_", so just it could be "User". This function was not added to support ACL's, we rely on this to find the mappers that go with this model (it's all part of our nifty Datamap library remember?). We also append "Model" to the resource name to avoid resource name collisions in ACL. It works with strings after all, and you wouldn't want to add privileges on the user(Model) resource when you were actually thinking about the user(Controller) resource, right?

So.. in conclusion.. adding two wrapper classes and an extended ACL, you've added the ability to add privileges to all your models in one swift go. Without ever touching any existing models. How snug is that?

Ramon de la Fuente

Pointy haired boss