I loved everything of CakePHP except one thing and that is ACL. Don’t know why, I never understood it very well. I tried/read lots of tutorials and always had to give up in the middle. One of the reasons why I gave up was, I always thought it was a matter of 30-40 minutes. I was wrong; if you are trying ACL for the first time it’s not a matter of that short time. You have to have enough time in your hand to understand it and work with it. But once you are done, you will find it pretty easy, trust me! So let’s start…
From the beginning to the end we are going to consider a smart scenario and it’s like the following…
You have a soccer team, so you have striker, midfielder, and defender right? As a team manager you are going to tell them like
Striker: You can come to defend, you can play in the midfield and you can of course play in striking position.
Midfielder: You can play as a striker and you can play in midfield but you NEVER can come to defend.
Defender: You ALWAYS have to be inside the D box!
So we got 3 players to consider for our example. Let’s revise them again. Striker who has all power, midfielder who has almost all power and defender who has only defending power from the manager.
Before we proceed further, let me download the latest CakePHP from here https://github.com/cakephp/cakephp/downloads
Ok, I have installed and placed it in my webroot directory. I named it “soccer”. Now what? Let’s create 2 tables once will be for groups and another will be to register users. As my project name is soccer, I will give my database name also soccer.
CREATE TABLE `groups` (
`id` SMALLINT( 3 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 30 ) NOT NULL ,
`created` DATETIME NULL ,
`modified` DATETIME NULL
) ENGINE = MYISAM ;
CREATE TABLE `USERS` (
`ID` INT(11) NOT NULL AUTO_INCREMENT,
`FIRST_NAME` VARCHAR(30) DEFAULT NULL,
`LAST_NAME` VARCHAR(30) DEFAULT NULL,
`EMAIL` VARCHAR(30) DEFAULT NULL,
`USERNAME` VARCHAR(30) NOT NULL,
`PASSWORD` VARCHAR(150) NOT NULL,
`CREATED` DATETIME DEFAULT NULL,
`GROUP_ID` SMALLINT(3) DEFAULT NULL,
PRIMARY KEY (`ID`),
KEY `USERNAME` (`USERNAME`)
) ENGINE=MYISAM DEFAULT CHARSET=LATIN1 AUTO_INCREMENT=1 ;
Ok, are these 2 tables enough for our example? We need some other actions to check the access limitations right? So let’s create another table called “scores” where we will keep all scored of all players, make sense?
CREATE TABLE `scores` (
`id` SMALLINT( 4 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`user_id` INT( 11 ) NULL ,
`score` SMALLINT( 3 ) NULL
) ENGINE = MYISAM ;
Now, I’m going to create controllers/models for them by baking. Hopefully no explanation need, as I will upload all codes. Be right back after baking…
Great, baking is done. I have created 3 controllers and 3 models for each of the table. It’s time to create our Auth. Why? Because we need to create users. So let’s create login form and place the following 2 methods in users controller
function login() {
//Auth Magic
/*
* We don't need to put anything here .
* Auth will handle this functionality
*/
}
function logout() {
$this->redirect($this->Auth->logout());
}
I have created a app_controller under app/ directory to customize our controller instead of using any other defaults. So let’s add some components there.
class AppController extends Controller {
var $components = array('Acl', 'Session', 'Auth');
function beforeFilter() {
//Configure AuthComponent
$this->Auth->authorize = 'actions';
$this->Auth->loginAction = array('controller' => 'users', 'action' => 'login');
$this->Auth->logoutRedirect = array('controller' => 'users', 'action' => 'login');
$this->Auth->loginRedirect = array('controller' => 'users', 'action' => 'index');
}
}
At this stage if you run go to users/ in your browser it will show an error like
Error: Database table acos for model Aco was not found.
It’s because we have added ACL component, but we haven’t added few tables that are required for ACL. So lets add them in our database. I used baking to create those acl tables and exported the structure in /sql/acl.sql . You can simply import from there.
Now, if I refresh the page it says
Error: The view for UsersController::login() was not found.
So we have to create view file for login. Which we did, but wait, we haven’t added any users yet right? And to add users we need some exclusive permission. Add the followings to users and group controllers.
function beforeFilter() {
parent::beforeFilter();
$this->Auth->allow('*');
}
This will help us to create some groups and users doesn’t matter who we are….so let’s create groups and users. We will add some groups now, but For Auth and Acl to work properly we need to associate our users and groups to rows in the Acl tables. CakePHP manual explains it very well why we need add some AclBehavior. Have a look here before your proceed further.
I hope you read and understood the AclBehavior from the given link. No more delay, let’s add some groups and users now.
I have created 3 groups
1. Striker
2. Midfielder
3. Defender
3 users
1. Leonel Messi
2. David Beckham
3. Paolo Maldini
At this stage you can say we are 60% work done! Wow, was there anything too hard to understand? Rest 40% is even easier believe me. Let’s get into the final stage…
Remember, we created 3 ACL tables? We need to get little bit ideas about them.
1. Aros : It’s keeps the record of our groups. So we will tell this table what groups we have.
2. Acos : It keeps the record of what these users who stored in Aros table will do. So let’s say we will say Acos, we will ask you to keep score_record() in your stomach.
3. Aros_Acos : It keeps all permission for all players who can move where and who can see what or not.
Cool eh?
Now let’s give some manual entry to Aros table. We need to keep our all groups in this tables right.
INSERT INTO `aros` VALUES(1, NULL, 'Group', 1, NULL, 1, 6);
INSERT INTO `aros` VALUES(2, NULL, 'Group', 2, NULL, 7, 10);
INSERT INTO `aros` VALUES(3, NULL, 'Group', 3, NULL, 11, 14);
INSERT INTO `aros` VALUES(4, 1, 'User', 1, NULL, 2, 3);
INSERT INTO `aros` VALUES(5, 2, 'User', 2, NULL, 8, 9);
INSERT INTO `aros` VALUES(6, 3, 'User', 3, NULL, 12, 13);
INSERT INTO `aros` VALUES(10, 1, 'User', 4, NULL, 4, 5);
We are done with Aros here. Now time to move to Acos. We have to tell Acos table what all these users want to do right? There is magic for it. Who wants to find how many comtrollers/actions we have and we have to add them one by one? Hell no! Blindly copy the following codes…
/*
* block this method when you move your project to production
* or Live
*/
function build_acl() {
if (!Configure::read('debug')) {
return $this->_stop();
}
$log = array();
$aco = & $this->Acl->Aco;
$root = $aco->node('controllers');
if (!$root) {
$aco->create(array('parent_id' => null, 'model' => null, 'alias' => 'controllers'));
$root = $aco->save();
$root['Aco']['id'] = $aco->id;
$log[] = 'Created Aco node for controllers';
} else {
$root = $root[0];
}
App::import('Core', 'File');
$Controllers = Configure::listObjects('controller');
$appIndex = array_search('App', $Controllers);
if ($appIndex !== false) {
unset($Controllers[$appIndex]);
}
$baseMethods = get_class_methods('Controller');
$baseMethods[] = 'buildAcl';
$Plugins = $this->_getPluginControllerNames();
$Controllers = array_merge($Controllers, $Plugins);
// look at each controller in app/controllers
foreach ($Controllers as $ctrlName) {
$methods = $this->_getClassMethods($this->_getPluginControllerPath($ctrlName));
// Do all Plugins First
if ($this->_isPlugin($ctrlName)) {
$pluginNode = $aco->node('controllers/' . $this->_getPluginName($ctrlName));
if (!$pluginNode) {
$aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $this->_getPluginName($ctrlName)));
$pluginNode = $aco->save();
$pluginNode['Aco']['id'] = $aco->id;
$log[] = 'Created Aco node for ' . $this->_getPluginName($ctrlName) . ' Plugin';
}
}
// find / make controller node
$controllerNode = $aco->node('controllers/' . $ctrlName);
if (!$controllerNode) {
if ($this->_isPlugin($ctrlName)) {
$pluginNode = $aco->node('controllers/' . $this->_getPluginName($ctrlName));
$aco->create(array('parent_id' => $pluginNode['0']['Aco']['id'], 'model' => null, 'alias' => $this->_getPluginControllerName($ctrlName)));
$controllerNode = $aco->save();
$controllerNode['Aco']['id'] = $aco->id;
$log[] = 'Created Aco node for ' . $this->_getPluginControllerName($ctrlName) . ' ' . $this->_getPluginName($ctrlName) . ' Plugin Controller';
} else {
$aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $ctrlName));
$controllerNode = $aco->save();
$controllerNode['Aco']['id'] = $aco->id;
$log[] = 'Created Aco node for ' . $ctrlName;
}
} else {
$controllerNode = $controllerNode[0];
}
//clean the methods. to remove those in Controller and private actions.
foreach ($methods as $k => $method) {
if (strpos($method, '_', 0) === 0) {
unset($methods[$k]);
continue;
}
if (in_array($method, $baseMethods)) {
unset($methods[$k]);
continue;
}
$methodNode = $aco->node('controllers/' . $ctrlName . '/' . $method);
if (!$methodNode) {
$aco->create(array('parent_id' => $controllerNode['Aco']['id'], 'model' => null, 'alias' => $method));
$methodNode = $aco->save();
$log[] = 'Created Aco node for ' . $method;
}
}
}
if (count($log) > 0) {
debug($log);
}
}
function _getClassMethods($ctrlName = null) {
App::import('Controller', $ctrlName);
if (strlen(strstr($ctrlName, '.')) > 0) {
// plugin's controller
$num = strpos($ctrlName, '.');
$ctrlName = substr($ctrlName, $num + 1);
}
$ctrlclass = $ctrlName . 'Controller';
$methods = get_class_methods($ctrlclass);
// Add scaffold defaults if scaffolds are being used
$properties = get_class_vars($ctrlclass);
if (array_key_exists('scaffold', $properties)) {
if ($properties['scaffold'] == 'admin') {
$methods = array_merge($methods, array('admin_add', 'admin_edit', 'admin_index', 'admin_view', 'admin_delete'));
} else {
$methods = array_merge($methods, array('add', 'edit', 'index', 'view', 'delete'));
}
}
return $methods;
}
function _isPlugin($ctrlName = null) {
$arr = String::tokenize($ctrlName, '/');
if (count($arr) > 1) {
return true;
} else {
return false;
}
}
function _getPluginControllerPath($ctrlName = null) {
$arr = String::tokenize($ctrlName, '/');
if (count($arr) == 2) {
return $arr[0] . '.' . $arr[1];
} else {
return $arr[0];
}
}
function _getPluginName($ctrlName = null) {
$arr = String::tokenize($ctrlName, '/');
if (count($arr) == 2) {
return $arr[0];
} else {
return false;
}
}
function _getPluginControllerName($ctrlName = null) {
$arr = String::tokenize($ctrlName, '/');
if (count($arr) == 2) {
return $arr[1];
} else {
return false;
}
}
/**
* Get the names of the plugin controllers ...
*
* This function will get an array of the plugin controller names, and
* also makes sure the controllers are available for us to get the
* method names by doing an App::import for each plugin controller.
*
* @return array of plugin names.
*
*/
function _getPluginControllerNames() {
App::import('Core', 'File', 'Folder');
$paths = Configure::getInstance();
$folder = & new Folder();
$folder->cd(APP . 'plugins');
// Get the list of plugins
$Plugins = $folder->read();
$Plugins = $Plugins[0];
$arr = array();
// Loop through the plugins
foreach ($Plugins as $pluginName) {
// Change directory to the plugin
$didCD = $folder->cd(APP . 'plugins' . DS . $pluginName . DS . 'controllers');
// Get a list of the files that have a file name that ends
// with controller.php
$files = $folder->findRecursive('.*_controller\.php');
// Loop through the controllers we found in the plugins directory
foreach ($files as $fileName) {
// Get the base file name
$file = basename($fileName);
// Get the controller name
$file = Inflector::camelize(substr($file, 0, strlen($file) - strlen('_controller.php')));
if (!preg_match('/^' . Inflector::humanize($pluginName) . 'App/', $file)) {
if (!App::import('Controller', $pluginName . '.' . $file)) {
debug('Error importing ' . $file . ' for plugin ' . $pluginName);
} else {
/// Now prepend the Plugin name ...
// This is required to allow us to fetch the method names.
$arr[] = Inflector::humanize($pluginName) . "/" . $file;
}
}
}
}
return $arr;
}
Now paste them in yoru GROUP controller. Go to your browser and call it like this /groups/build_acl you will see some MAGIC. I won’t tell you more about it here, you will check it by yourself to see it ok? After running that action we are done for Acos. Ok wait, what if you add more actions/controllers after that? Yes, please run that action again to update your acos table.
Now we are on FINAL stage! Yea, we are almost done! We did all, except permission right? So aros_acos table is asking us to give some permission data. We can do it similar way we did it for Acos table. So let’s do it. Open your users controller and blindly paste the following code
function initDB() {
$group = & $this->User->Group;
//Allow Striker to everything
$group->id = 1;
$this->Acl->allow($group, 'controllers');
/* Allow midfielder add/edit/view scores and edit/view users.
* but they can't add users or group
*/
$group->id = 2;
$this->Acl->deny($group, 'controllers');
$this->Acl->allow($group, 'controllers/Scores/index');
$this->Acl->allow($group, 'controllers/Users/index');
$this->Acl->allow($group, 'controllers/Scores/add');
$this->Acl->allow($group, 'controllers/Scores/edit');
$this->Acl->allow($group, 'controllers/Scores/view');
$this->Acl->allow($group, 'controllers/Users/edit');
$this->Acl->allow($group, 'controllers/Users/view');
$this->Acl->allow($group, 'controllers/Users/logout');
/*
* Allow defender to view score and view users only
*/
$group->id = 3;
$this->Acl->deny($group, 'controllers');
$this->Acl->allow($group, 'controllers/Scores/index');
$this->Acl->allow($group, 'controllers/Users/index');
$this->Acl->allow($group, 'controllers/Scores/view');
$this->Acl->allow($group, 'controllers/Users/view');
$this->Acl->allow($group, 'controllers/Users/logout');
}
I have allowed striker to ALL access, midfielder to edit/view users and scores and defender to view only users and scores. Make sense?
Now what? We need to run this action like before. So open your browser again and call it like this /users/initDB ignore some missing view message but CHECK your acos_aros table. You will see some 1-1-1-1 added into the table. If you want change the permission update the initDB and run it again from your browser.
Are we done here? Yes, just one thing left; please take off the exclusive permission from both users controller and users controller as we have added our initial data. Now let’s Auth and ACL to handle it.
Let me check the permission by login all these 3 users.
Bingo! They are working perfectly!
That’s it!
We are done!
Here is the whole project you can download and simply upload in your webroot directory.
Then import database from /sql/soccer.sql change database settings and run users/login from the browser. You have 3 users login
Messi/messi
David/david
Maldini/maldini
It works very well; I hope it was easy enough. If you still don’t understand anything please don’t hesitate to leave a comment here. I will try my best to help.