Access Control for all (Part 1)
04 Oct 2006
So, you read the manual but how to put in place access control for the sensitive content of your site still eludes you. Have no fear, this post is for you. A while ago I wrote a descriptive blog on the difference between authentication and access control, it didn't go into code details but in it I mentioned that i would write a tutorial on ACL if the question was still floating around in the future - well here is that tutorial.
This blog will cover a few basic concepts and how to set up your application to permit access control, in the next part I will show how easy it is to administer your access control lists including a demo
All of the examples will be using nothing but core code (including the cake db acl solution itself), I hope that it is found to be useful. Enough! Let us begin..
Background
Granularity
The trouble with access control is that it's implementation is often project specific - what changes? Granularity.
Sometimes you want an on/off switch style access control (owner can do everything, public can only view public content), other times you want things to be controlled based on group rights, another time you want to be able to control things based on individual rights, and that's not even addressing what happens to the things you are controlling - do you want to be able to check if Users can edit all posts? Do you want to check if Bob can edit all posts? Do you want to check if Bob can edit all posts the he created? Do you want to check if Bob can edit section 2 of post number 217? Etc. How granular you make your access control is up to you.
The cost of ACL
Before implementing anything, it's a good idea to think about how to organize your access control such that the logic is simple, and the number of checks are few. It's then worth thinking about it again ;).
Access control isn't free - you are adding additional logic to your application and sending extra queries to your database (unless you use a file-based system, but I'm not going to address that in this article); all of this means extra time, and a poorly implemented ACL system can cripple your application.
There's also a management overhead, as somebody needs to make sure that users are assigned to the right groups (if appropriate) and ensure that they have, or inherit, the correct rights. This is covered in the next post on the topic.
Prerequisites
Authentication
To be able to check if a user can access something, before you can do anything, you need to know who the user is. If you already have some form of authentication system in place, skip on... still here? The purpose here is not to discuss authentication, but thankfully those boys over at CakePHP have already written a simple tutorial on how to put the most basic user authentication in place. If you follow this or a similar approach (DAuth from the bakery seems a more complete solution), then by checking for the presence of a particular session variable it should be possible to know if a user is logged in, and by reading the value you know who is logged in. So, that provides the answer of how to know who is logged in.
"You've gotta have a system"
It is important to be consistent regarding the names for your aros(users/groups/roles) and acos so that the code doesn't need convoluted logic to be able to ask the question "can the current user access this url". What that system is is up to you but if you use a convention, it should then be easier to understand and code how you are going to put access control in place. So here's a suggestion which will be used in this and subsequent posts on the topic:
- the aro used for acl checks is the current user name in lower case.
- aros related to groups or roles will be in upper case.
- the "action" (3rd parameter for the acl queries) will always be "*"
- acos will be named following the syntax "section:controller:method:parameter1:parameter2" (also lower case and underscored if appropriate.)
- The parent of an aco named "x:y:z" will be named "x:y" etc.
- There is only one aco with no parent and that is the aco named "ROOT"
Section refers to the part of the application that is being accessed, in reality that means the name of the plugin or the name of the App folder.
Setting up your app to use Access control
Let's make a couple of assumptions.
- You have one website administrator and only this user has access to /admin/ urls.
- An unidentified user will have the same rights as the aro 'PUBLIC'
- access control will be added to every url (removing acl for an individual controller will be covered a little later).
How can this be achieved? Quite simply by including the AC (access control) component, which is shown below:
{
<?php
/* SVN FILE: $Id: a_c.php 120 2007-04-18 21:00:13Z Andy $ */
/**
* Short description for file.php.
*
* Long description for file
*
* PHP versions 4 and 5
*
* Copyright (c), Andy Dawson
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @filesource
* @copyright Copyright (c) 2007, Andy Dawson
* @package noswad
* @subpackage noswad.app.controllers.components
* @since Noswad site version 3
* @todo TODO Edit this block
* @version $Revision: 120 $
* @created 26/01/2007
* @modifiedby $LastChangedBy$
* @lastmodified $Date: 2007-04-18 23:00:13 +0200 (mié, 18 abr 2007) $
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
/**
* Short description for Class
*
* Long descrption of Class
*/
/**
* Description for var
*
* @var type
* @access public/private/protected
*/
/**
* Description for method
*
* @param type $name description
* @return type description
*/
if (!defined('CAKE_ADMIN')) {
define('CAKE_ADMIN', '!not used');
}
class ACComponent extends Object {
var $name= 'AC';
var $components= array (
'Acl',
'Session'
);
var $settings= array (
'acl' => array (
'mode' => 'full', // Full or lite
'site_admin' => 'andy', // Only aro (user) who can bypass denied access in live mode.
'pages_controller' => 'pages',
'user_id_session_key' => 'User.id', // Where in the session the user ID is stored.
'user_name_session_key' => 'User.username', // Where in the session the user name is stored.
'owner_fk' => 'user_id', // the field used for identifying if the current user is the owner
'owner_aco_prefix' => 'owner_', // prepended the the method name
'slug_param' => 'title', // A named parameter that is set with the slug by the route.
'slug_field' => 'slug' // Field used to find the id of the current item if slugs are used
)
);
var $aro= 'PUBLIC';
var $aco= 'ROOT';
/**
* Startup - Link the component to the controller.
*
* @param controller
*/
function startup(& $controller) {
$this->controller= & $controller;
if (!isset ($this->Acl->Aro)) {
loadModel('Aro');
loadModel('Aco');
$this->Acl->Aro= new Aro; // Temporary
$this->Acl->Aco= new Aco; // Temporary
}
$this->aro= $this->getAro();
if (isset ($this->controller->params[CAKE_ADMIN])) {
$AcoRoot= up(CAKE_ADMIN);
} else {
$AcoRoot= 'ROOT';
}
$this->aco= $this->getAco(null, $AcoRoot);
if (!isset ($this->pageAco)) {
$this->pageAco= 'not defined';
}
if (($this->controller->accessLevel == 'public') && (!isset ($this->controller->params[CAKE_ADMIN]))) {
return true;
}
if ((isset ($this->controller->publicAccess) && ($this->controller->publicAccess)) || isset ($this->params['requested'])) {
return true;
}
if ((low($this->name) == 'app')) {
// It's an error don't do anything
}
elseif ($this->controller->here == '/') {
// don't do anything for the root url to avoid loops
}
elseif (!$this->checkACL($this->aro, $this->aco)) {
$this->accessDenied($this->aro, $this->aco);
}
}
function getAro() {
$aro= $this->Session->read($this->settings['acl']['user_name_session_key']);
if ($aro) {
if ($this->Acl->Aro->find('alias = \'' . $aro . '\'', null, null, -1)) {
$this->aro= $aro;
} else {
$this->aclLog("NO ARO for $aro", 0);
}
}
return $this->aro;
}
// Get the name of the ACO or the Parent if it doesn´t exist.
function getAco($acoAlias= null, $root= 'ROOT') {
$acoAliasOwner= null;
if (!$acoAlias) {
if (method_exists($this->controller, '_getAcoAlias')) {
$acoAlias= $this->controller->_getAcoAlias();
} else {
$action= $this->controller->action;
if ($root == up(CAKE_ADMIN)) {
$acoAlias= $root . ':';
$action= r(CAKE_ADMIN . '_', '', $action);
} else {
$acoAlias= '';
}
if ($this->controller->plugin) {
$acoAlias .= $this->controller->plugin;
} else {
$acoAlias .= APP_DIR;
}
$acoAlias .= ':' . Inflector :: underscore($this->controller->name);
$constraint= $this->__getSlugConstraint();
if ($constraint) { // True if the controller has a model and the model instance can be identified
$fields[]= $this->controller-> {
$this->controller->modelClass }
->primaryKey;
if ($this->controller-> {
$this->controller->modelClass }
->hasField($this->settings['acl']['owner_fk'])) {
$fields[]= $this->settings['acl']['owner_fk'];
$acoAliasOwner= $acoAlias . ':' . $this->settings['acl']['owner_aco_prefix'] . $action;
}
$result= $this->controller-> {
$this->controller->modelClass }
->find($constraint, $fields, null, -1);
unset ($constraint);
if ($result) {
$acoAlias .= ':' . $action . ':' . $result[$this->controller->modelClass][$this->controller-> {
$this->controller->modelClass }
->primaryKey];
$acoAliasOwner .= ':' . $result[$this->controller->modelClass][$this->controller-> {
$this->controller->modelClass }
->primaryKey];
}
} else {
$acoAlias .= ':' . $action;
}
}
}
$this->pageAco= $acoAlias; // store the original page Aco for logging purposes
$constraint['Aco.alias']= $this->__getAliasList($acoAlias, $root, $acoAliasOwner);
$results= $this->Acl->Aco->find($constraint, array (
'alias'
), 'Aco.lft DESC', -1);
if ($results) {
return $results['Aco']['alias'];
} else {
$this->aclLog("No Aco or equivalent parent found for $acoAlias", 0);
return false;
}
}
// What to do when a user requests something they don't have access to.
function accessDenied($aro, $acoAlias, $type= "denied", $url= FULL_BASE_URL) {
if (Configure::read() == 0) {
$this->_addFlash("Ooops no access to that page", "flash_error");
$this->controller->redirect($url, null, true);
die;
} else {
$this->_addFlash("$aro denied to $acoAlias (" . $this->pageAco . ')', "flash_error");
}
}
// Store debugging messages in /app/tmp/logs/debug.log
function aclLog($Message, $level= 1) {
if (Configure::read() >= $level) {
$this->log($level . ' ' . $Message, LOG_DEBUG);
}
}
function checkACL($aro, $aco, $action= null, $originalAcoAlias= null) {
$action= $action ? $action : '*';
if (!$originalAcoAlias) {
$originalAcoAlias= $this->pageAco;
}
$aco= $this->getAco($aco);
if (!$aco) {
$this->aclLog("DENIED $aro access to unknown/blank ACO (" . $originalAcoAlias . ')', 0);
return false;
}
$access= $this->Acl->check($aro, $aco, $action);
if ($access == true) {
$this->aclLog("PASSED $aro for $aco (" . $originalAcoAlias . ')');
return true;
} else {
if ($aro == $this->settings['acl']['site_admin']) {
$this->aclLog("DENIED $aro for $aco (" . $originalAcoAlias . '), **overriden by site Admin setting**', 0);
return true;
} else {
$this->aclLog("DENIED $aro for $aco (" . $originalAcoAlias . ')');
return false;
}
}
}
function checkURL($url, $aro= null) {
$pagesController= $this->settings['acl']['pages_controller'];
$aro= $aro ? $aro : $this->aro;
if (is_array($url)) {
$section= isset ($url['plugin']) ? Inflector :: underscore($url['plugin']) : APP_DIR;
$controller= isset ($url['controller']) ? Inflector :: underscore($url['controller']) : $section;
if (low($controller) == $pagesController) {
$action= isset ($this->params['pass']) ? implode(':', $this->params['pass']) : null;
} else {
$action= isset ($url['action']) ? Inflector :: underscore($url['action']) : 'index';
}
} else {
$result= Router :: parse($url);
$controller= Inflector :: underscore($result['controller']);
if (file_exists(APP . 'plugins' . DS . $controller)) {
$section= $controller;
$controller= $result['action'] ? Inflector :: underscore($result['action']) : $section;
$action= isset ($result['pass'][0]) ? Inflector :: underscore($result['pass'][0]) : 'index';
} else {
$section= APP_DIR;
if ($controller == $pagesController) {
$action= isset ($result['pass']) ? implode(':', $result['pass']) : 'home';
} else {
$action= $result['action'] ? Inflector :: underscore($result['action']) : 'index';
}
}
}
$acoUrl= $action ? "$section:$controller:$action" : "$section:$controller";
$url= is_array($url) ? serialize($url) : $url;
$this->aclLog("url " . $url . " mapped to aco $acoUrl", 2);
$aco= $this->getAco($acoUrl);
if ($this->checkACL($aro, $aco, null, $acoUrl)) {
return true;
}
return false;
}
// Return the list of Namex expoded by : that represent the passed argument and all parents including the root (ultimate parent) in that order.
function __getAliasList($acoAlias, $root= 'ROOT', $acoAliasOwner= null) {
if ($acoAliasOwner) {
$results[]= $acoAliasOwner;
$array= explode(':', $acoAliasOwner);
array_pop($array);
$results[]= implode(':', $array);
}
$results[]= $acoAlias;
$array= explode(':', $acoAlias);
for ($i= 1; $i <= count($array) + 1; $i++) {
array_pop($array);
$results[]= implode(':', $array);
}
$results[]= $root;
return $results;
}
function _addFlash($message,$layout) {
if (method_exists($this->controller, '_addFlash')) {
$this->controller->_addFlash($message,$layout);
} else {
$this->Session->addFlash($message);
}
}
function __getSlugConstraint() {
if (isset ($this->controller-> {
$this->controller->modelClass })) {
if (isset ($this->controller->params['pass'][0]) || isset ($this->params[$this->settings['acl']['slug_param']])) {
if (is_numeric($this->controller->params['pass'][0])) {
$constraint[$this->controller->modelClass . '.' . $this->controller-> {
$this->controller->modelClass }
->primaryKey]= $this->controller->params['pass'][0];
return $constraint;
} else {
if (isset ($this->params) && in_array($this->controller->modelClass, $this->controller->uses) && ($this->controller-> {
$this->controller->modelClass }
->hasField($this->settings['acl']['slug_field']))) {
$Slug= isset ($this->params[$this->settings['acl']['slug_param']]) ? $this->params[$this->settings['acl']['slug_param']] : $this->controller->params['pass'][0];
$constraint[$this->controller->modelClass . '.' . $this->params[$this->settings['acl']['slug_field']]]= $Slug;
return $constraint;
} else {
return null;
}
}
} else {
return null;
}
} else {
return null;
}
}
}
?>
<?php/* SVN FILE: $Id: a_c.php 120 2007-04-18 21:00:13Z Andy $ *//*** Short description for file.php.** Long description for file** PHP versions 4 and 5** Copyright (c), Andy Dawson** Licensed under The MIT License* Redistributions of files must retain the above copyright notice.** @filesource* @copyright Copyright (c) 2007, Andy Dawson* @package noswad* @subpackage noswad.app.controllers.components* @since Noswad site version 3* @todo TODO Edit this block* @version $Revision: 120 $* @created 26/01/2007* @modifiedby $LastChangedBy$* @lastmodified $Date: 2007-04-18 23:00:13 +0200 (mié, 18 abr 2007) $* @license http://www.opensource.org/licenses/mit-license.php The MIT License*//*** Short description for Class** Long descrption of Class*//*** Description for var** @var type* @access public/private/protected*//*** Description for method** @param type $name description* @return type description*/if (!defined('CAKE_ADMIN')) {define('CAKE_ADMIN', '!not used');}class ACComponent extends Object {var $name= 'AC';var $components= array ('Acl','Session');var $settings= array ('acl' => array ('mode' => 'full', // Full or lite'site_admin' => 'andy', // Only aro (user) who can bypass denied access in live mode.'pages_controller' => 'pages','user_id_session_key' => 'User.id', // Where in the session the user ID is stored.'user_name_session_key' => 'User.username', // Where in the session the user name is stored.'owner_fk' => 'user_id', // the field used for identifying if the current user is the owner'owner_aco_prefix' => 'owner_', // prepended the the method name'slug_param' => 'title', // A named parameter that is set with the slug by the route.'slug_field' => 'slug' // Field used to find the id of the current item if slugs are used));var $aro= 'PUBLIC';var $aco= 'ROOT';/*** Startup - Link the component to the controller.** @param controller*/function startup(& $controller) {$this->controller= & $controller;if (!isset ($this->Acl->Aro)) {loadModel('Aro');loadModel('Aco');$this->Acl->Aro= new Aro; // Temporary$this->Acl->Aco= new Aco; // Temporary}$this->aro= $this->getAro();if (isset ($this->controller->params[CAKE_ADMIN])) {$AcoRoot= up(CAKE_ADMIN);} else {$AcoRoot= 'ROOT';}$this->aco= $this->getAco(null, $AcoRoot);if (!isset ($this->pageAco)) {$this->pageAco= 'not defined';}if (($this->controller->accessLevel == 'public') && (!isset ($this->controller->params[CAKE_ADMIN]))) {return true;}if ((isset ($this->controller->publicAccess) && ($this->controller->publicAccess)) || isset ($this->params['requested'])) {return true;}if ((low($this->name) == 'app')) {// It's an error don't do anything}elseif ($this->controller->here == '/') {// don't do anything for the root url to avoid loops}elseif (!$this->checkACL($this->aro, $this->aco)) {$this->accessDenied($this->aro, $this->aco);}}function getAro() {$aro= $this->Session->read($this->settings['acl']['user_name_session_key']);if ($aro) {if ($this->Acl->Aro->find('alias = \'' . $aro . '\'', null, null, -1)) {$this->aro= $aro;} else {$this->aclLog("NO ARO for $aro", 0);}}return $this->aro;}// Get the name of the ACO or the Parent if it doesn´t exist.function getAco($acoAlias= null, $root= 'ROOT') {$acoAliasOwner= null;if (!$acoAlias) {if (method_exists($this->controller, '_getAcoAlias')) {$acoAlias= $this->controller->_getAcoAlias();} else {$action= $this->controller->action;if ($root == up(CAKE_ADMIN)) {$acoAlias= $root . ':';$action= r(CAKE_ADMIN . '_', '', $action);} else {$acoAlias= '';}if ($this->controller->plugin) {$acoAlias .= $this->controller->plugin;} else {$acoAlias .= APP_DIR;}$acoAlias .= ':' . Inflector :: underscore($this->controller->name);$constraint= $this->__getSlugConstraint();if ($constraint) { // True if the controller has a model and the model instance can be identified$fields[]= $this->controller-> {$this->controller->modelClass }->primaryKey;if ($this->controller-> {$this->controller->modelClass }->hasField($this->settings['acl']['owner_fk'])) {$fields[]= $this->settings['acl']['owner_fk'];$acoAliasOwner= $acoAlias . ':' . $this->settings['acl']['owner_aco_prefix'] . $action;}$result= $this->controller-> {$this->controller->modelClass }->find($constraint, $fields, null, -1);unset ($constraint);if ($result) {$acoAlias .= ':' . $action . ':' . $result[$this->controller->modelClass][$this->controller-> {$this->controller->modelClass }->primaryKey];$acoAliasOwner .= ':' . $result[$this->controller->modelClass][$this->controller-> {$this->controller->modelClass }->primaryKey];}} else {$acoAlias .= ':' . $action;}}}$this->pageAco= $acoAlias; // store the original page Aco for logging purposes$constraint['Aco.alias']= $this->__getAliasList($acoAlias, $root, $acoAliasOwner);$results= $this->Acl->Aco->find($constraint, array ('alias'), 'Aco.lft DESC', -1);if ($results) {return $results['Aco']['alias'];} else {$this->aclLog("No Aco or equivalent parent found for $acoAlias", 0);return false;}}// What to do when a user requests something they don't have access to.function accessDenied($aro, $acoAlias, $type= "denied", $url= FULL_BASE_URL) {if (Configure::read() == 0) {$this->_addFlash("Ooops no access to that page", "flash_error");$this->controller->redirect($url, null, true);die;} else {$this->_addFlash("$aro denied to $acoAlias (" . $this->pageAco . ')', "flash_error");}}// Store debugging messages in /app/tmp/logs/debug.logfunction aclLog($Message, $level= 1) {if (Configure::read() >= $level) {$this->log($level . ' ' . $Message, LOG_DEBUG);}}function checkACL($aro, $aco, $action= null, $originalAcoAlias= null) {$action= $action ? $action : '*';if (!$originalAcoAlias) {$originalAcoAlias= $this->pageAco;}$aco= $this->getAco($aco);if (!$aco) {$this->aclLog("DENIED $aro access to unknown/blank ACO (" . $originalAcoAlias . ')', 0);return false;}$access= $this->Acl->check($aro, $aco, $action);if ($access == true) {$this->aclLog("PASSED $aro for $aco (" . $originalAcoAlias . ')');return true;} else {if ($aro == $this->settings['acl']['site_admin']) {$this->aclLog("DENIED $aro for $aco (" . $originalAcoAlias . '), **overriden by site Admin setting**', 0);return true;} else {$this->aclLog("DENIED $aro for $aco (" . $originalAcoAlias . ')');return false;}}}function checkURL($url, $aro= null) {$pagesController= $this->settings['acl']['pages_controller'];$aro= $aro ? $aro : $this->aro;if (is_array($url)) {$section= isset ($url['plugin']) ? Inflector :: underscore($url['plugin']) : APP_DIR;$controller= isset ($url['controller']) ? Inflector :: underscore($url['controller']) : $section;if (low($controller) == $pagesController) {$action= isset ($this->params['pass']) ? implode(':', $this->params['pass']) : null;} else {$action= isset ($url['action']) ? Inflector :: underscore($url['action']) : 'index';}} else {$result= Router :: parse($url);$controller= Inflector :: underscore($result['controller']);if (file_exists(APP . 'plugins' . DS . $controller)) {$section= $controller;$controller= $result['action'] ? Inflector :: underscore($result['action']) : $section;$action= isset ($result['pass'][0]) ? Inflector :: underscore($result['pass'][0]) : 'index';} else {$section= APP_DIR;if ($controller == $pagesController) {$action= isset ($result['pass']) ? implode(':', $result['pass']) : 'home';} else {$action= $result['action'] ? Inflector :: underscore($result['action']) : 'index';}}}$acoUrl= $action ? "$section:$controller:$action" : "$section:$controller";$url= is_array($url) ? serialize($url) : $url;$this->aclLog("url " . $url . " mapped to aco $acoUrl", 2);$aco= $this->getAco($acoUrl);if ($this->checkACL($aro, $aco, null, $acoUrl)) {return true;}return false;}// Return the list of Namex expoded by : that represent the passed argument and all parents including the root (ultimate parent) in that order.function __getAliasList($acoAlias, $root= 'ROOT', $acoAliasOwner= null) {if ($acoAliasOwner) {$results[]= $acoAliasOwner;$array= explode(':', $acoAliasOwner);array_pop($array);$results[]= implode(':', $array);}$results[]= $acoAlias;$array= explode(':', $acoAlias);for ($i= 1; $i <= count($array) + 1; $i++) {array_pop($array);$results[]= implode(':', $array);}$results[]= $root;return $results;}function _addFlash($message,$layout) {if (method_exists($this->controller, '_addFlash')) {$this->controller->_addFlash($message,$layout);} else {$this->Session->addFlash($message);}}function __getSlugConstraint() {if (isset ($this->controller-> {$this->controller->modelClass })) {if (isset ($this->controller->params['pass'][0]) || isset ($this->params[$this->settings['acl']['slug_param']])) {if (is_numeric($this->controller->params['pass'][0])) {$constraint[$this->controller->modelClass . '.' . $this->controller-> {$this->controller->modelClass }->primaryKey]= $this->controller->params['pass'][0];return $constraint;} else {if (isset ($this->params) && in_array($this->controller->modelClass, $this->controller->uses) && ($this->controller-> {$this->controller->modelClass }->hasField($this->settings['acl']['slug_field']))) {$Slug= isset ($this->params[$this->settings['acl']['slug_param']]) ? $this->params[$this->settings['acl']['slug_param']] : $this->controller->params['pass'][0];$constraint[$this->controller->modelClass . '.' . $this->params[$this->settings['acl']['slug_field']]]= $Slug;return $constraint;} else {return null;}}} else {return null;}} else {return null;}}}?>
Note that for easy debugging you are not automatically redirected in this way if your DEBUG setting is greater than 0 - as such never use DEBUG on a live system. I wrote about changing the config at run time before and it can be used to great effect to investigate what is happening on a live system without affecting other users; you can even use the same approach to turn ACL on and off to simplify debugging tasks. It may be useful to write to a log file in the aclLog method, so that keeping track of what is happening is possible.
Great but if it's all automatic, what if I need to change something?
This question is likely to be one of the first on your lips. If you take a look at the component code, it has 2 features which should account for any deviation from how it acts by default.
Different ACO names for a controller
You can change the way aco names are determined by defining the method _getAcoAlias in your controller. Take a look at this snippet from a pages controller to see how you can make use of that:
<?php
class PagesController extends AppController{
...
function _getACOAlias()
{
$acoAlias = "App:".$this->name.":";
$args = $this->passedArgs;
if ($args) {
return $acoAlias.implode(":",$args);
} else {
return "App:".$this->name;
}
}
}
?>
<?phpclass PagesController extends AppController{...function _getACOAlias(){$acoAlias = "App:".$this->name.":";$args = $this->passedArgs;if ($args) {return $acoAlias.implode(":",$args);} else {return "App:".$this->name;}}}?>
Isn't that beautiful? By overriding the method for determining the ACO alias, it is possible to change the ACL check to be meaningful without excessive code.
Bypassing the ACL check for a controller
You can cause the AC component to ignore access to a controller by defining the var publicAccess in the controller and giving it the value 'true'.
Conclusion
The goal of this blog was to explain how to integrate ACL checks into your app I hope that was achieved. The next post on the topic covers how to administer your AROs, ACOs and ACL rules - without writing code yourself to create the objects, and with the flexibility to define the rules as you wish. Did I mention the demo enough times? :)
Tarique Sani, on 1/1/70
Instead of ROOT maybe it can be called BASE for a more congruent terminology (as in base URL etc)
Andy, on 1/1/70
Nice to have you here :). I did wonder about ROOT when I was writing, but decided to stick with the name describing the node position as that is how the demo is implemented. Perhaps I should change it to PEON or something that may give less implied meaning to readers.
Cheers,
AD7six
Dieter@be, on 1/1/70
1) if ($this->name == "app") { // It's an error don't apply acl to error pages :)
i added a strtolower(), because in my case $this->name == 'App' for error pages
2) _addFlash: evil function, it overrides the $helpers array (which breaks a lot of functionality in my case), i didnt understand why, so i just commented that out
3) another _addFlash issue: because of this function i can't access $this->controller->Session in my views. (not that that is proper programming practice but that's how they show it in the better tutorials about improving flashes). if i comment out this _addFlash function (and comment out the calls of it), my code works again
i'll probably encounter more problems, i have some work to do ;-)
Andy, on 1/1/70
Have added a low() call around the name - thanks for the heads up (affects PHP5 instals only)
The flash problem you have encountered is a bit of a conundrum, as the code is just a copy of the session component setFlash method. I have changed the layout used to be 'flash' in the code snippet as previously it refered to a layout that I had created and therefore by default won't exist. Will run a check when I get a minute to see if it now works on a clean 1.1.8 install with no problems. In any event as you konw making use of that method isn´t core to the approach :)
zheka, on 1/1/70
One suggestion:
in _getACOAlias() method use '$acoAlias = APP_DIR' instead of hard coded '$acoAlias ="App"'. this way the code should work with nonstandard cake set ups.
I am also having problems with _addFlash. I had to comment out all the code and use simple $this->Session->setFlash($flashMessage) instead, which surely defeats the purpose.
Thank you again, keep up great work!
gd
zheka, on 1/1/70
ACL on acl demo plugin itself does not work unless " _checkACL ($aro)" is removed from acl_admin_demo_app_controller.php.
I think it is there just to make the demo accessible to all on your site. However, if used unchganged elsewhere, parent::_checkACL would never get called.
zheka, on 1/1/70
_addFlash breaks my custom flash.thtml layout. So I created separate layout file for this function and now I get the multiple flash msgs just fine.
Thanks!
CrazyLegs, on 1/1/70
anyone who has worked on an Iseries would recognise the term *PUBLIC to indicate unidentified users, much clearer
Andy, on 1/1/70
CrazyLegs, on 1/1/70
CrazyLegs, on 1/1/70
Andy, on 1/1/70
But seriously, please don't abuse the possibility to submit comments without logging in, or I'll have to write an IP blacklist beforeFilter method ;).
Brandon, on 7/12/06
Fatal error: Call to undefined method PagesController::_isRequestAction() in /home/kcrock/cake/test/controllers/components/a_c.php on line 162
Obiously, I know that my Pages controller doesn't have an _isRequestAction function. Is there a recommended workaround?
Thanks!
Andy, on 7/12/06
The method isn´t fundamental to the component´s functionality, it adds an implicit "all request action calls are authorised". I removed it from the code posted above a coulpe of days ago, but it is still inlcuded in the download.
There are 2 possible solutions:
1) Delete from the component the references to _isRequestAction
2) Add the method to your app controller
I would recommend the second, as it´s quite useful to know (although it recently became easier, as you can just check for $this->param['requested']), but there will probably be no noticable effect in doing the first also (or alone) - as it doesn´t make sense to make a requestAction call for something that the user doesn't have the rights to see
Bake On!.
Brandon, on 12/12/06
1) In the permissions action of acl_admin_demo_controller.php I had to add the line $this->set('data', $this->data);
2) I had to do the same thing for the index function of aros_acos_controller.php.
I'm running version 1.1.11.111.11111.4064. I know you've mentioned running 1.2. Is that a new feature of 1.2? $this->data is automagically available to the view? I've seen enough of your work to know that you don't make mistakes.
Andy, on 13/12/06
That's a lot of 1s :). Thanks for the complement.
The code for $this->data always being available is something bespoke I put in place when I realized that I could chop something like 300 lines out of my app, and reduce download sizes, by making a simple change. What this means for the downlods is: The generic controller, which the plugin app controller extends and therefore all of the plugin's controllers inherit, includes this in the beforeRender method:
if (!isset($this->_viewVars['data'])) {
$this->set('data',$this->data);
}
Although I run this site using 1.2. I do test the plugins on 1.1 when I make changes. I can't duplicate the error, perhaps it was temporal, or some minor verison differences are affecting how this code works (I tested with 1.1.11.4050). I will investigate a little more later.
To organize things a little better, I recently created a project on cakeforge (http://cakeforge.org/projects/acl-admin) to host the plugin files. I will continue to update the demo files, but as such the online demo and download will be the preview of what become updates to the project on cakeforge.
Thanks for your comment and glad you are finding the plugin useful.
Bake On!
Huz, on 23/2/07
Thanks for your great plugin. However I've problem when I need to deny/allow permission (Acl Permission menu). It seems that it always granting allow but I can't grant deny at all. Do I miss something? My Cake is 1.1.13.4450
Andy, on 24/2/07
Hi Huz,
The plugin itself wouldn't cause that (afaik) have a
look for tickets, there seems to be a relavent ticket related to a table
change.