Zend_Acl and MVC Integration Part II (Advanced Use)

July 16, 2008

Tutorials, Zend Framework

By Aldemar Bernal

In part one we saw how to setup our Zend_Acl instance and how to attach it to the MVC environment (by using the Front Controller Plugin), but what about setting another action for the denied access, or how does an article be edited only by its owner?, this, and some more is what we are going to see in the following examples.

As I said in part one, this article is based on the following Zend Framework Proporsal (link), by this moment this proposal is in the laboratory.

1. Using Modules

Let’s begin talking about modules, we were using devzone as an example for this article, what about if we create an admin module to handle the approval of articles and have some others tasks like changing the quick links or categories?, then we will have to redefine our ACL resources model:

Default Module:

  1. user controller.
  2. article controller.

Admin Module:

  1. article controller.
  2. quick-link controller.
  3. category controller.

Based on this new resources model, we create a Zend_Acl instance that reflects this.

Note: Remember that this code and the creation of the Zend_Acl object must be executed before calling the Front Controller dispatch method (at bootstrap time).

/** Creating Roles */
require_once 'Zend/Acl/Role.php';
$myAcl->addRole(new Zend_Acl_Role('guest'))
      ->addRole(new Zend_Acl_Role('writer'), 'guest')
      ->addRole(new Zend_Acl_Role('admin'), 'writer');

/** Creating resources */
require_once 'Zend/Acl/Resource.php';
/** Default module */
$myAcl->add(new Zend_Acl_Resource('user'))
      ->add(new Zend_Acl_Resource('article'));

/** Admin module */
$myAcl->add(new Zend_Acl_Resource('admin'))
      ->add(new Zend_Acl_Resource('admin:article', 'admin'))
      ->add(new Zend_Acl_Resource('admin:quick-link', 'admin'))
      ->add(new Zend_Acl_Resource('admin:category', 'admin'));

/** Creating permissions */
$myAcl->allow('guest', 'user')
      ->deny('guest', 'article')
      ->allow('guest', 'article', 'view')
      ->allow(array('writer', 'admin'), 'article', array('add', 'edit'))
      ->allow('admin', 'admin');

/** Setting up the front controller */ 
require_once 'Zend/Controller/Front.php'; 
$front = Zend_Controller_Front::getInstance(); 
$front->setControllerDirectory(array('default' => 'path/to/default/controllers',
                                     'admin' => 'path/to/admin/controllers'));

/** Registering the Plugin object */ 
require_once 'Zend/Controller/Plugin/Acl.php'; 
$front->registerPlugin(new Zend_Controller_Plugin_Acl($myAcl, 'guest')); 

/** Dispatching the front controller */ 
$front->dispatch(); 

Notice that as we did before, we created a resource for every controller, but in the case of the admin module, we created a resource for the module and one for every controller inside that module in the format ‘module:controller’ and making them children of the admin resource, also we allowed admin role to be the only one that can access all the admin module.

2. Using Roles

Once an user has login the application, that user must have a role, in our example that role can be either ‘guest, ‘writer’ or ‘admin’, so, how can we change the current ACL role in our component? First at all, you must store this role into a variable which is in the session scope, in that way, once an user logs in, you must store that user role into the session, then, in the next request, you will take that session variable to setup the Front Controller Plugin at bootstrap time.

user controller

class UserController extends Zend_Controller_Action
{
    protected $_application;

    public function init()
    {
        require_once 'Zend/Session/Namespace.php';
        $this->_application = new Zend_Session_Namespace('myApplication');
    }

    public function loginAction()
    {
        ... Validation code
        if ($valid) {
            /** Setting role into session */
            $this->_application->currentRole = $user->role;
            $this->_application->loggedUser = $user->username;
        }
    }

    public function logoutAction()
    {
        $this->_application->currentRole = 'guest';
        $this->_application->loggedUser = null;
    }
}



bootstrap

/** Loading application from session */
require_once 'Zend/Session/Namespace.php';
$application = new Zend_Session_Namespace('myApplication');

if (!isset($application->currentRole)) {
    $application->currentRole = 'guest';
}

/** Setting up the front controller */ 
require_once 'Zend/Controller/Front.php'; 
$front = Zend_Controller_Front::getInstance(); 
$front->setControllerDirectory('path/to/controllers'); 

/** Registering the Plugin object */ 
require_once 'Zend/Controller/Plugin/Acl.php'; 
$front->registerPlugin(new Zend_Controller_Plugin_Acl($myAcl, $application->currentRole)); 

/** Dispatching the front controller */ 
$front->dispatch();

3. Setting the denied error action

Maybe some of you (hope nobody =D) just don’t like the idea of having the access denied action in the error controller, or just want to name it some other way. This can be done by calling the setErrorPage method of the Front Controller Plugin.

/** Setting up the front controller */ 
require_once 'Zend/Controller/Front.php'; 
$front = Zend_Controller_Front::getInstance(); 
$front->setControllerDirectory('path/to/controllers'); 
 
/** Setting default access denied action */ 
require_once 'Zend/Controller/Plugin/Acl.php'; 
$aclPlugin = new Zend_Controller_Plugin_Acl($myAcl, 'guest');
$aclPlugin->setErrorPage('goaway', 'my-error-controller', 'my-module'); 

/** Registering the Plugin object */ 
$front->registerPlugin($aclPlugin); 
 
/** Dispatching the front controller */ 
$front->dispatch(); 

setErrorPage method can be called just passing the action name, and the controller and module will remain ‘error’ and ‘default’, also can be called passing the action and the controller or passing all the three parameters.

4. Using the action helper

Finally we’ll see one of the most important parts of this proposal, we’ve seen so far in our devzone example, that we can allow only writers and admins to edit an article, but wait, there is still a missing piece, if I am a writer and I can access article/edit/:id, that means that I can not only edit my articles but others too, big doh!, that’s not really good, is it?, so, what do we do now? manage it using the Action Helper, that means that you will be able to access our ACL inside any controller and not only at bootstrap time.

So, the first thing we do is to register not only our Front Controller Plugin but also our Action Helper in the Controller Action Helper Broker.

bootstrap

/** Loading application from session */
require_once 'Zend/Session/Namespace.php';
$application = new Zend_Session_Namespace('myApplication');

if (!isset($application->loggedUser)) {
    $application->loggedUser = null;
}

/** Setting up the front controller */ 
require_once 'Zend/Controller/Front.php'; 
$front = Zend_Controller_Front::getInstance(); 
$front->setControllerDirectory('path/to/controllers'); 

/** Registering the Plugin object */ 
require_once 'Zend/Controller/Plugin/Acl.php'; 
$front->registerPlugin(new Zend_Controller_Plugin_Acl($myAcl, $application->currentRole)); 

/** Registering the Action Helper object */ 
require_once 'Zend/Controller/Action/Helper/Acl.php'; 
require_once 'Zend/Controller/Action/HelperBroker.php'; 
Zend_Controller_Action_HelperBroker::addHelper(new Zend_Controller_Action_Helper_Acl()); 

/** Dispatching the front controller */ 

$front->dispatch();

And after registering the Helper, we can use it inside every controller we have, so, let’s only give edit access to the owner or any admin.

article controller

class ArticleController extends Zend_Controller_Action 
{ 
    protected $_acl; 
    protected $_application;
 
    public function init() 
    { 
        /** Get our Action Helper */
        $this->_acl = $this->_helper->getHelper('acl'); 

        require_once 'Zend/Session/Namespace.php';
        $this->_application = new Zend_Session_Namespace('myApplication');
    } 

    ...
 
    public function editAction() 
    { 
        /** Load article by id */ 
        $article = new Article($this->_request->id);

        /** Validate if the user is the owner or an Admin */
        if (($article->author != $this->_application->loggedUser) && ($this->_application->currentRole != 'admin')) { 
            $this->_acl->denyAccess(); 
        } 
 
        ...
    } 
} 

Conclusion

Some archaeologists waste their whole life trying to find the missing link (and they will die looking for it =D) and some ZF’ers get older trying to make ACL work properly with their MVC environment (don’t worry, you won’t die before figuring it out), that said, I hope this proposal can be one of those missing pieces in the world of ACL + MVC.

A final recommendation, follow the Keep It Simple principle, if you don’t really need ACL to be dynamic loaded, handwriting it is not a sin, it’s just the best way to do it.

For more information about this you can go to:

Zend_Acl & MVC Integration

and here is a small implementation source code of this:

Source Code

13 Responses to “Zend_Acl and MVC Integration Part II (Advanced Use)”

  1. whisher Says:

    Hi.
    I’m playing around with your source
    code, btw excellent tutorial ;),
    an I realized that if you request
    like /nocontroller you end up with
    a denied action so I manage this
    code

    <pre>
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    $resourceName = ”;
    $response = $this->getResponse();
    if($response->isException()){
    $this->_request->setModuleName(‘default’);
    $this->_request->setControllerName(‘error’);
    $this->_request->setActionName(‘error’);
    return null;
    }

    if ($request->getModuleName() != ‘default’) {
    $resourceName .= $request->getModuleName() . ‘:’;
    }

    $resourceName .= $request->getControllerName();

    /** Check if the controller/action can be accessed by the current user */
    if (!$this->getAcl()->isAllowed($this->_roleName, $resourceName, $request->getActionName())) {
    /** Redirect to access denied page */
    $this->denyAccess();
    }
    }
    </pre>

    Is it the right way ?

    Thanks in advance.

  2. basskeeper Says:

    vertigoone wrote:

    /** Admin module */
    $myAcl->add(new Zend_Acl_Resource(‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:article’, ‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:quick-link’, ‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:category’, ‘admin’));

    —————————————————————-

    maybe thats right:
    $myAcl->add(new Zend_Acl_Resource(‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:article’), ‘admin’)
    ->add(new Zend_Acl_Resource(‘admin:quick-link’), ‘admin’)
    ->add(new Zend_Acl_Resource(‘admin:category’), ‘admin’);

    i.e. $myAcl->add(new Zend_Acl_Resource(‘child’), ‘parent’),
    not $myAcl->add(new Zend_Acl_Resource(‘child’, ‘parent’))

  3. pruden Says:

    After I have tried to solve the problem which I told you about above I think I know where is the problem. I am using a Auth plugin which I found in this site web (I think so). The mistake is in the isAllowed function because It returns true instead of returning false according to what is established in the ACl authorization.

    How can I use the isAlloed function to work with module?

    Thanks!

    MYACL

    <?php

    class MyAcl extends Zend_Acl
    {
    public function __construct()
    {
    $roleGuest = new Zend_Acl_Role(‘guest’);

    // Resources
    /* Default module */
    $this->add(new Zend_Acl_Resource(‘index’))
    ->add(new Zend_Acl_Resource(‘user’));

    /* Admin module */
    $this->add(new Zend_Acl_Resource(‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:index’), ‘admin’);

    // Roles
    $this->addRole(new Zend_Acl_Role(‘guest’))
    ->addRole(new Zend_Acl_Role(‘member’), ‘guest’)
    ->addRole(new Zend_Acl_Role(‘staff’), ‘member’)
    ->addRole(new Zend_Acl_Role(‘admin’), ‘staff’);

    // Authorization
    $this->allow(‘guest’, array(‘index’, ‘user’))
    ->deny(‘guest’, ‘admin’)
    ->allow(‘admin’, ‘admin’);
    }
    }

    ______________________________________________________________________

    MyPluginAuth

    <?php

    class MyPluginAuth extends Zend_Controller_Plugin_Abstract
    {
    private $_auth;
    private $_acl;

    private $_noauth = array (‘module’ => ‘default’, ‘controller’ => ‘user’, ‘action’ => ‘login’);
    private $_noacl = array (‘module’ => ‘default’, ‘controller’ => ‘user’, ‘action’ => ‘login’);

    public function __construct ($auth, $acl)
    {
    $this->_auth = $auth;
    $this->_acl = $acl;
    }
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
    if($this->_auth->hasIdentity())
    {
    $role = $this->_auth->getIdentity()->role;
    }
    else
    {
    $role = ‘guest’;
    }

    $controller = $request->controller;
    $action = $request->action;
    $module = $request->module;
    $resource = $request->controller;

    if (!$this->_acl->has($resource))
    {
    $resource = null;
    }

    if (!$this->_acl->isAllowed($role, $resource, $module)) //$role, $resource, $action
    {
    if(!$this->_auth->hasIdentity())
    {
    $module = $this->_noauth['module'];
    $controller = $this->_noauth['controller'];
    $action = $this->_noauth['action'];
    }
    else
    {
    $module = $this->_noacl['module'];
    $controller = $this->_noacl['controller'];
    $action = $this->_noacl['action'];

    }
    }

    $request->setModuleName($module);
    $request->setControllerName($controller);
    $request->setActionName($action);

    }

    }

  4. pruden Says:

    I was reading your article and then I used it for my project. The code of MyAcl class is the following:

    class MyAcl extends Zend_Acl
    {
    public function __construct()
    {
    $roleGuest = new Zend_Acl_Role(‘guest’);

    // Resources
    /* Default module */
    $this->add(new Zend_Acl_Resource(‘index’))
    ->add(new Zend_Acl_Resource(‘user’));

    /* Admin module */
    $this->add(new Zend_Acl_Resource(‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:index’, ‘admin’))
    ->add(new Zend_Acl_Resource(‘admin:create’, ‘admin’));

    // Roles
    $this->addRole(new Zend_Acl_Role(‘guest’))
    ->addRole(new Zend_Acl_Role(‘member’), ‘guest’)
    ->addRole(new Zend_Acl_Role(‘staff’), ‘member’)
    ->addRole(new Zend_Acl_Role(‘admin’), ‘staff’);

    // Authorization
    $this->allow(‘guest’, array(‘index’, ‘user’))
    ->deny(‘guest’, ‘admin’) //this does not work
    ->allow(‘admin’, ‘admin’);
    }
    }

    The problem is that the code work with the index and the user controller within the default module, however It does not work with the admin module and the index controller within the admin module. In the code of the article It is possible to see that all the controller have different name, in the case of my code, there are two controller with the same name Index. If I use that:

    $this->deny(‘guest’, ‘index’);

    "guest" can not access to index controller within the default module and the admin module. I think the system is not distinguishing between the admin and default module.

    On the other hand, I have a question. Is this right?

    $this->deny(‘admin’, ‘admin’)
    ->allow(‘admin’, ‘admin:index’);

    Thanks!

  5. bytte Says:

    Nice article, exactly what I was looking for.
    The view helper as proposed by someone here before, would be a nice addition.

  6. vahabzadeh Says:

    Its a perfect article about ACL, but is any one have its source code? and its usage with Zend_Auth ?
    Please help me, I wanna its advance usage
    Thanks

  7. paboivin Says:

    thanks, but yes, I think we might need part 3:

    Architecturally most data model will predicate the scope of identity+authority to be closely contained, e.g. top down from user to role(s). Therefore unfolds an implicit
    supply chain of validation from ‘who’ to ‘what.’

    Worded differently, Auth validates Id, to which role is attached and hands it of to Acl.

    Consider below:
    ##########################################
    public function preDispatch($request)
    {
    if ($this->_auth->hasIdentity()) {
    $role = $this->_auth->getIdentity()->getUser()->role;
    } else {
    $role = ‘guest’;
    }
    …………………….

    —————————
    Zend_Acl / Zend_Auth Example Scenario
    Tutorials Zend Framework Zend_Acl Zend_Auth
    by Cal Evans (editor) | 31 comments | Thursday, February 8, 2007
    ##########################################

    From Cal’s article in dev zone this code makes perfect sense. The only problem I was not able to implement auth the way Cal did and retrieve
    role from the authorization obj.
    I hoping to find out in your article …. maybe in part 3 ;-)

    Hope this makes more sense now.

    Thanks,

    - p

  8. vertigoone Says:

    @bladeofsteel: human knowlegde belongs to the world =) (quoted from <a href="http://www.imdb.com/title/tt0218817/"&gt;’antitrust’</a>), I’m not against, don’t know devzone :P

    @dmuir: I thought about a view helper but found it useless but you just gave us a clear example, so, I’ll add it to the proposal, thx. Stay tuned about non-static Acl, I’ll post 2 articles about how to <a href="http://framework.zend.com/wiki/display/ZFPROP/Zend_Acl+dynamic+loading+-+Aldemar+Bernal">dynamic load ACLs</a>.

    @chriswoodford: load Acl from a Zend_Config object <a href="http://framework.zend.com/wiki/pages/viewpage.action?pageId=39025&focusedCommentId=39150#comment-39150">was part</a> of the original plan for this proposal but I left it because it didn’t fit in this proposal, I’ll think later about creating a new proposal for this kind of functionality.

    @paboivin: Quoted from <a href="http://framework.zend.com/manual/en/zend.auth.html#zend.auth.introduction">Zend_Auth manual</a>:

    "Zend_Auth is concerned only with authentication and not with authorization. Authentication is loosely defined as determining whether an entity actually is what it purports to be (i.e., identification), based on some set of credentials. Authorization, the process of deciding whether to allow an entity access to, or to perform operations upon, other entities is outside the scope of Zend_Auth. For more information about authorization and access control with the Zend Framework, please see Zend_Acl."

    btw, there is not part 3 :P, maybe it would be if necessary (view helper explanation).

  9. paboivin Says:

    Maybe I am missing something here or isn’t the ‘missing link’ is between
    Zend_Auth and Zend_ACL?

    We should be using Zend_Auth to pull the roles then pass it to ACL no?

    Tried to look for an example somewhere and could not find one.

    Maybe it’s going to be in part 3?

  10. chriswoodford Says:

    Very concise and easy to folow.

    I would really like to see an update to the Zend_Acl such that an instance of Zend_Config can be passed to the constructor. That way an entire hierarchy of roles can easily be loaded from a ini file or a database table

  11. dmuir Says:

    Would it be wise to set up a view helper as well?
    Sometimes you want certain parts of an application to be visible to certain types of users.
    Eg. If you’re a writer an edit link should only be visible when viewing your own article.

    Loved the article. The part 1 was a real help, and part 2 has nearly completed the puzzle for me :-)

    The only thing that is missing for me now is how to go from hard-coding the acl like you have, to loading the acl from a database.

  12. bladeofsteel Says:

    Thank you for this article.

    I translated the article into Russian. I hope you are not against.
    Translate: http://lobach.info/develop/zf/zend_acl-and-mvc-integration-part-2/

  13. cabernet Says:

    Great information, with perfect timing. I was just getting into trying to implement functionality like this with one of my projects. Thanks!