CakePHP and Acl - Why is it so difficult?

Posted by Felix Geisendörfer, on May 31, 2006 - in PHP & CakePHP » Auth, Acl & Permissions

Deprecated post

The authors of this post have marked it as deprecated. This means the information displayed is most likely outdated, inaccurate, boring or a combination of all three.

Policy: We never delete deprecated posts, but they are not listed in our categories or show up in the search anymore.

Comments: You can continue to leave comments on this post, but please consult Google or our search first if you want to get an answer ; ).

CakePHP is great, no question about it. And so is Acl. The first time you start to understand the basic concepts of Acl you feel like "Wow, that is really the way to go for right management". But as soon as you start to dig deeper you'll find out that it's not as easy as most of the documentation that is available try to make you believe. There are many reasons for this, and I'll try to point them out to you, so your Acl experience won't become a nightmare like it has been for many before.

Reason #1 - Lag of documentation

For something as important and often needed as Acl there is really a huge gap when it comes to documentation. The two main sources for me have been the Wiki and the Manual chapter about Acl but besides the fact that the information on those pages are rare there is another issue with them, which brings me to the next point:

Reason #2 - Wrong, Confusing & Outdated Information

If you try to figure out something as complex as Acl, it can really cost you hours to figure out why something doesn't work, if the examples you are trying to do are simply wrong or incomplete. Let me show you a couple of examples:

Example 1 - Accessing Aro/Aco Object

No matter where I've looked so far, I've always seen the Aro or Aco object used like this:

php
  1. $aro = new Aro();
  2. $aro->create( $user_id, $parent_id, $alias );

But (almost) nobody tells you what those objects actually are - Models! And how do we use Models in CakePHP? We put them to our $uses array!

php
  1. class AppController extends AppModel
  2. {
  3.     var $components = array('Acl');
  4.     var $uses = array('Aro', 'Aco');
  5.  
  6.     function beforeFilter()
  7.     {
  8.         $this->Aro->create( $user_id, $parent_id, $alias );
  9.     }
  10. }

Makes more sense doesn't it? I think following the CakePHP conventions on this one should be a good idea, because that way people will instantly see that we deal with custom Models here and not with some totally unknown objects.

Example 2 - The user_id field / Accessing Aros / Acos

One of the first problems you'll run into when working with Acl is that you need to be able to have a unique id for all the Items in your trees. Well If you just have users as Aros and they are only one level in depth, things are easy - usernames are unique and therefor good enough for identifying each element/aro. But what if you want to setup more complex systems that include users, groups, and possibly other Aros? Then each of them needs to have a unique alias. One idea is to use a prefix for different types like "User.John" or "Group.Admins", but I would much rather have a tree that is clean of such things and identify my items via unique id's so there won't ever be any problems with duplicated aliases. Well the wiki has one solution for that but you should easily see why it's just wrong:

php
  1. $aro = new Aro();
  2. $aro->create($aro->findCount()+1, null, 'User.' . $this->User->getLastInsertID());

The thing that can't work is the $aro->findCount()+1 statement to create the user_id for the aro. Because if you have 7 aro's and you delete number 3 this function will create a new aro with the id 7 which makes you loose your unique id's. So the only way to create unique user_id's is this one (using Aro as a Controller Model this time, as shown above):

php
  1. $this->Aro->create(0, null, 'User.' . $this->User->getLastInsertID());
  2. $this->Aro->save(array('id' => $this->Aro->id, 'user_id' => $this->Aro->id));

This approach allows you to always keep the id and user_id field in your aro table in sync, making it easy to identify *any* element just by saving the aco_id ($this->Aco->id) to a field in the table of item's you want to be aro's (like users). And then you don't even need to use an ugly prefix like "User.23" as an alias in your tree, but can directly set it to "Jim", "Jack", or "Johnny".

Example 3 - Outdated stuff & Confusion

There are information spread out everywhere that are not up to date any more. Let me show you an Example from the Wiki:

Question (Olle): What do the fields lft and rght mean in the ARO table? /cake/controller/components/dbacl/models/aclnode.php refers to rght a lot. And the controller uses an “order by lft ASC”. Answer (ChrisPart): The lft and rght fields are for MPTT (Modified Pre-Order Tree Traversal), a method used to hold the tree structure of the permissions.

Question (Olle): Is the database field aros.user_id for integrating dbAcl with my other database tables holding user information? Answer (Chris Partridge): That field is used to identify the record in the DB, however there is no support for specifying the model which the id belongs to, so it’s best to set an alias (3rd parameter passed to create()) for each. I am using the following alias format, to easily identify records: $ModelName.$id. For example: User.27

Olle mentions the missing feature of specifying a model certain data belongs to by saying: "however there is no support for specifying the model which the id belongs to", but if you take the db_acl.sql from the latest nightly you'll see there actually *is* a model field in the table. Well nice, but how do I make use of that? As mentioned before Aro/Aco objects are Models, so you propably have to manually execute save() and find() statements in order to identify Aro/Aco elements by using the Model field. Which again, puts you in the need of having either a unique user_id or alias in your table (because all the Acl functions only take those to identify items). And since I already explained why I think it's a bad idea to force aliases to be unique, you need to have unique user_id's. Which brings me to my next Reason for Acl to be difficult:

Reason #3 - Why, oh why do we need the user_id field?

I think the user_id field is the worst part when working with acl. It's a numeric field that's ment to be unique across multiple Aro types like Users, Groups, etc., but it *does not* auto-increment (same goes for Acos with 'object_id')! So we have to figure out our own way to make it unique, which leads to more confusion. Why can't we just simple use the id field of the aro/aco table to identify elements? Would'nt the sun shine much brighter for all of use if the Aro::create() function would return the id of the Aro that was created so we could simply store it in our other models as the aro_id? Just look how much easier things would be:

php
  1. // $parent_id = Id of the Aro "Users"
  2.  
  3. $user = array('name' => 'John', 'pw' => 'secret-hash');
  4. $user['aro_id'] = $this->Aro->create($parent_id, $user['name']);
  5. $this->User->save($user);

This is how I feel Acl should be implemented, since it's just *way* easier for people to work out, and doesn't limit you in any ways. Using auto_incrementing id fields you could also use a combination of alias & model to identify users / groups without knowing their aco_id. I'll leave this idea for discussion since it's quite possible that I might be wrong on this since I'm no experienced Acl user, but in case people agree I'll open a RFC / Enhancment ticket on Trac for this. Meanwhile you can use the workaround I posted above if you want to go with this approach.

Reason #4 - MPTT What!??

One of the things that makes Acl very inaccesible to many of us is the fact that the AclTree's are stored with a method known as Modified Preorder Tree Traversal (short MPTT). While I don't want to go into great detail about how MPTT works I just want to point out why it's used. The advantage of trees stored in MPTT format is that you can retrieve (and build) the tree a lot faster then you can with the other method of using a parent_id. The only disadvantage is, that it takes longer to modify the tree (to delete or add nodes), but that's no issue for right management. The best resource about MPTT that I know of is over at sitepoint in their article called: Storing Hierarchical Data in a Database. If you look at the article you'll quickly be like "rght, lft, WHAT!?!?" and your enthusiasm about Acl will be gone before you've even started to get really into it. And as of right now, you'll not be able to work with Acl without having a certain understanding about the MPTT way of storing tree data.

Reason #5 - The incompletness of scripts/acl.php

Imagine you are new to this entire Acl thing, you have a project to finish, and all the stuff mentioned above is already making your life hard enough. All you want is a simple tool to play around & debug your Acl trees. Well, there is such a tool for the command line inside the cake/scripts/acl.php and for certain things it works like a charm. For others, it simply doesn't. Just 2 examples that'll ruin it for you right away:

Example #1 - View your Aco/Aro tree

You just added your first 4-5 elements to your Aro tree and now you want to see if the tree is stored in the db as you hope it would be. Well easy: Fire up the command line, switch to your /cake/scripts/ directory and type in "php.exe acl.php view aro" (windows). Voila, you see the Aco tree like you've created it. Hmm, but what are those numbers for? Ah right! We specified an $user_id for the Aro's we created, that's what it must be! Well ... Wrong! It's not the id you specified when you called $aro->create($user_id, $parent_id, $alias);, but it is the auto-incrementing primary key of your aros table. It's so easy to get this thing wrong in the beginning (especially if both numbers are the same for user 1,2,3) and it can cost you hours to figure out. I can't say the acl.php is incomplete on this one, but the documentation for it is. It's also questionable why the primary key id get's displayed when it *can not* (directly) be used to identify the elements when working with the Acl component. Another reason why I think user_id and object_id need to go.

Example #2 - Delete doesn't work

Well let's expect you've not figured out that the id's displayed by the "aro view" can not be used for identifying elements and you want to try to delete an item from your tree using one of those id's (which is what I went through). You type in "php.exe acl.php delete aro 2" and hit enter. An HTTP header get's returned and no error message shows up, hmm, did it work? Back to "aro view" - the item is still there. Must have been the wrong id, try it again, back to "aro view" - item is still there! What!? Alright you don't expect the delete function to be undone yet, because it would throw an error in that case (right?), so you try to figure out what *you* did wrong. Well it will take you a while to figure out that both is the case. delete() is undone, and you are wrong about the id's display by "aro view" ... What a mess : /.

Summing everything up

Why did I write this article? I wrote this article to point out a couple things that I struggled with when I started working with Acl a couple of days ago (including some earlier experiments with it as well), so people who still got their first Acl experience ahead of them don't have to go through the same problems. However, I'm sure there are still more then those I mentioned. I already see a dev asking why I've not opened tickets for all of this stuff so far and the answer is: "I will open them, but when you get into Acl for the first time, things are just so confusing that you really start to question yourself and get to the point where you think you don't know anything about programming. So give me some more days until I'm sure the stuff I've found is really worth a couple of tickets and you'll see them on Trac".

As said, I will use the next couple of days to figure out what tickets need to be opened, and to see if my idea of removing user_id and object_id is a product of my ignorance torwards the concept of Acl or worth an enhancement ticket. After that I'll do a proof of concept of my idea on how to integrate Acl into my SpliceIt! project, and if this works out I'll start a series of 2-3 articles where I'll explain everything I've learned about Acl together with a (small) real world example. I'll also release some source code that will help people to work with MPTT trees, allowing them to convert them into nested arrays and such. Meanwhile I would appriciate every comment on the stuff I've talked about above so I don't go off running into the wrong direction ; ).

--Felix

PS: I know I've been sarcastic more then once in the article above, but it's not ment to offend anybody in the community or from the core dev's. I simply tried to express how I felt when working with Acl for the first time.