CakePHP 1.2: Using Auth with ACL

A project we’re working on needed something to keep track of user activity and restrict parts of the application to certain users/groups. We are using CakePHP 1.2 and decided to use its built-in support for user authentication and access control list. Though each of them is nicely documented at the Cookbook, there wasn’t much info or resource that shows how to put these two components to work together. Fortunately, we have found some nice tutorials/articles that helped us implement them into our application. Check out those links at the end.

The Setup

We want to restrict certain actions to certain types of users. But instead of assigning permissions to each user, we just divide users into groups and assign permission to groups. Users under the same group have the same set of permission.

For this example, we’ll be dividing users into groups namely: admin, editor and member.

Setting up the database tables

Users Table:

CREATE TABLE `users` (     

	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
	`username` VARCHAR( 20 ) NOT NULL ,
	`password` VARCHAR( 50 ) NOT NULL ,
	`group_id` TINYINT( 2 ) NOT NULL DEFAULT '0',
	PRIMARY KEY ( `id` )) ENGINE = MYISAM ;

Groups Table:

CREATE TABLE `groups` (

	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
	`parent_id` INT UNSIGNED NOT NULL DEFAULT '0',
	`name` VARCHAR( 20 ) NOT NULL ,
	PRIMARY KEY ( `id` )
) ENGINE = MYISAM ;

ACL Tables:
The sql dump for the acl table can also be found at the app/config/sql folder of your application

CREATE TABLE acos (

     id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	parent_id INTEGER(10) DEFAULT NULL,
	model VARCHAR(255) DEFAULT '',
	foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
	alias VARCHAR(255) DEFAULT '',
	lft INTEGER(10) DEFAULT NULL,
	rght INTEGER(10) DEFAULT NULL,
	PRIMARY KEY  (id));

CREATE TABLE aros_acos (
	id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	aro_id INTEGER(10) UNSIGNED NOT NULL,
	aco_id INTEGER(10) UNSIGNED NOT NULL,
	_create CHAR(2) NOT NULL DEFAULT 0,
	_read CHAR(2) NOT NULL DEFAULT 0,
	_update CHAR(2) NOT NULL DEFAULT 0,
	_delete CHAR(2) NOT NULL DEFAULT 0,
	PRIMARY KEY(id)
);

CREATE TABLE aros (
	id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	parent_id INTEGER(10) DEFAULT NULL,
	model VARCHAR(255) DEFAULT '',
	foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
	alias VARCHAR(255) DEFAULT '',
	lft INTEGER(10) DEFAULT NULL,
	rght INTEGER(10) DEFAULT NULL,
	PRIMARY KEY  (id)

);

Setting up the models

After setting up the tables, we now move on to creating the models for our CakePHP application:

User model:

uses('Sanitize');
class User extends AppModel {
	var $name = 'User';

	// Set the User model to use the ACL Behavior
	// This will take care of ACL-related things when working on
	// this model like adding/deleting the corresponding ARO node
	// when a user is added/deleted.
	var $actsAs = array('Acl');

	// Associate with the Group table
	var $belongsTo = array('Group');	

	function parentNode(){
		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']);
		}
	}

	// Ok, even if the ACL behavior takes care of the insertion of the
	// corresponding ARO node, it doesn't save an alias so you have to
	// give one yourself. We'll be using the username for the alias.
	// We'll do this after a new user is saved/inserted, so do it inside
	// the model's afterSave function
  	function afterSave($created) {

		// Do this if the save operation was an insertion/record creation
		// and not an update operation
  		if($created) {
  			// Ah, yes... we'll be needing the Sanitize component
  			$sanitize = new Sanitize();	

			// Get the id of the inserted record
			$id = $this->getLastInsertID();

			// Instantiate an ARO model that will be used for updating
			// the ARO
			$aro = new Aro();

			// I'm using updateAll() instead of saveField()
			// Instead of querying the table to get the id of the
			// ARO node that corresponds to the user, I just provided
			// two field conditions whose combination uniquely identifies
			// the node (Model=> User, Foreign Key=> User id).

			// I don't know why it wasn't sanitizing my input and not
			// enclosing the input in quotes. I had to do it myself
			$aro->updateAll(
			array('alias'=>'\''.$sanitize->escape($this->data['User']['username']).'\''),
				array('Aro.model'=>'User', 'Aro.foreign_key'=>$id)
			);
		}
		return true;
	}
}

The line:

var $actsAs = array('Acl');

Sets the model to use CakePHP built-in ACL behavior. This will take care of ACL-related things when working on this model like adding/deleting the corresponding ARO node when a user is added/deleted.

When using the ACL Behavior, you need to provide a parentNode() method in your model. The parentNode() method returns the id of the parent of the current acl node. A tree structure is used to implement ACL, so you can either directly assign permission to a node or just allow a node to inherit the permissions of its parent.

I found out from Geoff’s blog (LemonCake) that the key to using groups for user permission is to set the user aros as children of the a group aro. You can do this inside the models parentNode() method:

Group Model:

// Okay, I won't be commenting on this since this almost the same
// as what you can see from the user model definition

uses('Sanitize');
class Group extends AppModel {
	var $name = 'Group';
	var $actsAs = array('Acl');

	function parentNode(){
    	    	if (!$this->id) {
      		    	return null;
    	    	}
    	    	$data = $this->read();

    	    	if (!$data['Group']['parent_id']){
    	      		return null;
    	    	} else {
    	      		return $data['Group']['parent_id'];
    	    	}
  	}

  	function afterSave($created) {
  		if($created) {
  			$sanitize = new Sanitize();	

			$id = $this->getLastInsertID();

			$aro = new Aro();

			$aro->updateAll(
				array('alias'=>'\''.$sanitize->escape($this->data['Group']['name']).'\''),
				array('Aro.model'=>'Group', 'Aro.foreign_key'=>$id)
				);
		}
		return true;
	}
}

Setting up the controllers

Okay now, let’s keep the code short. We won’t provide any user registration here, just a way of logging users in and out of the application.

users_controller.php:

class UsersController extends AppController {
	var $name = 'Users';
	// We will be using the Acl and Auth component
	// The order you declare the components is
	// important. Acl first before Auth or you
	// will get an error message.
	var $components = array('Acl', 'Auth');

	// This is how complicated the login part
	// gets when using the auth component.
	function login() {
	}

	// ... and the logout part
	function logout() {
		$this->redirect($this->Auth->logout());
	}

	function beforeFilter() {
		$this->Auth->logoutRedirect = array(
			'admin' => false,
			'controller' => 'pages',
			'action' => 'index'
		);
	}

}

Now that takes care of the user authentication. To use Auth w/ ACL in our controllers, we first need to include the components (of course!). Make sure that Acl goes before the Auth component or else, you’ll get an error message.

 var $components = array('Acl', 'Auth');

Then, we need to set the Auth component to treat the controller and actions as our access control objects. Inside the beforeFilter() method:

 function beforeFilter()
 {
 	$this->Auth->authorize = 'actions';
 }

Setting Up AROs, ACOs and Permissions:

Our AROs will be the users and groups. Since our user and group models already use the ACL behavior, we no longer have to worry creating AROs for them- they are already created when a new user or group is added. We only have to deal with creating ACOs. If you’ve already read the part of the manual about access control lists, you’ll find out that adding an ACO is just like saving data using models. You just need to specify the name of the controller/action as its alias and its parent id.

 	$aco = new Aco();
 	$aco->create();

 	$aco->save(array(
 	   'model'=>null,
 	   'foreign_key'=>null,
 	   'parent_id'=> $parentIdIfAny,
 	   'alias'=> $nameOfControllerOrAction
 	));

You might want to have a root node where all default permissions will be based upon. Then this node will contain child nodes, which will be our controller names. Then each controller will have action names as child nodes.

Controller
     |- Pages
     |    |- index
     |    |- about
     |    |- contact
     |- Articles
          |- index
          |- view
          |- add
          |- edit
          |- delete

To assign permissions, use the allow() and deny() methods of the ACL component. These methods takes at least 2 parameters: first is the ARO and second is the ACO. We could reference an ARO/ACO node using its alias or by providing an array of field conditions that uniquely describes the node.

$this->Acl->allow(array('model'=>'Group', 'foreign_key'=>$groupId), $nameOfControllerOrAction);

Since it is very likely that different controllers to have the same action names (thus making their alias non-unique and cause ambiguity), we can specify the path to the node using slashes (/).

$this->Acl->allow(array('model'=>'Group', 'foreign_key'=>$groupId), 'Pages/index');
$this->Acl->allow(array('model'=>'Group', 'foreign_key'=>$anotherGroupId), 'Articles/index');

You can deny all permissions on the root node (in this case, the ‘Controller’) to set default permissions that will be inherited by all the controllers. Then grant permission per controller or per action.

The Test Run:

I provided a simple app demo for download (acl_demo.zip).
1) Extract the contents of the zip file.
2) Run the sql file found inside the models folder
3) Edit the content (ROOT, APP_DIR and CAKE_CORE_INCLUDE_PATH paths) of index.php under the webroot folder according to you CakePHP setup
4) Access the index page of the app (ex: http://localhost/acl/) and click the setup button. Take note of the username and password of the accounts that will be created.
5) Login and test the application. Members page should only be accessible to members and admin. Editors page to editors and admin. Administrators page to administrator only.

Errors, corrections or suggestions? Feel free to leave a comment.

Sources:
http://manual.cakephp.org/view/171/access-control-lists
http://manual.cakephp.org/view/172/authentication
http://lemoncake.wordpress.com/2007/07/15/using-aclbehavior-in-cakephp-12/
http://lemoncake.wordpress.com/2007/07/19/acl-with-groups/
http://aranworld.com/article/161/cakephp-acl-tutorial-what-is-it

3 Responses to “CakePHP 1.2: Using Auth with ACL”

  1. matt Says:

    I was trying to download your acl demo, but the file comes up not found. Any chance you can update the link? Thanks.

  2. rolan Says:

    Hi Matt,

    I think we’re currently having problem with the uploaded files. But I updated the link with the file I uploaded on my homepage so you can check it out.

  3. links for 2008-08-14 « Richard@Home Says:

    [...] CakePHP 1.2: Using Auth with ACL A very thorough introduction to ACL with code examples. (tags: cakephp acl authentication tutorial auth) [...]

Add Comment

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>