Dynamic Assertions for Zend_Acl in ZF

August 13th, 2009 § 16 comments

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.

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:

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.

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.

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.

Tagged , , , ,

  • Exception e

    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.

  • http://ralphschindler.com/ Ralph Schindler

    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

  • Exception e

    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. :D

  • Matt

    Thanks for this!

    Just wondering – how would you go about spitting out a list of blog posts that a user is able to edit?

  • Greg

    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);

  • Konr Ness

    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?

  • Toxygene

    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

  • Jonathan

    Great Article! Thanks!

  • http://davidmintz.org David Mintz

    “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. (-:

  • http://blog.virgentech.com/ Hector Virgen

    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.

  • http://blog.virgentech.com/ Hector Virgen

    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.

  • http://google.com Cameron

    This is literally the best webpage I have ever seen. Thanks, Ralph.

  • Pingback: Dynamic Assertions for Zend_Acl in ZF | Zend Framework University

  • http://wilmoore.com Wil Moore III

    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?

  • http://weltkind.com Weltkind

    Great thanks for this article! It is that I need so much!

  • gogink

    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?

What's this?

You are currently reading Dynamic Assertions for Zend_Acl in ZF at Ralph Schindler.

meta