Amazium bvba, your online partner
Adding Context to your ACL
  • Share post with Twitter
  • Share post with StumbleUpon
  • Share post with Delicious
  • Share post with Digg
  • Share post with Technorati
  • Share post with Blinklist
2010-06-04 15:43

Adding Context to your ACL

zend_acl, zend framework

Recently I reached the bounderies of what Zend_Acl could do for me. I needed extra levels of access control and ended up cluttering up my controllers. In this article, I investigate and show a clean way to add context to your acl.

IMPORTANT: Based on the comments on this post, I wrote another blog post Content-driven Access Control with Zend ACL. The approach is cleaner and better than the approach displayed here. Please have a look there first.

Two or Three Dimensional ACL

Zend_Acl is quite powerful in itself. But sometimes, in bigger applications it's not always just about giving certain access to a certain resource. In Zend Framework you can have 2D (role & resource) or 3D (role, resource & privilege) ACL rules. While this is good in 99% of the cases, sometimes you need to go further. Sometimes you have 2 levels of access: first on application, then on the context you are in.

Example 1 : the reporter

Suppose you have a news application where you need to pay per article in the archive if you are not a subscriber. You can see all articles you have bought in the past or buy a new article. Your account will have read privileges on the resource articles, but how can the system be sure that you are allowed to see this article?

Example 2 : the reseller

Suppose you have a webhost application where you have multiple resellers that each have their own clients. A reseller can open a client's detail ("read" privilege on "clients"), but only for his own clients. How can the system be sure that the reseller is allowed to view this client?

Four Dimensional ACL

In comes the extra, fourth dimension: context. Context is what defines the state you are currently in. It can be a page you are visiting, a client's details you are requesting, etc... basically it can be anything you want to access.

Zend Framework already has a system that allows you to do extra, real-time checks on ACL, see Conditional ACL Rules with Assertions.

It allows you to do extra checks, like allowing access from certain IPs or denying access outside business hours. All you have to do is create a new class that implements Zend_Acl_Assert_Interface and it's assert function.

There is one big problem though: you can not pass variables from your application to this assertion class. Euhm, correction, you can when you configure your ACL, but most of the time you don't know your context yet at that moment. Your context is known at the time you do your "isAllowed" check on your ACL. This can be for example in your service layer or controller.

Defining the Context

The Dirty Way

So how do we pass the context to the Assertion class? My first idea was to override Zend_Acl's isAllowed() function so I could pass parameters. It would look something like this:

public function isAllowed($role = null, 
                          $resource = null,
                          $privilege = null,
                          $assertParams = array());

This would lead to overriding the function _getRuleType swell. It looked dirty and I didn't like it at all.

A Clean Way

Then it hit me... I had seen that Zend_Acl_Assert_Interface::assert() also passes the acl object to the function. So I did not need to pass the params to the function, I could let my ACL class know what his context was. I decided to extend Zend_Acl to add context to it:

/**
 * Amazium library
 *
 * @category   Amz
 * @package    Amz_Acl
 * @author     Jeroen Keppens; Amazium bvba (http://www.amazium.com)
 */

class Amz_Acl extends Zend_Acl
{

    
/**
     * Array holding the current context. 
     * This could be the currently loaded page, article, user, etc...
     * 
     * @var array
     */
    
protected $_context = array();
    
    
/**
     * Set the context array
     * 
     * @param array $context
     */
    
public function setContextArray($context = array())
    {
        
$this->_context $context;
    }
    
    
/**
     * Get the context array
     * 
     * @return array $context
     */
    
public function getContextArray()
    {
        return 
$this->_context;
    }
    
    
/**
     * Set a context value
     * 
     * @param string $key context item name
     * @param mixed $value 
     */
    
public function setContextValue($key$value null)
    {
        
$this->_context[$key] = $value;
    }
    
    
/**
     * Get the context array
     * 
     * @param string $key context item name
     * @return mixed $value
     */
    
public function getContextValue($key)
    {
        if (isset(
$this->_context[$key])) {
            return 
$this->_context[$key];
        } else {
            throw new 
Zend_Acl_Exception('Context value [' $key '] not set');
        }
    }
    
}

Using Context

Setting up ACL

Setting up the ACL is done like you would normally do it:

$acl = new Amz_Acl();
$acl->addRole(new Zend_Acl_Role('client_subscription'));
$acl->addRole(new Zend_Acl_Role('client_payperview'));
$acl->add(new Zend_Acl_Resource('articles'));

// Clients with a subscription have full access
$acl->allow('client_subscription'
            
'articles'
            
'read');

// Clients without subscription pay per view
$acl->allow('client_payperview'
            
'articles'
            
'read'
            new 
Amz_Acl_Assert_HasArticleAccessAssertion());

Note that we set added the assertion class in the allow.

Assertion class

The assertion class defined above looks like this:

<?php
interface Amz_Acl_Assert_Interface extends Zend_Acl_Assert_Interface
{
    
/**
     * Returns true if and only if the assertion conditions are met
     *
     * @param  Amz_Acl                     $acl
     * @param  Zend_Acl_Role_Interface     $role
     * @param  Zend_Acl_Resource_Interface $resource
     * @param  string                      $privilege
     * @return boolean
     */
    
public function assert(Amz_Acl $acl
                           
Zend_Acl_Role_Interface $role null
                           
Zend_Acl_Resource_Interface $resource null,
                           
$privilege null);

}

class 
Amz_Acl_Assert_HasArticleAccessAssertion implements Amz_Acl_Assert_Interface
{
    
    
/**
     * Check if a user has access to the article
     * 
     * @param  Amz_Acl                     $acl
     * @param  Zend_Acl_Role_Interface     $role
     * @param  Zend_Acl_Resource_Interface $resource
     * @param  string                      $privilege
     * @return boolean
     */
    
public function assert(Amz_Acl $acl,
                           
Zend_Acl_Role_Interface $role null,
                           
Zend_Acl_Resource_Interface $resource null,
                           
$privilege null)
    {
        try {
            
$article $acl->getContextValue('article_id');
            
// Check if user has bought article before
            // ...
        
} catch (Zend_Acl_Exception $e) {
            return 
false;
        }
    }

}

Meanwhile in our Controller...

Last but not least we check access from our controller like this:

$id $this->_getParam($id);
$acl->setContextValue('article_id'$id);
if (
$acl->isAllowed($user->getRole(), 'articles''read')) {
    
// ...
}

Wrapping up

So, I hope you enjoyed this tutorial. I am sure there are plenty of other ways to accomplish the same thing, but this way you can keep your classes clean and still add this extra depth of access control. Let me know what you guys think!

Enjoy!

Jeroen

Comments and Feedback
I don't think that's the way you're supposed to do it. The approach you are outlining has a number of issues. First, you are adding state to your ACL, and you'd have to remove it afterwards. Cumbersome. Second, it defeats the entire purpose of an ACL, as the code that *checks* permissions needs to be aware of what checks are performed, and then set additional information accordingly.

The correct way to do this is to have an object representing your Article resource (typically a domain model object, but could also be some data model object like a Doctrine record, if you absolutely must), and implement Zend_Acl_Resource_Interface in that class*. This object is then passed to the isAllowed() method, and the assertion can perform the necessary checks.

Zend_Acl_Resource_Interface requires you to implement a getResourceId() method*. This method in Zend_Acl_Resource returns whatever you pass into the constructor. Zend_Acl then compares this value for equality. That means that in our case, the method would return "articles" (this should be singular IMO), so your subscriber check works too.

At least that's how I understood Zend_Acl.

*: don't have the source code here, so forgive me if method names are wrong etc
Similarly, your $user should implement Zend_Acl_Role_Interface, and you should have nested roles, where "client" has pay per view privileges, and a "subscriber" role inherits all these privileges.

Zend_Acl is a bit of a beast and very hard to implement right using all of its features, as the documentation makes many of these things not verzpy clear. I suppose one could write a whole book on it. Maybe I should someday :p

TheSorrow

2010-06-05 10:44
I agree with David Zuelke. Use cases for assertions in Zend_Acl are IP checking (CleanIPAssertion in Zend Framework doc) or Time Checking but not individual domain model autorizations management...
Hi,

based on the comments on this article I have written another blogpost Content-driven Access Control with Zend ACL.

Please have a look there, the approach is a lot cleaner than the one explained here. As an added bonus I'm also displaying some application layer magic there. ;)

Cheers,
Jeroen

Add a Comment

Your email is never published or shared. Required fields are marked*