When I first started using the Zend Framework, one of the most confusing things was the ACL component. Even after I succesfully implemented an ACL solution in my app, something seemed wrong with it.
I would like to argue that the proper use for the Zend ACL is actually impossible, since there is a reported bug in the code which, to my knowledge, has not been fixed.
The use case I will discuss is where you want to only authors to edit articles and only to articles authored by themselves. I think this is a fairly common case.
How does the ACL help us? The ACL allows the developer to setup access rules for arbitrary resources and arbitrary roles. The key word here is “arbitrary”, and I’ll get back to that in a moment.
My feeling is that when most people start out with the ACL, they assume that resources are controllers (or some combination of module and controller), and permissions are actions. So, for example, let’s say you had a controller called “Article” with an action called “edit”.
class ArticleController extends Zend_Controller_Action {
public function editAction() {
//do stuff
}
}
So an ACL would be set up with something like this:
$acl = new Zend_Acl();
$acl->add(new Zend_Acl_Resource('article'));
$acl->addRole(new Zend_Acl_Role('author'));
$acl->deny();
$acl->allow('author', 'article', array('edit'));
This looks farely straight forward. Some place before the action is dispatched, like in a plugin, action helper or whatever you want, you would do a check against this acl.
$acl->isAllowed($role, $resource, $permission);
There are two problems with this approach, one obvious and one not so obvious.
The obvious one is that, while this may be successful in blocking regular users from editing the articles, it doesn’t block another author from editing the article.
In order to do that, we need to add in an additional assertion:
$acl->allow('author', 'article', array('edit'), new AssertionIsArticleAuthor());
The problem then becomes: in this assertion, how do you know which article it is? How do you know who the current author is? These can be very tricky questions. You might have to somehow stick the request object in there, and access the Zend_Auth singleton. While these certaintly are viable solutions, they just “smell” bad.
The other not so obvious problem is seen by taking a few steps back and looking at the whole problem from the beginning. The first question to ask is, what exactly are we trying to solve here? We want to restrict access to an article. So what did we do instead? We restricted access to a controller? What kind of sense does that make? Additionally, we are now tying in our ACL implementation to the url structure of the site. That is primarily a function presentation. If we would want to change that, it would require changing our ACL implentation. While Zend_Acl is certainly a “Zend” component, it has nothing directly to do with Zend MVC, but this approach ties us down to Zend MVC. What if we decided one day that we did not like the MVC components of ZF and wanted to move to Symfony or MVCnPHP?
Zend Framework is primarily a presentation framework. It helps in providing a clear and organized way to bring the presentation layer to the user. And that’s what it excels at. That, I believe, is the number one reason why they don’t ship with a “model” component. The model layer must be provided by yourself, and this part is probably the hardest part of the whole game. But if you keep some simple principles in mind, and keep focused, you can reapproach this whole problem in a new light.
We’ll start with our model layer. This should be fully functional without ZF. Pretend you’re writing a desktop app. Or a shell script. Whether or not the model layer should be tied to the database or separated from that as well, is a different question.
My personal approach, one which is fairly common in the Java world, is to work with Business objects, which are just “POPOs”…Plain Old PHP Classes. These business objects represent all the entities in my application…but this is a post for later. Let’s not digress too much.
In our model layer, we have two objects, an Article and an Author.
class Article {
public $id;
public $title;
public $body;
/**
* @var Author
*/
public $author;
}
class Author {
public $id;
public $name;
}
Any business logic in your app should either take place within these objects, or within other objects that act as a service layer. Your controllers should either talk to these service layer objects or to the business objects themselves.
When you retrieve data from the database, instead of keeping it around as array, put it in one of these objects. You can also use a custom Zend_Db_Row object for this to if you want to make things really simple.
Now that we’re dealing with objects, we can see that we want to restrict access to an Article object to the Author object contained within it. So our resource becomes the Article and the role is the Author. How do you define these as resources and roles? All we have to do is have our objects implement the Zend_Acl_Resource and Zend_Acl_Role interfaces! So we will modify the previous code to something like this:
class Article implements Zend_Acl_Resource_Interface {
public $id;
public $title;
public $body;
/**
* @var Author
*/
public $author;
public function getResourceId() {
return 'article';
}
}
class Author implements Zend_Acl_Role_Interface {
public $id;
public $name;
public function getRoleId() {
return 'user';
}
}
Now we write our custom assertion:
class Chintion_Acl_Assert_AssertIsArticleAuthor implements Zend_Acl_Assert_Interface
{
public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $author = null,
Zend_Acl_Resource_Interface $article = null, $privilege = null)
{
return $this->article->author == $author;
}
}
Now when you have your Article object, you can take your Author object (which you can either store in Zend_Auth or construct it from the data in Zend_Auth), and do a check like this:
$acl->isAllowed($author, $article);
Very nice, clean and simple.
Except…..
It won’t work. By the time the assertion gets a hold of the instance of Zend_Acl_Role_Interface, its been demoted to plain old Zend_Acl_Role. Ditto for Zend_Acl_Resource_Interface. It doesn’t have our fancy beefed up role or resource.
This I think is due to the bug report that I linked to at the beginning. Fortunately, for the moment, there’s a work around:
We have to override the method Zend_Acl::get(Zend_Acl_Resource_Interface $resource) and the method Zend_Acl_Role_Registry::get(Zend_Acl_Role_Interface $role);
To do this, you have to subclass Zend_Acl and overridde the get method as such:
public function get($resource)
{
if ($resource instanceof Zend_Acl_Resource_Interface) {
return $resource;
}
return parent::get($resource);
}
and similary for Zend_Acl_Role_Registry:
class RoleRegistry extends Zend_Acl_Role_Registry {
public function get($role)
{
if ($role instanceof Zend_Acl_Role_Interface) {
return $role;
}
return parent::get($role);
}
}
and in your subclassed Zend_Acl:
$this->_roleRegistry = new RoleRegistry();
#1 by rick at May 3rd, 2009
Simply genius. Great example for the assertion and an elegant solution.
However, I do find acl at the controller/action level quite valuable. You could combine that with your assertion for 2 step authorization. Deny regular users before having to call the assertion. You’re not really tied to urls when using routes. But potentially the most compelling reason, you can do a directory scan for controllers and use reflection on the classes to create an admin gui for protecting controllers. The most dangerous part of an acl is not knowing what is protected. What was missed. A clear visual of the what controllers/actions are allowed/denied by role is safer.
#2 by Avi at May 3rd, 2009
If you just want to deny access to pages in your app, I would use something like Zend_Navigation which is a level of abstraction higher than routes.
#3 by jim at May 13th, 2009
Good stuff!
I was using Zend_Db_Table objects as a lazy “model layer” as I had not encountered a compelling *enough* argument to add a business object abstraction by default (for my current project). Your example has changed my perspective. Ultimately I can write less code by creating a business object class, compose my Zend_Db_Table object(s) where needed within the class, and lock down access at the model layer with an ACL rule. Thanks again for the article!
#4 by Avi at May 14th, 2009
Ultimately I think it boils down to the nature of the application and its complexity, plus how much it might need to scale. I was once a big proponent of always separating everything nicely like that. But then I needed to write a simple app which this was clearly overkill, but still needed a database. I chose django/python for that and I was amazed at how much you could do when an app basically just needs CRUD and a little extra push! But anything more than that, and it breaks down quickly.
#5 by Harry at May 25th, 2009
This is golden, and is most definitely the way to do things. I have a couple questions though: when you build your ACL, do you keep resources for each instance of your model below their generic parent in the tree? What happens when you need to grant an author permission to edit things they didn’t create?
#6 by robert at July 2nd, 2009
Why not using linux’s style permissions like rwxrwx? The first 3 goes for owner,(articles?the creator),the other 3 for the roles’ users. If you are reffering to others resources.Do you want to share some privileges with other users?create a temp group(role+userid) e put the users there.
#7 by ayesha at July 15th, 2009
return $this->article->author == $author;
shouldn’t it be
$article->author == $author
#8 by jonathan at July 24th, 2009
How would this work when roles are inheriting other roles? for instance, if i have a user who inherits both an author role and an admin role. if i am passing in the user, Zend will check both the author and the admin roles that are stored in the acl. however, both of those roles will be of type Zenc_Acl_Role and the current user object wont be used in the assertion. how can i get around that?
#9 by Avi at July 26th, 2009
Why would the user object not be in the assertion?
#10 by jonathan at July 27th, 2009
because when you pass in the user to check if it’s allowed to perform the action, the acl checks up the chain, grabbing roles from the registry. when it gets to author, it would be getting whatever was added to the acl when the acl was set up. i’m not sure how that would be associated with the user that was passed in?
#11 by Avi at July 31st, 2009
The user object in this case implements the Zend_Acl_Role_Interface. If the role of your user object is one that inherits from two roles, Zend_Acl should be able to take care of that. I’m not following you’re problem.
#12 by ronny stalker at November 11th, 2009
Its refreshing to see another person detecting that whole ‘ACL protects controllers’ smell’. I have always been scared of that technique. But, cos of my limited knowledge of OOP I did not feel confident enough to argue against that idea. Instead i just remained confused about ACL as a whole. So, thanks for the reassurance that i was not totally dumb.
I’m still a bit confused, though. In your model, or ubiquitous language, what exactly is ‘an author’?
Is it ‘a user’ or ‘a role’. It seems to be both.
As far as i can tell from your example. For every type of resource, (article/message/comment) you need to create a user/role object that deals with it specifically.
I would be sooo grateful if you could add some more to this tutorial and give, say, two more examples of how it works.
3 is the magic number.
Then, I feel I might be able to extrapolate these dots of wisdom into a curve of knowledge.
#13 by Avi at April 27th, 2010
Sorry for taking such a long time to get back. A role is anything that implements the Zend_Role interface. Doesn’t necessarily have anything to do with users per se.
#14 by darvid at December 9th, 2009
Resource should instead allow you to optionally specify any object, and you name it when you add it. So say you have an application object you want to protect you do this:
$object=new Application();
$acl->addResource(new ZendAclResource(“name”, $object));
Then the object is magically contained in the resource record so you can access it with the assert interface.
#15 by talentedmrjones at July 14th, 2010
It seems that the bug you mentioned was fixed in ZF 1.9.1. So I assume that at this point (Im using 1.10.4) I dont need to subclass Zend_Acl or Zend_Acl_Role_Registry, and that implementing a Zend_Acl_Assert_Interface will suffice. Am I correct?
#16 by Avi at July 14th, 2010
Yup, this bug has been fixed.
#17 by Kim at February 13th, 2011
I see that Matthew Weier O’Phinney does something similar, but his user object (which implements the ACL Role interface) is stored in the session. Could be a nice tweak?