ACL Behavior

By Steve Oliveira (coeus)
This behavior is built off of the core Acl behavior. It fixes some issues with the core behavior and allows you to set a model as both an Aro and Aco. It also adds the alias where as the core behavior doesn't.

Description

This Acl behavior makes a number of improvements from the built-in one. It allows you to have a model act as both an "Aro" and "Aco". It creates an alias in this format "Model.id". If parent node is not provided, it'll create one based on root object. For example, if you create Post.1 as an ACO and you set parentNode to null, it'll set the parent_id to the root "Post" Aco if it exists. So your tree would look like the following:

Download code
Acos
----------
Post
  |-Post.1
  |-Post.2
  |-Post.3

Usage Instructions


Follow the following steps to use the Acl behavior.
Step 1
Copy the behavior code below into a file named "acl.php" in your /app/models/behaviors folder.

Step 2
Load the behavior in the model you want to use it in.

Examples:

Ex 1: Act as Aco
Download code var $actsAs = array('Acl' => array('Aco'));
Ex 2:
Act as Aco
Download code var $actsAs = array('Acl' => 'Aco'); // Can accept a string as well
Ex 3: Act as Aro
Download code var $actsAs = array('Acl' => array('Aro'));
Ex 4: Act as Aco and Aro
Download code var $actsAs = array('Acl' => array('Aro','Aco'));
Step 3
Create the method parentNode($type) in your model. This will return the parent_id to a method in the Acl behavior.

Here's an example of a User model passing a group id as it's parent id only for the ARO:
Download code
function parentNode($type)
{
    if ($type == 'Aro') {
        if (!$this->id) {
            return null;
        }

        $data = $this->read();

        if (!$data['User']['group_id']){
            return null;
        } else {
            return array('model' => 'Group', 'foreign_key' => $data['User']['group_id']);
        }
    } else {
        return false;
    }
}

That's it!

Behavior Class

Download code
<?php

class AclBehavior extends ModelBehavior {

/**
 * Maps ACL type options to ACL models
 *
 * @var array
 * @access protected
 */

/**
 * Sets up the configuation for the model, and loads ACL models if they haven't been already
 *
 * @param mixed $config
 */
    
function setup(&$model$config = array()) {

        if (empty(
$config)) {
            
$config = array('Aro');
        } 
        elseif (
is_string($config)) {
            
$config = array($config);
        }

        
$this->settings[$model->name]['types'] = $config;

        foreach (
$this->settings[$model->name]['types'] as $type)
        {
            if (!
ClassRegistry::isKeySet($type)) {
                
uses('model' DS 'db_acl');
                
$object =& new $type();
            } else {
                
$object =& ClassRegistry::getObject($type);
            }
            
$model->{$type} =& $object;
        }
        

        if (!
method_exists($model'parentNode')) {
            
trigger_error("Callback parentNode() not defined in {$model->name}"E_USER_WARNING);
        }
    }
/**
 * Retrieves the Aro/Aco node for this model
 *
 * @param mixed $ref
 * @return array
 */
    
function node(&$model$type$ref null) {
        if (empty(
$ref)) {
            
$ref = array('model' => $model->name'foreign_key' => $model->id);
        }
        return 
$model->{$type}->node($ref);
    }
/**
 * Creates a new ARO/ACO node bound to this record
 *
 * @param boolean $created True if this is a new record
 */
    
function afterSave(&$model$created) {
        if (
$created) {

            foreach (
$this->settings[$model->name]['types'] as $type)
            {
                if (
$parent $model->parentNode($type)) {
                    
$parent $this->node($model$type$parent);
                } else {
                    
$parent $model->{$type}->node($model->name);
                }
                
$parent_id Set::extract($parent"0.{$type}.id");
            
                
$model->{$type}->create();
                
$model->{$type}->save(array(
                    
'parent_id'        => $parent_id,
                    
'model'            => $model->name,
                    
'foreign_key'    => $model->id,
                    
'alias'            => $model->name "." $model->id
                
));
            }
        }
    }
/**
 * Destroys the ARO/ACO node bound to the deleted record
 *
 */
    
function afterDelete(&$model) {
        foreach (
$this->settings[$model->name]['types'] as $type)
        {
            
$node Set::extract($this->node($model$type), "0.{$type}.id");
            if (!empty(
$node)) {
                
$model->{$type}->delete($node);
            }
        }
    }
}

?>

 

Comments 628

CakePHP Team Comments Author Comments
 

Comment

1 Awesome

This is insane, you got some mad PHP skills alright.
Posted Mar 13, 2008 by Anders Ingemann
 

Comment

2 Great

Nice, thanks a lot for the hard work.
Im still trying to wrap my mind around ACL, and something tells me is simpler that I think... but what the hell. :)
Posted Mar 27, 2008 by Ramiro Araujo
 

Comment

3 reduce sql queries

Hi,

i wrote a little class to increase the speed of ACL's by decreasing the SQL queries by saveing them into the SESSION data. This might be useful for everybody who dont change the permission very frequently.

But the weakpoint -> its written in german, but the classes are quite clearly to understand (on request i would translate it if some facts are unclear).

http://ttwhy.wordpress.com/2008/03/13/cakephp-mit-access-listen-beschleunigen/
Hope you enjoy it (please leave a comment, and tell me your sugestions)
Posted Apr 9, 2008 by ttwhy
 

Comment

4 Thanks

Nice, thanks a lot for the hard work.
Im still trying to wrap my mind around ACL, and something tells me is simpler that I think... but what the hell. :)

Thank you for the compliments, I'd just like to note that most of the hard work should be credited to the CakePHP team, I simply made minor modifications to their behaviour. This is nowhere near where it should be and I expect a better behaviour to be released for 1.2 stable.
Posted Apr 15, 2008 by Steve Oliveira
 

Comment

5 Not Playing Nice With Auth

Steve, thanks for the great behavior. Unfortunately, it doesn't seem to play nice with Auth.

The basic problem is that the ACL code above creates ACOs based upon the model name (e.g. 'User') while Auth uses the controller name (e.g. 'users') when setting Auth to use 'crud' authentication. For model-level access, this can easily be addressed by editing auth.php to use Inflector::singularize() before passing ACO aliases to $this->Acl->check(). However, this doesn't solve the problem of record-specific ACOs (e.g. 'User.1'), since Auth in 'crud' mode only searches for ACOs based on the controller (as mentioned above).

Have you run into this before? How did you solve this? Did you just use one of Auth's other authentication methods? If this is a problem for other people, I'd be happy to contribute a little patch to auth.php that addresses this.
Posted May 14, 2008 by Peter Robinett
 

Comment

6 Possible fix for CRUD mode

Have you run into this before? How did you solve this? Did you just use one of Auth's other authentication methods? If this is a problem for other people, I'd be happy to contribute a little patch to auth.php that addresses this.
I think I just solved this problem for me by changing the acl.php behavior's afterSave() method to the following:

PHP Snippet:

<?php /**
 * Creates a new ARO/ACO node bound to this record
 *
 * @param boolean $created True if this is a new record
 */
    
function afterSave(&$model$created) {
        if (
$created) {

            foreach (
$this->settings[$model->name]['types'] as $type)
            {
                if (
$parent $model->parentNode($type)) {
                    
$parent $this->node($model$type$parent);
                } else {
                    
$parentName = ($type == 'Aco') ? Inflector::pluralize($model->name) : $model->name;
                    
$parent $model->{$type}->node($parentName);
                }
                
$parent_id Set::extract($parent"0.{$type}.id");
                
$modelName = ($type == 'Aco') ? Inflector::pluralize($model->name) : $model->name;
                
                
$model->{$type}->create();
                
$model->{$type}->save(array(
                    
'parent_id'        => $parent_id,
                    
'model'            => $modelName,
                    
'foreign_key'    => $model->id,
                    
'alias'            => $model->name.'.'.$model->id
                
));
            }
        }
    }
?>

Have not fully tested, but it works if crud mode in my initial attempts.

All I have changed is that, if the type of request is 'Aco', a call to Inflector::pluralize is made when creating the records. It still creates a singular alias for actual user records so that you don't need to change any custom checks you are doing (since the core behavior didn't even create aliases, I figured that wouldn't matter).

Can anyone comment on this? Thanks.
Posted Aug 27, 2008 by Ben McClure
 

Comment

7 Users changing groups

Except this still doesn't take into account if a User changes a Group:
http://mark-story.com/posts/view/auth-and-acl-automatically-updating-user-aros
Not that it has to, but would be nice ... should be an easy fix-up:

In User model:

    function afterSave($created) {
        if (!$created) {
            $parent = $this->parentNode('Aro');
            $parent = $this->node('Aro', $parent);
            $node = $this->node('Aro');
            $aro = $node[0];
            $aro['Aro']['parent_id'] = $parent[0]['Aro']['id'];
            $this->Aro->save($aro);
        }
    }
Posted Oct 22, 2008 by Brenton