CakePHP 1.2 ACL/Auth The Quest Continues
In a recent post, I had stated that my ACL Quest was complete. Now it continues. Based upon http://realm3.com/ I have started a continuation of the previous Quest. A current project requires that the users have multiple roles(groups) assigned to them. This can be quite benefits in many applications today, so much that I have tried to write this to be available for any HABTM ACL Relationship. I have setup Cake 1.2 with a users table, groups table, and a HABTM groups_users table. The following is a sql dump of the 3 tables.
-- version 2.10.0.2
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Jan 26, 2008 at 03:58 PM
-- Server version: 5.0.37
-- PHP Version: 4.4.4
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
--
-- Database: `cms`
--
-- --------------------------------------------------------
--
-- Table structure for table `groups`
--
CREATE TABLE `groups` (
`id` smallint(5) unsigned NOT NULL auto_increment,
`name` varchar(50) collate utf8_bin default NULL,
`description` text collate utf8_bin,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin AUTO_INCREMENT=10 ;
-- --------------------------------------------------------
--
-- Table structure for table `groups_users`
--
CREATE TABLE `groups_users` (
`id` int(10) unsigned NOT NULL auto_increment,
`group_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `group_id` (`group_id`,`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='Group User HABTM Relational Table' AUTO_INCREMENT=51 ;
-- --------------------------------------------------------
--
-- Table structure for table `users`
--
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL auto_increment,
`username` varchar(250) collate utf8_bin NOT NULL default '',
`password` varchar(250) collate utf8_bin NOT NULL default '',
`active` tinyint(1) unsigned NOT NULL default '1',
`created` datetime default NULL,
`modified` datetime default NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='User Data Information' AUTO_INCREMENT=17 ;
The first thing we need to address is the parent_node function in the Groups table. For the Groups table we don't want groups to parent to anything but the root so we have this return it as null in the parentNode function, which is called by the ACLBehavior. More on this in a minute. We also set the HABTM Relationship here, with the minimal assignments. There are no special finderQuerys needed for this example.
class Group extends AppModel {
var $name = 'Group';
var $actsAs = array('Acl'=>'requester');
var $hasAndBelongsToMany = array(
'User' => array(
'className' => 'User',
'joinTable' => 'groups_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'group_id',
'uniq' => true,
)
);
function parentNode()
{
return null;
}
}
?>
Next we address the Users Model. Here, because of the HABTM relationship to Groups, we have to adjust the parentNode in the model. I have left out the validation aspects for brevities sake. As you can see here, this model $actsAs CustomACL. The parenNode has also been restructured to parse the multiple groups assigned to the User data. This is all pretty straight forward.
class User extends AppModel {
var $name = 'User';
var $actsAs = array( 'CustomAcl'=>'requester' );
var $displayField = 'username';
var $hasAndBelongsToMany = array(
'Group' =>
array(
'className' => 'Group',
'joinTable' => 'groups_users',
'foreignKey' => 'group_id',
'associationForeignKey' => 'user_id',
'uniq' => true,
)
);
function checkUnique($data, $fieldName) {
$valid = false;
if(isset($fieldName) && $this->hasField($fieldName))
{
$valid = $this->isUnique(array($fieldName => $data));
}
return $valid;
}
function parentNode() {
if (!$this->id) {
return null;
}
$data = $this->read();
$aroArray = array();
if (!$data['Group']){
return null;
} else {
foreach($data['Group'] as $key)
{
$aroArray[] = array('model' => 'Group', 'foreign_key' => $key['id']);
}
return $aroArray;
}
}
function validLogin($data)
{
$user = $this->find(array('username' => $data['User']['username'], 'password' => ($data['User']['password'])), array('id','username', 'password'));
if(!empty($user)){
$this->user = $user['User'];
return TRUE;
}
else
{
return FALSE;
}
}
}
?>
Now we go to work on the CustomAclBehavior. This is basically a direct copy of the AclBehavior from the Cake core library. There are2 main differences between this behavior, and the core AclBehavior.
- afterSave(&$model, $created)
- beforeDelete(&$model)
Restructured to parse multi-dimensional parents Array passed from parentNode.Also restructured code to allow for HABTM associations in the {$type} to be erased before assigning new associations. Without this, the old ARO's were not being erased, and simply adding to the ARO Tree.Alias has been set now. In the original AclBehavior, the alias was not being set. I have assigned it here based upon $displayField so as to be reuseable by other models if need be. This also means that you should be sure to set your models displayField implicitly to ensure that the correct alias is set. This was changed from afterDelete(&$model) to be able to access the model data so as to remove multiple group assignments in the ACO/ARO as the case may be.
class CustomAclBehavior extends ModelBehavior {
/**
* Maps ACL type options to ACL models
*
* @var array
* @access protected
*/
var $__typeMaps = array('requester' => 'Aro', 'controlled' => 'Aco');
/**
* 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 (is_string($config)) {
$config = array('type' => $config);
}
$this->settings[$model->name] = am(array('type' => 'requester'), $config);
$type = $this->__typeMaps[$this->settings[$model->name]['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, $ref = null) {
$type = $this->__typeMaps[low($this->settings[$model->name]['type'])];
if (empty($ref)) {
$ref = array('model' => $model->name, 'foreign_key' => $model->id);
}
return $model->{$type}->node($ref);
}
/**
* Creates new ARO/ACO nodes bound to this record
*
* @param boolean $created True if this is a new record
*/
function afterSave(&$model, $created) {
$type = $this->__typeMaps[low($this->settings[$model->name]['type'])];
$parents = $model->parentNode();
$data = $model->read();
$parentArray = array();
if (!empty($parents)) {
foreach ($parents as $parent)
{
$parentArray[] = $this->node($model, $parent);
}
} else {
$parent = null;
}
$nodes = $this->node($model);
if (!empty($nodes)) {
foreach ($nodes as $node)
{
if($model->{$type}->deleteAll(array('alias' => $data[$model->name][$model->displayField])))
{
$return = TRUE;
}
}
}
foreach ($parentArray as $parent)
{
$model->{$type}->create();
$model->{$type}->save(array(
'parent_id' => Set::extract($parent, "0.{$type}.id"),
'model' => $model->name,
'foreign_key' => $model->id,
'alias' => $data[$model->name][$model->displayField]
));
}
}
/**
* Destroys the ARO/ACO nodes bound to the deleted record
*
*/
function beforeDelete(&$model) {
$data = $model->read();
$return = FALSE;
$type = $this->__typeMaps[low($this->settings[$model->name]['type'])];
$nodes = $this->node($model);
if (!empty($nodes)) {
foreach ($nodes as $node)
{
if($model->{$type}->deleteAll(array('alias' => $data[$model->name][$model->displayField])))
{
$return = TRUE;
}
}
}
return $return;
}
}
?>
And that's all there is to it. Combined with the previous post I had set, and Brian's post This has become a seriously extensible ACL/AUTH System. Any questions don't be afraid to ask.


FUZZI Said:
It would be really great if someone could setup a working live example of this and put the whole source code online.