logo       

Re: Zend_Acl custom Assert query: msg#00001

php.zend.framework.auth

Subject: Re: Zend_Acl custom Assert query

This is a very helpful example of how to pull off ACL with clever use of built-in Framework components. IMHO, this should become the basis for a something that should go into the ACL documentation. Thank you guys for putting this together!

Bryce Lohr

Simon Mundy wrote:
Hi Graham, Ralph

After taking all your suggestions and applying them to a real-world application I'm happy to say I've eaten my words(!) and have reworked my code into - in my opinion - a very robust implementation for both high-level ACL access to my controllers as well as a granular low-level ACL strategy for Zend_Db_Table rows.

The approach that worked for me was taking Graham's example of ensuring that my individual 'Business' logic rows implemented the Zend_Acl_Assert_Interface. Originally I thought this would be cumbersome but as I worked my way through the rules it actually helped me plan my approach and build a strong structure around my rules. So a BIG thankyou for that inspiration.

Second was Darby's/Ralphs posting on the wiki at http://framework.zend.com/wiki/display/ZFDEV/Zend_Acl - I had already started treating my per-site ACLs as a separate model but this helped crystallise my thoughts.

My final solution - for those who are interested - involved the following components. I'll list them here and then provide code samples for each to explain how they all work together.

Granted, it has narrowed the scope of ACL to a very specific implementation, but it may serve as a good starting point for some people.

*Requirements*
This simplified example requires an ACL to provide rules on a blog system. 'Guest' (or non-authorised) users can view and search blog posts only. A 'member' can add, view, search and update posts - but a member can only update their own posts, not anyone elses. 'Administrators' can do what they please :). These 3 roles (2 actual, 1 implied) are defined within the ACL as roles.

I am using a custom Zend_Db_Table_Row class in conjunction with Zend_Auth so that when a user has logged in, the Zend_Auth 'identity' is simply a serialized row of my user table. For convenience, I have added a 'getRole()' method (in case I wish to extend this functionality later down the track instead of simply returning a column value of 'member' or 'administrator'). I haven't included these additional components here but if anyone needs them I'll gladly send them on.

The setup revolves around a Front Controller plugin that provides the high level privileges - it checks if an unauthorised visitor is attempting a privileged operation and then directs them to a login controller (not provided here). If a user is authorised but lacks the necessary privileges then they are directed to a custom error page (again, not provided here).
In this example there's no explicit reference to setting up this plugin - you'll need to instantiate this during the Front Controller setup. Also, the ACL business rules don't specifically 'lock out' a guest visitor - if you wished to do that (to test the functionality of the plugin) you could add:-

$this->deny(null, 'default.blog');

The resource is written in a 'module.controller' format to reduce the risk of namespace clashes in an application with multiple modules and similar controller names.

Granular ACL checking happens inside the Controller and the View. Custom view helpers provide a convenient conduit to the ACL rules so that in our view we can show/hide links depending on the user's role and capabilities. With a modified ACL component and appropriately coded models we can not only pass 'known' resources to the ACL for checking, but also individual database records.

The example uses two hypothetical tables - User and Blog. Each Blog entry has a correspending user_id field that relates to a user record, as well as a 'name', 'description' and 'created' field.

*Component list:-*
MyApp_Acl extends Zend_Acl
MyApp_Acl_Resource_Interface
MyApp_Acl_Assert_Owner implements Zend_Acl_Assert_Interface
MyApp_Blog extends Zend_Db_Table_Abstract
MyApp_Blog_Row extends Zend_Db_Table_Row implements Zend_Acl_Resource_Interface
MyApp_Controller_Plugin_Auth
MyApp_View_Helper_IsAllowed

*MyApp_Acl*
class MyApp_Acl extends Zend_Acl
{
protected $_identity;

public function __construct()
{
if (Zend_Auth::getInstance()->hasIdentity()) {
$this->_identity = Zend_Auth::getInstance()->getIdentity();
}

$this->init();
}
// Shortens the signature for 'isAllowed' - we will retrieve the role from the built-in identity
public function isAllowed($resource = null, $privilege = null)
{
return parent::isAllowed($this->getRole(), $resource, $privilege);
}

// Override the default 'get' method to allow MyApp_Acl_Resource_Interface objects straight through
public function get($resource)
{
if ($resource instanceof MyApp_Acl_Resource_Interface) {
return $resource;
}

return parent::get($resource);
}

// Allows the ACL tighter integration with the identity
public function getIdentity()
{
return $this->_identity;
}

// Retrieves a role from the current identity
public function getRole()
{
if (!isset($this->_identity)) {
return null;
}

return $this->_identity->getRole();
}

// Add business logic
public function init()
{
// Add roles
$this->addRole(new Zend_Acl_Role('member'));
$this->addRole(new Zend_Acl_Role('administrator'), 'member');
// Add controller/action resources - used for high level checking during pre-dispatch
$this->add(new Zend_Acl_Resource('default.blog'));

// Add model resources - used for granular checking at controller or view level
// // This line is important. It means you can still query the 'blog' resource for high-level
// privileges if you provide the resource as the string 'blog' or the original resource
// object. If you pass an individual row instead then you can apply granular rules
// as seen below.
$this->add(new Zend_Acl_Resource('blog'));

// Set guest privileges
$this->allow();

// Set model privileges
$this->deny(null, 'blog', array('add', 'update', 'delete'));

// Allow members only to perform these actions
$this->allow('member', 'blog', array('add', 'update', 'delete'));
// Custom assertion to ensure members' credentials match those of an individual blog entry
$this->allow('member', 'blog', array('update', 'delete'), new MyApp_Acl_Assert_Owner());
}
}

*MyApp_Acl_Resource_Interface*
interface MyApp_Acl_Resource_Interface implements Zend_Acl_Resource_Interface
{
public function getResourceId();
}

*MyApp_Acl_Assert_Owner *
class MyApp_Acl_Assert_Owner 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)
{
if ($acl->getRole() == 'administrator') {
return true;
}
// Very simple check to match identity credentials to database row
if ($acl->getIdentity()->id !== $resource->user_id) {
return false;
}
return true;
}
}

*MyApp_Blog*
class MyApp_Blog extends Zend_Db_Table_Abstract
{
protected $_name = 'blog';
protected $_rowClass = 'MyApp_Blog_Row';
}

*MyApp_Blog_Row*
class MyApp_Blog_Row extends Zend_Db_Table_Row_Abstract implements Zend_Acl_Resource_Interface
{
public function getResourceId()
{
return 'blog';
}
}

MyApp_View_Helper_IsAllowed
class MyApp_View_Helper_IsAllowed
{ protected $_acl;

public function __construct()
{
$front = Zend_Controller_Front::getInstance();
if ($front->getParam('acl') instanceof Zend_Acl) {
$this->_acl = $front->getParam('acl');
} elseif (Zend_Registry::isRegistered('acl')) {
$this->_acl = Zend_Registry::get('acl');
} else {
throw new Exception('cannot access the acl via the front controller or the registry');
}
}

// Matches signature of MyApp_Acl (we don't need to pass the $role)
public function isallowed($resource = null, $privilege = null)
{
// Default business rule to return null instead of throwing exceptions for non-known resources if (!$this->_acl->has($resource)) {
$resource = null;
}

return $this->_acl->isAllowed($resource, $privilege);
}
}

*MyApp_Controller_Plugin_Auth*
class MyApp_Controller_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
protected $_auth;
protected $_authName = 'auth';

protected $_acl;
protected $_aclName = 'acl';

protected $_aclRoute = '%s.%s';

protected $_noauth = array('module' => 'default',
'controller' => 'login',
'action' => 'index');

protected $_noacl = array('module' => 'default',
'controller' => 'error',
'action' => 'privileges');

public function setNoAuth($action, $controller = null, $module = null)
{
if ($module !== null) {
$this->_noauth['module'] = $module;
}

if ($controller !== null) {
$this->_noauth['controller'] = $controller;
}

$this->_noauth['action'] = $action;
}
public function setNoAcl($action, $controller = null, $module = null)
{
if ($module !== null) {
$this->_noacl['module'] = $module;
}

if ($controller !== null) {
$this->_noacl['controller'] = $controller;
}

$this->_noacl['action'] = $action;
}
public function preDispatch($request)
{
$controller = $request->getControllerName();
$action = $request->getActionName();
$module = $request->getModuleName();
$resource = sprintf($this->_aclRoute, $module, $controller);
if (!$this->_getAcl()->has($resource)) {
$resource = null;
}

if (!$this->_getAcl()->isAllowed($resource, $action)) {
if (!$this->_getAuth()->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);
}
protected function _getAcl()
{
if ($this->_acl === null) {
$this->_acl = Zend_Controller_Front::getInstance()->getParam($this->_aclName);
if (!($this->_acl instanceof Zend_Acl)) {
throw new Zend_Controller_Dispatcher_Exception('no ACL has been passed to the front controller');
}
}
return $this->_acl;
}
protected function _getAuth()
{
if ($this->_auth === null) {
$this->_auth = Zend_Auth::getInstance();
}
return $this->_auth;
}
}

*Example BlogController...*
The 'index' and 'view' actions are fairly straight-forward. The 'updateAction' shows the ACL rules in operation...

class BlogController extends PeptoKit_Controller
{
protected $_acl;

public function init()
{
$this->_acl = $this->getInvokeArg('acl');
}

public function indexAction()
{
$blog = new MyApp_Blog();

$this->view->blog = $blog->fetchAll();
}
public function viewAction()
{
$blog = new MyApp_Blog();

$entry = $blog->find($this->getParam()->post)->current();

if (!$entry) {
return $this->_forward('missing', 'error');
}
$this->view->blog = $entry;
}

public function addAction()
{
...form and business logic...
}

public function updateAction()
{
$blog = new MyApp_Blog();

$entry = $blog->find($this->getParam()->post)->current();

if (!$entry) {
return $this->_forward('missing', 'error');
}
if (!$this->_acl->isAllowed($entry, 'update')) {
return $this->_forward('permissions', 'error');
}
...form and business logic...
}
}

*Example 'view.phtml' script*
Shows the 'isAllowed' helper in action - if a visitor is not authorised or not privileged they won't see the 'Update' widget.

<h1><?= $this->escape($this->blog->name) ?></h1>
<p class="date"><?= date('jS F, Y', strtotime($this->blog->created)) ?></p>
<p><?= $this->blog->description ?></p>
<p>
<ul>
<? if ($this->isAllowed($this->blog, 'update')): ?>
<li><a href="<?= $this->url(array('action' => 'update')) ?>">Updated/Edit your post</a></li>
<? endif ?>
<li><a href="<?= $this->url(array('action' => 'report')) ?>">Report as inappropriate</a></li>
</ul>
</p>


....

So that string of components has allowed me to create a reasonably flexible implementation. If anyone has anything further to add to this I'd be most keen to hear - it's been fun finding my way through this one!

Cheers

Hi Simon,

I'm not sure whether this is overkill or if it will even work! If it doesn't maybe someone can pickup on where it might.

What about implementing the Zend_Acl_Resource_Interface on a sub classed
Zend_Db_Table_Row object then setting that row in the table model via
setRowClass().

So in your ACL definition:

$acl->add(new Zend_Acl_Resource('blogrow'));

$this->allow('member', 'blogrow', array('update', 'delete'), new
My_Acl_Assert_BlogRule());

The row object...

class My_Db_Table_Row_Blog extends Zend_Db_Table_Row_Abstract
implements Zend_Acl_Resource_Interface {

protected $_resourceId = 'blogrow';

public function getResourceId(){
return $this->_resourceId;
}

}


A table model...

class My_Table_Model extends Zend_Db_Table_Abstract {

...

public function __construct($config = array()){
parent::__construct($config);
$this->setRowClass('My_Db_Table_Row_Blog');
}

...
}

The view helper.

class My_View_Helper_AllowBlogAction {

// Assume acl and role is known here
public function allowBlogAction($resource, $privilige) {
return $this->_acl->isAllowed($this->_role, $resource, $privilege);
}
}

As far as I'm aware passing the custom row object into the helper like this should work as the ACL has/get methods use the resourceId string to check for the existence of the resource and just check against instanceof Zend_Acl_Resource_Interface. Also, the role, resource and privilige specified in the arguments to $acl->isAllowed() will be passed into the assertation class via it's assert() method when the query eventually tries the _getRule() method. In this case the resource also being an instance of Zend_Db_Table_Row_Abstract should allow your assertation class to conditionally check the for whatever conditions would satisfy your needs.

so the assertation could be something like this...

class My_Acl_Assert_BlogRule 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) {
return $this->_isValidBlogger(Zend_Db_Table_Row_Abstract $resource);
}

protected function _isValidBlogger($resource)
{
// set previously from some interaction with Zend_Auth or so
$userData = Zend_Registry::get('userData');

if ($userData->hasIdentity()) {
$identity = $userData->getIdentity();
if ($identity->userRole == $resource->userRole
&& $identity->userId == $resource->userId) {
return true;
}
}
return false;
}
}

in some view script...

<?php foreach ($this->rowset as $row): ?>

<? if ($this->allowBlogAction($row, 'update')): ?>
...show update controls...
<? else: ?>
...show 'guest' default controls...
<? endif ?>

<? endforeach; ?>

Of course this is all thoroughly untested :>

Graham

--

Simon Mundy | Director | PEPTOLAB

""" " "" """""" "" "" """"""" " "" """"" " """"" " """""" "" "
202/258 Flinders Lane | Melbourne | Victoria | Australia | 3000
Voice +61 (0) 3 9654 4324 | Mobile 0438 046 061 | Fax +61 (0) 3 9654 4124
http://www.peptolab.com




<Prev in Thread] Current Thread [Next in Thread>
Google Custom Search

News | FAQ | advertise