Auth and ACL an end to end tutorial pt. 1

Now, there are many tutorials out there for Auth and ACL in CakePHP. However, none of them (as far as I know of) cover putting together Auth and ACL from beginning to end. That is what I’m going to do in this article. I am splitting the article into two parts; the first about setting up our app and getting the Aros running, the second on building Acos and putting it all together.

With that said, this article assumes you know the basics of CakePHP, and are familiar with all of the MVC core concepts. You are also comfortable using bake and the cake console. If not, learn those concepts first, and then come back. After learning these concepts everything else will make more sense. This tutorial will also use Auth in actions mode, this authenticates our ARO – groups and users – against the ACO objects – controllers & actions.

Tabula rasa

I am going to start with a blank install of cake that has just been checked out / unpacked, and has had database.php config file setup and the Security.salt value changed to remove the warning. If you have an app in progress that should work as well, but you will have to make adjustments for the user, and group models accordingly.

Schema-ing

First things first, we need to make some tables to base our application off of. We are going to create tables for users, groups, posts and widgets. This will give us a few things we can try our ACL system against.

Show Plain Text
  1. CREATE TABLE users (
  2.     id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  3.     username VARCHAR(255) NOT NULL UNIQUE,
  4.     password CHAR(40) NOT NULL,
  5.     group_id INT(11) NOT NULL,
  6.     created DATETIME,
  7.     modified DATETIME
  8. );
  9.  
  10. CREATE TABLE groups (
  11.     id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  12.     name VARCHAR(100) NOT NULL,
  13.     created DATETIME,
  14.     modified DATETIME
  15. );
  16.  
  17. CREATE TABLE posts (
  18.     id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  19.     user_id INT(11) NOT NULL,
  20.     title VARCHAR(255) NOT NULL,
  21.     body TEXT,
  22.     created DATETIME,
  23.     modified DATETIME
  24. );
  25.  
  26. CREATE TABLE widgets (
  27.     id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
  28.     name VARCHAR(100) NOT NULL,
  29.     part_no VARCHAR(12),
  30.     quantity INT(11)
  31. );

These are the tables I used to build the rest of this article, so get them into your database if you are playing at home. Once we have the table structure in the db we can start cooking. Bake up the models, controllers, and views. Leave out admin routing for now, this is a complicated enough subject without them. Also be sure not to add either the Acl or Auth Components to any of your controllers, we’ll be doing that soon enough. You should now have models, controller, and baked views for your users, groups, posts and widgets.

Preparing the application for Auth and Acl

We now have a functioning CRUD application. Bake should have setup all the relations we need, if not do that now. There are a few other pieces that need to be added before we can add components. First add a login and logout action to your UsersController.

Show Plain Text
  1. function login() {
  2.     //Auth Magic
  3. }
  4.  
  5. function logout() {
  6.     //Auth Magic
  7. }

We don’t need to worry about adding anything to hash passwords, as Auth will do this for us automatically when creating/editing users, and when they login. Furthermore, if you hash the passwords manually Auth will not be able to log you in at all.

AppController

Next we need to make some modifications to AppController. If you haven’t made one in app/ do so now. Since we want our entire site controlled with Auth and Acl, we will set them up in our AppController. Add the following to AppController:

Show Plain Text
  1. var $components = array('Auth', 'Acl');
  2.  
  3. function beforeFilter() {
  4.     //Configure AuthComponent
  5.     $this->Auth->authorize = 'actions';
  6.     $this->Auth->loginAction = array('controller' => 'users', 'action' => 'login');
  7.     $this->Auth->logoutRedirect = array('controller' => 'users', 'action' => 'login');
  8.     $this->Auth->loginRedirect = array('controller' => 'posts', 'action' => 'add');
  9. }

We also need to add a few temporary overrides to the groups and users controllers. Add the following to both the users and groups controllers.

Show Plain Text
  1. function beforeFilter() {
  2.     parent::beforeFilter();
  3.     $this->Auth->allowedActions = array('*');
  4. }

These overrides shut off the Auth component so we can actually get some data in our tables. If we left this out, Auth would do its job and lock us out. Not very useful.

Initialize the Acl

Before we can start using the Acl we need to initialize it. If you try to view any pages right now, you will get a missing table error, so lets get rid of that. In a shell run the following cake schema run create DbAcl and follow the prompts to drop and create the tables.

With the controllers setup for data entry, and the Acl tables initialized we are ready to go right? Not entirely, we still have a bit of work to do in the user and group models. Namely, making them auto-magically attach to the Acl.

Acts As a Requester

Auth stores information about the currently logged in user in the Session, however we are building an Access Control System based on groups. Therefore, we need to associate our users to our groups in the Acl. In order to do this we can use the AclBehavior. The AclBehavior allows for the automagic connection of models with the Acl tables. Its use requires an implementation of parentNode() on your model. In our User model we will add the following.

Show Plain Text
  1. var $actsAs = array('Acl' => array('requester'));
  2.  
  3. function parentNode() {
  4.     if (!$this->id && empty($this->data)) {
  5.         return null;
  6.     }
  7.     $data = $this->data;
  8.     if (empty($this->data)) {
  9.         $data = $this->read();
  10.     }
  11.     if (!$data['User']['group_id']) {
  12.         return null;
  13.     } else {
  14.         return array('Group' => array('id' => $data['User']['group_id']));
  15.     }
  16. }

Then in our Group Model Add the following:

Show Plain Text
  1. var $actsAs = array('Acl' => array('requester'));
  2.  
  3. function parentNode() {
  4.     return null;
  5. }

What this does, is tie the Group and User models to the Acl, and tell CakePHP that every-time you make a User or Group you want an entry on the aros table as well. This makes Acl management a piece of cake as your AROs become transparently tied to your users and groups tables.

So now our controllers are prepped for adding some initial data, and our Group and User models are bound to the Acl table. So add some groups and users. I made the following groups:

  • administrators
  • managers
  • users

I also created a user in each group so I had some different users to test with later. Write everything down or use easy passwords so you don’t forget. If you do a SELECT * FROM aros; from a mysql prompt you should get something like the following:

Show Plain Text
  1. +----+-----------+-------+-------------+-------+------+------+
  2. | id | parent_id | model | foreign_key | alias | lft  | rght |
  3. +----+-----------+-------+-------------+-------+------+------+
  4. |  1 |      NULL | Group |           1 | NULL  |    1 |    4 |
  5. |  2 |      NULL | Group |           2 | NULL  |    5 |    8 |
  6. |  3 |      NULL | Group |           3 | NULL  |    9 |   12 |
  7. |  4 |         1 | User  |           1 | NULL  |    2 |    3 |
  8. |  5 |         2 | User  |           2 | NULL  |    6 |    7 |
  9. |  6 |         3 | User  |           3 | NULL  |   10 |   11 |
  10. +----+-----------+-------+-------------+-------+------+------+
  11. 6 rows in set (0.00 sec)

This shows us that we have 3 groups and 3 users. The users are nested inside the groups, which opens up the possibility of setting per-user permissions.

One thing I noticed when making this tutorial, is that if you modify a User’s group it does not modify their Aro. You have to do that separately it seems.

So that concludes the first part of this series. We now have a users/groups system bound for the most part to the Acl (see above). Up next getting our controllers and actions into the acos table and getting everything to jive together. Also, at the end of this process I will be making all the files I used to build this tutorial with all the SQL dumps available for your perusal.

Update: Part two is now up
Update: Corrected Acl initialization shell command. Thanks Jason. Also simplified Users::parentNode() function.

Comments

Another good looking and informative post Mark. I’ve recently stepped into CakePHPs ACL / Auth components and really look forward to the next parts of this tutorial.

Definitely an improvement over other tutorials that only show the how and don’t explain the why :)

anonymous user on 7/7/08

Great first article in the series, ACL is a tough subject to grasp and explain thoroughly. So far you’ve done good.

anonymous user on 7/7/08

Very nice tutorial, but I have a question: What’s the difference or advantage on using an extra table for groups and not defining the groups as aro’s or aco’s, for exampe as stated in the official cakephp book?

anonymous user on 7/8/08

@Juan Luis Baptiste: The advantage comes from being able to create drop downs when creating users, and other interface elements. Furthermore, it makes it easier for others to understand the user hierarchy of the site, instead of having to guess at ARO associations.

mark story on 7/8/08

This might be a good candidate for the Common Tasks with CakePHP at book.cakephp.org. I know there are detailed explanations of ACL and Authentication in the Components section, but a more tutorial-style approach would be good as well.

anonymous user on 7/9/08

This might be a good candidate for the Common Tasks with CakePHP at book.cakephp.org. I know there are detailed explanations of ACL and Authentication in the Components section, but a more tutorial-style approach would be good as well.

anonymous user on 7/9/08

There’s a slight typo in your code:

var $actsAs = array('requester');

in the User model should read:

var $actsAs = array('Acl' => array('requester'));

Good stuff Mark. Keep em’ coming.

anonymous user on 7/9/08

@Joel: thanks for the find, its been fixed it up.

@Brian: I have every intention of posting it to either the book.cakephp.org or bakery.cakephp.org once the tutorial done.

mark story on 7/10/08

@Mark: thank you for your answer. Another question, when do you think you are going to publish the second part of the article ?

anonymous user on 7/11/08

Great stuff, thanks! It’s coming up with “Fatal error: Call to undefined method stdClass::node() in C:\wamp\www\cake\app\models\user.php on line 19” every time I add a user. That’s this line of the user model “$groupNode = $this->Group->node();”. Any ideas? Thanks!

anonymous user on 7/16/08

Never mind! Just needed to sort my hasMany’s and belongsTo’s out.

anonymous user on 7/16/08

Mark, great article; thanks for putting it together. As I was following along I noticed that the shell command cake acl initdb is deprecated for 1.2. The Cookbook has the new command.

anonymous user on 8/1/08

Hi Mark
Great tutorial it’s really got me to understand how I should use ACL.

The reason why the ARO isn’t updated when modifying a user is in the afterSave method of the AclBehavior.

Nothing happens when you save.

	function afterSave(&$model, $created) {
		if ($created) {
			$type = $this->__typeMaps[strtolower($this->settings[$model->alias]['type'])];
			$parent = $model->parentNode();
			if (!empty($parent)) {
				$parent = $this->node($model, $parent);
			} else {
				$parent = null;
			}

			$model->{$type}->create();
			$model->{$type}->save(array(
				'parent_id'		=> Set::extract($parent, "0.{$type}.id"),
				'model'			=> $model->alias,
				'foreign_key'	=> $model->id
			));
		}
	}

I've been trying to fix this all day but when I save instead of updating a new record in the aro table is created.

Maybe you have a nice solution :)

anonymous user on 9/4/08

Well, with some time of from the problem. I think I’ve come up with a solution.

	function afterSave(&$model, $created) {
		$type = $this->__typeMaps[strtolower($this->settings[$model->name]['type'])];
		$parent = $model->parentNode();

		if (!empty($parent)) {
			$parent = $this->node($model, $parent);
		} else {
			$parent = null;
		}

		$data = array(
			'parent_id'		=> Set::extract($parent, "0.{$type}.id"),
			'model'			=> $model->name,
			'foreign_key'	=> $model->id
		);
		if ($created) {
			$model->{$type}->create();
		} else {
			$data = am($data, array('id' => Set::extract($this->node($model), "0.{$type}.id")));
		}

		$model->{$type}->save($data);
	}

anonymous user on 9/6/08

When I get to the bake part, telling me to create the models, views, and controllers for the 4 things, I understand that. But bake asks all kinds of questions, like if I want scaffolding. What should I put? Also, am I doing it right if I have to do “m” 4 times, “v” 4 times, and “c” 4 times? Just doesn’t seem too automagic when so much input is required, so I thought I may be doing it wrong.

anonymous user on 10/15/08

beatles: There is a cake bake all function as well, but I don’t think that bake is really that much work considering how much typing time it saves. You can use scaffolds but I prefer to not use them.

mark story on 10/18/08

Thanks for the tutorial! it helps a lot…

jpablobr on 5/30/09

Warning (512): DbAcl::check() – Failed ARO/ACO node lookup in permissions check. Node references:
Aro: Array
( [User] => Array ( [id] => 1 [username] => admin [group_id] => 1 )

)

Aco: Pages/display [CORE\cake\libs\controller\components\acl.php, line 239]

Code | Context

$aro = array( “User” => array( “id” => “1”, “username” => “admin”, “group_id” => “1”
)
)
$aco = “Pages/display”
$action = “*”
$permKeys = array( “_create”, “_read”, “_update”, “_delete”
)
$aroPath = array( array( “Aro” => array()
), array( “Aro” => array()
)
)
$acoPath = false

if (empty($aroPath) || empty($acoPath)) { trigger_error(“DbAcl::check() – Failed ARO/ACO node lookup in permissions check. Node references:\nAro: “ . print_r($aro, true) . “\nAco: “ . print_r($aco, true), E_USER_WARNING);

DbAcl::check() – CORE\cake\libs\controller\components\acl.php, line 239
AclComponent::check() – CORE\cake\libs\controller\components\acl.php, line 89
AuthComponent::isAuthorized() – CORE\cake\libs\controller\components\auth.php, line 477
AuthComponent::startup() – CORE\cake\libs\controller\components\auth.php, line 399
Component::startup() – CORE\cake\libs\controller\component.php, line 112
Dispatcher::_invoke() – CORE\cake\dispatcher.php, line 210
Dispatcher::dispatch() – CORE\cake\dispatcher.php, line 194
[main] – APP\webroot\index.php, line 88

Warning (2): Cannot modify header information – headers already sent by (output started at C:\Programme\xampp\htdocs\cake\cake\basics.php:111) [CORE\cake\libs\controller\controller.php, line 640]

Code | Context

$status = “Location: http://localhost/”

header – [internal], line ??
Controller::header() – CORE\cake\libs\controller\controller.php, line 640
Controller::redirect() – CORE\cake\libs\controller\controller.php, line 621
AuthComponent::startup() – CORE\cake\libs\controller\components\auth.php, line 404
Component::startup() – CORE\cake\libs\controller\component.php, line 112
Dispatcher::_invoke() – CORE\cake\dispatcher.php, line 210
Dispatcher::dispatch() – CORE\cake\dispatcher.php, line 194
[main] – APP\webroot\index.php, line 88

Madi on 6/13/09

OOOps! Sorry it didn’t copy all the message! Anyway, that is my problem! Thanks for your help!

Madi on 6/13/09

on 6/30/09

Comments are not open at this time.