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