In Zend Framework 1.9.1, Zend_Acl gets two major issues resolved and a simple API change that now make it possible to create a more robust, more expressive ACL definition with less code. ZF issues ZF-1721 and ZF-1722, each nearly two years old, have both been solved. Over the last two years, I’ve seen a variety of duplicate issues come into the issue tracker, which stem from two fundamental flaws in Zend_Acl – “Zend_Acl::isAllowed does not support Role/Resource Inheritance down to Assertions” and “Zend_Acl assertions breaks when inheritance is required (ie DepthFirstSearch)”. In this article, we’ll explore the API changes that alleviate these two problems, and we’ll demonstrate how to leverage the Zend_Acl assertion system to create expressive, dynamic assertions that work with your applications models.
Backwards Compatible API Changes
Before discussing the issues, let’s go over the API change and how that affects the component. Previously, the two methods for setting up an ACL that were used by a developer were add() and addRole(). Interestingly, add() was intended to imply addResource(). Since add() implied that you were adding a resource, its clear that this component was created from the perspective of resources as a primary actor, and then roles and assertions as secondary actors.
The new API allows for the creation of an ACL by using strings instead of having to use Zend_Acl_Role and Zend_Acl_Resource objects explicitly. To me, this is a pretty important step towards what I’d like to see in 2.0. In 2.0, I would ideally like to see addRole() and addResource() accept strings for types of roles and resources to query against, and accept objects for explicit role and resource objects to query against (even if they match an already registered type). To put simply, I would expect addRole('user') and addRole($userObjectForRalph) to have different behaviors if different permissions were registered for each. This would allow me to specify specific access for the user object ‘ralph’ separately from the ACL’s for objects of role type ‘user’. The behavior can be further defined to either inherit from the type, or override type ACL’s depending on the desired effect. Ultimately, this would allow for a more dynamic experience with Zend_Acl.
Dynamic Assertions Example
In the following example, we’ll have a look at a common use case that is now possible in Zend_Acl. In plain English, what developers want to be able to do is be able to design assertions that can accept application models that implement the Resource or Role interface, and be able to apply some dynamic or custom logic to assess whether or not the given role has access to the given resource. As mentioned previously, this was not possible because in the process of checking the ACL tree, using a depth-first search, the calling resource and roles was lost, and only the original registered objects was being persisted into the assertions. Well, that’s fixed now.
For the purposes of this example, we’ll take a simple concept: a user needs to be able to only edit their own blog post. The user in this case, would be our applications model for users. The actual class will implement the Zend_Acl_Role_Interface. We will also have a BlogPost model which will serve as the resource in question, thus implementing the Zend_Acl_Resource_Interface. Naturally, our system will be able to handle users of different role ‘types’, but our BlogPost will only be of a single resource type ‘blogPost’.
Note: the following code is demonstration only. As such, some coding standards or conventions are not necessarily what you’d expect in proper object-oriented code or even a Zend Framework MVC based application. Some of the code might contain rouge ‘echo’ statements so that the demonstration below will be more expressive of what its actually doing.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class User implements Zend_Acl_Role_Interface { // using public members here for brevity in this article public $id = null; public $role = 'guest'; public function getRoleId() { return $this->role; } } class BlogPost implements Zend_Acl_Resource_Interface { public $id = null; public $ownerUserId = null; public function getResourceId() { return 'blogPost'; } } |
Next, we’ll create the dynamic assertion. We generally would expect this assertion to be called when a User is requested to modify a BlogPost. This assertion will ensure that the BlogPost‘s owner id (the user id that owns said BlogPost), is the same as the provided User objects id. If it is, pass, if not, fail. Fairly common use case, right? Here is what our assertion should look like, with a few inline comments:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class UserCanModifyBlogPostAssertion implements Zend_Acl_Assert_Interface { /** * This assertion should receive the actual User and BlogPost objects. * * @param Zend_Acl $acl * @param Zend_Acl_Role_Interface $user * @param Zend_Acl_Resource_Interface $blogPost * @param $privilege * @return bool */ public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $user = null, Zend_Acl_Resource_Interface $blogPost = null, $privilege = null) { echo ' == Checking the assertion ==' . PHP_EOL; // only here for the purposes of article if (!$user instanceof User) { throw new InvalidArgumentException(__CLASS__ . '::' . __METHOD__ . ' expects the role to be an instance of User'); } if (!$blogPost instanceof BlogPost) { throw new InvalidArgumentException(__CLASS__ . '::' . __METHOD__ . ' expects the resource to be an instance of BlogPost'); } // if role is publisher, he can always modify a post if ($user->getRoleId() == 'publisher') { return true; } // check to ensure that everyone else is only modifying their own post if ($user->id != null && $blogPost->ownerUserId == $user->id) { return true; } else { return false; } } } |
Note: Assertions, as with ACL’s can be treated, and most likely should be treated, as application models. As such, if you are using the Zend Framework MVC application structure, you might want to name this one similarly to Default_Model_Acl_UserCanModifyBlogPostAssertion, and would live in application/models/Acl/UserCanModifyBlogPostAssertion.php. Likewise, the User class would actually be Default_Model_User, and BlogPost might be Default_Model_BlogPost.
Now that we have our models setup for our ACL to interact with, its time to define the actual ACL definition itself. For the purposes of this exercise, we’ll not assume that the ACL itself is a model, but our consuming script below will simply interact with it. In a Zend Framework MVC application, one might find the ACL defined as a model within your application, depending on your needs.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$acl = new Zend_Acl(); // setup the various roles in our system $acl->addRole('guest'); $acl->addRole('contributor', 'guest'); $acl->addRole('publisher', 'contributor'); // add the resources $acl->addResource('blogPost'); // add privileges to roles and resource combiniations $acl->allow('guest', 'blogPost', 'view'); $acl->allow('contributor', 'blogPost', 'contribute'); $acl->allow('contributor', 'blogPost', 'modify', new UserCanModifyBlogPostAssertion()); $acl->allow('publisher', 'blogPost', 'publish'); |
The above code has produced a fully defined ACL object, at least for the purposes of this article, that we can now start interacting with. In the follow examples, we’ll interact with this ACL object. The User and BlogPost objects utilize public properties for brevity and illustrative purposes, but you can assume that these object properties might be populated and persisted via Zend_Db_Table row, a web service, or some other data source persistence layer.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
$user = new User(); $post = new BlogPost(); // some default values $user->id = 1; $post->ownerUserId = 1; /** * Demonstrate guest Privileges */ echo 'Demonstrating ' . $user->role . ' privileges' . PHP_EOL . '------------------------------------------' . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') view?' . PHP_EOL . ($acl->isAllowed($user, $post, 'view') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') contribute?' . PHP_EOL . ($acl->isAllowed($user, $post, 'contribute') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') modify?' . PHP_EOL . ($acl->isAllowed($user, $post, 'modify') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') publish?' . PHP_EOL . ($acl->isAllowed($user, $post, 'publish') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; /** * Demonstrate contributor Privileges */ $user->role = 'contributor'; echo 'Demonstrating ' . $user->role . ' privileges' . PHP_EOL . '------------------------------------------' . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') view?' . PHP_EOL . ($acl->isAllowed($user, $post, 'view') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') contribute?' . PHP_EOL . ($acl->isAllowed($user, $post, 'contribute') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; $post->ownerUserId = 5; // the following two examples should demonstrate the assertion being checked echo 'Can user (' . $user->role . ') modify someone elses blogPost?' . PHP_EOL . ($acl->isAllowed($user, $post, 'modify') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; $post->ownerUserId = 1; echo 'Can user (' . $user->role . ') modify own blogPost?' . PHP_EOL . ($acl->isAllowed($user, $post, 'modify') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') publish?' . PHP_EOL . ($acl->isAllowed($user, $post, 'publish') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; /** * Demonstrate publisher Privileges */ $user->role = 'publisher'; echo 'Demonstrating ' . $user->role . ' privileges' . PHP_EOL . '------------------------------------------' . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') view?' . PHP_EOL . ($acl->isAllowed($user, $post, 'view') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') contribute?' . PHP_EOL . ($acl->isAllowed($user, $post, 'contribute') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; $post->ownerUserId = 5; echo 'Can user (' . $user->role . ') modify someone elses blogPost?' . PHP_EOL . ($acl->isAllowed($user, $post, 'modify') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; $post->ownerUserId = 1; echo 'Can user (' . $user->role . ') modify own blogPost?' . PHP_EOL . ($acl->isAllowed($user, $post, 'modify') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; echo 'Can user (' . $user->role . ') publish?' . PHP_EOL . ($acl->isAllowed($user, $post, 'publish') ? 'yes' : 'no') . PHP_EOL . PHP_EOL; |
Once you have all of that in place, you can see a the run of such a script would produce these results:
[code]
/home/ralph/test-script/$ php acl-inheritance.php
Demonstrating guest privileges
------------------------------------------
Can user (guest) view?
yes
Can user (guest) contribute?
no
Can user (guest) modify?
no
Can user (guest) publish?
no
Demonstrating contributor privileges
------------------------------------------
Can user (contributor) view?
yes
Can user (contributor) contribute?
yes
== Checking the assertion ==
Can user (contributor) modify someone elses blogPost?
no
== Checking the assertion ==
Can user (contributor) modify own blogPost?
yes
Can user (contributor) publish?
no
Demonstrating publisher privileges
------------------------------------------
Can user (publisher) view?
yes
Can user (publisher) contribute?
yes
== Checking the assertion ==
Can user (publisher) modify someone elses blogPost?
yes
== Checking the assertion ==
Can user (publisher) modify own blogPost?
yes
Can user (publisher) publish?
yes
[/code]
Conclusion
Zend_Acl can now be used to make concise, dynamic and expressive ACL systems. The assertion system that is in place in Zend_Acl can be leveraged in ways never seen before out of the box. While the User/BlogPost example is on the simple side, you can use this article to start thinking about the different ways such a system can be leveraged in your own projects where dynamic assertions would simplify controller or model code that is already in place.
Nice article!
Btw, why do we treat models different from libraries? Imho models are also reusable classes that pertain to your own namespace
I would expect that you have
lib/Zend/…
lib/MyComp/Proj1/Acl/User/
lib/MyComp/Proj2/User/
A bit offtopic, but I started to wonder why models are treated that special. Also the recommendation to use Model_User instead of just User strikes me.
Exception e,
Great question. I talk a bit about this in a previous article PHP: Environments, Libraries, and Applications – Oh My!. When you talk about the various layers of software and the different abstraction layers that are involved, each layer attempts to solve a problem up to a certain level of specification. This is how we build stable reusable code.
Models are generally a very project/application specific implementation, whereas the library is where you would expect more generalized code. For example, the ACL engine might live in library, but the actual ACL definition for your application is very much a “model”. Does that make sense?
-ralph
Hi, thanks for the link. It is a well written article. I can understand where your reasoning comes from
But suppose I’ve written a module with a class Model_User and I need to integrate a 3rd-party module that also contribute a model for User, which use the same convention and is thus called Model_User too. Now we can more easily get into problems I think.
Some might suggest that models are module-specific but I dare to question that. It is quite normal that different modules from you application use the same model. Modules are not a means to separate the model, but they contribute behaviour to an application. I can agree what you say
“A Module is a collection of code that solves a more specific atomic problem of the larger business problem.” But I miss the notion of «general/reusable/application agnostic behaviour».
Maybe we could define it as “A Module is a pluggable and reusable stand-alone collection of code that solves a (more or less) concrete larger business problem.”
Maybe I went off-topic. Maybe I am nit-picking.
Thanks for this!
Just wondering – how would you go about spitting out a list of blog posts that a user is able to edit?
I am interested in knowing how to make this multi-role aware. Example being if the user can be a publisher and a reviewer. roleId no longer accurately describes this. Would you create a new dynamic role (perhaps based on their username) and then attach both the pub and review roles? Something like:
$roles = $identity['ROLES']; //array of string roles are associated with the user perhaps from db
$acl->addRole(new Zend_Acl_Role('user-'.$name),$roles); //make sure ACL knows about this new "role"
$user->setRoleId('user-'.$name);
Why do you have ACL permission for the publisher role in the UserCanModifyBlogPostAssertion class?
// if role is publisher, he can always modify a post
if ($user->getRoleId() == ‘publisher’) {
return true;
}
Shouldn’t this instead be handled at the ACL level:
$acl->allow(‘publisher’, ‘blogPost’, ‘modify’);
Is it because publisher inherits from contributor? Would my example work if this permission was removed from UserCanModifyBlogPostAssertion?
class User implements Zend_Acl_Role_Interface {
public $id;
public function __construct($id, $role) {
$this->id = $id;
$this->role = $role;
}
public function getRoleId()
{
return “{$this->role}-{$this->id}”;
}
}
class Post implements Zend_Acl_Resource_Interface {
public $id;
public $authorId;
public function __construct($id = null, $authorId = null) {
$this->id = $id;
$this->authorId = $authorId;
}
public function isAuthor(User $user) {
return $user->id == $this->authorId;
}
public function getResourceId() {
if ($this->id) {
return “posts-{$this->id}”;
} else {
return “posts”;
}
}
}
class IsAuthorOfPost implements Zend_Acl_Assert_Interface {
public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $role = null, Zend_Acl_Resource_Interface $resource = null, $privilege = null) {
// type checks
return $resource->isAuthor($role);
}
}
$acl = new Zend_Acl();
$acl->addRole(“users”)
->addRole(“authors”, array(“users”))
->addRole(“admins”)
->addResource(“posts”)
->allow(“admins”, “posts”)
->allow(“authors”, “posts”, “create”)
->allow(“authors”, “posts”, array(“delete”, “edit”), new IsAuthorOfPost());
$allan = new User(1, “users”);
$acl->addRole($allan, $allan->role);
$mary = new User(2, “authors”);
$acl->addRole($mary, $mary->role);
$joe = new User(3, “authors”);
$acl->addRole($joe, $joe->role);
$mike = new User(4, “admins”);
$acl->addRole($mike, $mike->role);
var_dump($acl->isAllowed($allan, new Post(), “create”)); // false, users cannot create posts
var_dump($acl->isAllowed($mary, new Post(), “create”)); // true, authors can create posts
var_dump($acl->isAllowed($joe, new Post(), “create”)); // true, authors can create posts
var_dump($acl->isAllowed($mike, new Post(), “create”)); // true, admins can do anything with a post
$joesPost = new Post(1, $joe->id);
$acl->addResource($joesPost, “posts”);
var_dump($acl->isAllowed($allan, $joesPost, “edit”)); // false, Allan is not an author
var_dump($acl->isAllowed($mary, $joesPost, “edit”)); // false, Marry is not the author of the post
var_dump($acl->isAllowed($joe, $joesPost, “edit”)); // true, Joe is the author of the post
var_dump($acl->isAllowed($mike, $joesPost, “edit”)); // true, admins can do anything with posts
Great Article! Thanks!
“In plain English, what developers want to be able to do is be able to design assertions that can accept application models that implement the Resource or Role interface, and be able to apply some dynamic or custom logic to assess whether or not the given role has access to the given resource.”
That’s actually pretty funny, believe it or not. (-:
These changes are a huge improvement. Well done! It used to take me days to build a dynamic ACL system from scratch — now I can do it in a few minutes.
Hi Ralph,
In your class UserCanModifyBlogPostAssertion, you are throwing an InvalidArgumentException if the role is not a User or the resource is not a BlogPost. I’ve found that this can cause a problem if you query the ACL with strings instead of objects:
$acl->isAllowed(‘user’, $blogPost, ‘modify’); // throws exception
Although the query doesn’t make a whole lot of sense, this is still correct usage of Zend_Acl. It may be better to return false in your assertion if you can’t work with the passed in objects.
This is literally the best webpage I have ever seen. Thanks, Ralph.
[...] Zend_Acl can now be used to make concise, dynamic and expressive ACL systems. The assertion system that is in place in Zend_Acl can be leveraged in ways never seen before out of the box. While the User/BlogPost example is on the simple side, you can use this article to start thinking about the different ways such a system can be leveraged in your own projects where dynamic assertions would simplify controller or model code that is already in place. Author: Ralph Schindler Source: Ralph Schindler » Zend Framework [...]
Nice article Ralph. I sort of wish I had read this before implementing the same; however, doing so allowed me to dig pretty deep.
Konr mentioned this quite a while ago, but I thought I’d bring it up here in case anyone else stumbles across this article.
Instead of this:
// if role is publisher, he can always modify a post
if ($user->getRoleId() == ‘publisher’) {
return true;
}
I do this:
$acl->allow(‘publisher’, ‘blogPost’, array(‘publish’,'modify’));
Just to be clear; Is there any reason (besides demonstration purposes) to check the publisher’s roleId via the assertion class vs. int he allow definition?
Great thanks for this article! It is that I need so much!
What if resources is dynamic? If resorces are items with itemId and userId (user that create item)?How to dynamicly add resorces by userID, and then assert those same resources?