User Permissions and CakePHP ACL

By Theshz (theshz)
This article shows how to use CakePHP's ACL to control user access to different parts of a website. It covers CakePHP 1.1.10.3825 (November, 2006)
One of the features prominently listed for CakePHP is the builtin ACL (Access Control List.) It seems to be a perfect fit for modeling user permissions for web applications. But to actually make this work takessome digging into the sources. I spent a whole weekend trying to make this work. This article documents the findings of this struggle.

Here are two simple requirements I want to build the authorization using ACL. It is quite typical for a website:
  1. There are three types of users: anonymous, member, and admin. Each can be further divided (e.g., there can be regular members and premium members)
  2. Contents can be divided into (based on ULR):
    1. accessible to all
    2. only accessible to members.
    3. only accessible to admin

To model these using CakePHP's ACL, different type of users can be mapped to AROs, and contents are ACOs. Each of these are modeled as a tree. Let's be a little more specific. I modeled the user hierarchy as follows:
Download code
ARO's:
        group.all
            group.anoymous
                anonymous
            group.members
                group.regular
                    test_regular
                group.premium
                    test_premium
                group.admin
                    test_admin
In the above diagram, those with the "group." prefix are not real users, but are groups for authorization purposes. "anonymous", "test_regular", "test_premium" and "test_admin" are real users. Let's assume they're already in the User table:
Download code
ID       login             password
===    =========        ===========
101       anonymous         123456
102       test_regular         123456
103       test_premium         123456
104       test_admin         123456

Simiarly, the contents of the site can be modeled as follows:
Download code
ACO's:
        /                   <=== accessible to all
            /pages      <=== accessible to all
            /posts        <=== only accessible to members
            /users        <=== only accessible to admin
            /authentications   <=== to all, for login

So our task is to somehow put all these into the ACL tables so that we can use the builtin ACL to achieve the desired permissions.

The builtin database ACL uses three tables to store ARO's, ACO's and ARO_ACO permissions. The first step is to translate the above data and requirements into ARO/ACO and thier relations. There is a script acl.php in the cake/scripts/ directory, but I found it to be confusing, and buggy (it messes up my tree sometimes when I set parents for some nodes.) So instead, I chose to do everything by hand, this gives me better understanding of how ACL works.

Let's first create the tables (same as "php acl.php initdb" for MySQL):
Download code
CREATE TABLE acos (
    id             integer NOT NULL AUTO_INCREMENT,
    object_id     integer DEFAULT NULL,
    alias          varchar(255) NOT NULL DEFAULT '',
    lft            integer DEFAULT NULL,
    rght        integer DEFAULT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE aros (
    id integer NOT NULL AUTO_INCREMENT,
    foreign_key integer DEFAULT NULL,
    alias      varchar(255) NOT NULL DEFAULT '',
    lft        integer DEFAULT NULL,
    rght    integer DEFAULT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE aros_acos (
    id integer NOT NULL AUTO_INCREMENT,
    aro_id integer DEFAULT NULL,
    aco_id    integer DEFAULT NULL,
    _create    integer NOT NULL DEFAULT 0,
    _read    integer NOT NULL DEFAULT 0,
    _update    integer NOT NULL DEFAULT 0,
    _delete    integer NOT NULL DEFAULT 0,
    PRIMARY KEY(id)
);

Now we will try to put our ARO tree into the aros table. To do this, you need to understand how CakePHP ACL stores a tree in a table. The method is called MPTT(Modified Preorder Tree Traversal), it is better than the other standard approach (i.e., having a "parent_id" column) in that it only takes one select query to find a subtree or a path to the root. The difficulty is to figure out what to put for the "lft" and "rght" columns for each row. For a detailed introduction to MPTT, please consult the very readable article: http://www.sitepoint.com/article/hierarchical-data-database. One confusing point (due to the lack of documentation) is how the "id", "foreign_key" and "alias" relates to the User table. It turns out the AROS.id column is an internal auto_incremented id, thus not relevant when creating the AROS (But, the confusing thing is that the AROS.id is used for the relation mapping in AROS_ACOS.) As to the User table: foreign_key should be the USER.id, and "alias" should be the user name: User.login.

With this understanding, we will put our ARO tree into the AROS table with the following insert statements (we reserve the first 100 "foreign_key" ids for future user groups, thus our real user id starts at 101):
Download code
insert into aros (id, foreign_key,alias,lft,rght)values(1,1,'group.all',1, 20);
insert into aros (id, foreign_key,alias,lft,rght)values(2,2,'group.anonymous',2, 5);
insert into aros (id, foreign_key,alias,lft,rght)values(3,3,'group.member',6, 19);
insert into aros (id, foreign_key,alias,lft,rght)values(4,4,'group.regular',7, 10);
insert into aros (id, foreign_key,alias,lft,rght)values(5,5,'group.premium',11, 14);
insert into aros (id, foreign_key,alias,lft,rght)values(6,6,'group.admin',15, 18);
insert into aros (id, foreign_key,alias,lft,rght)values(7,100,'anonymous',3, 4);
insert into aros (id, foreign_key,alias,lft,rght)values(8,101,'test_admin',16, 17);
insert into aros (id, foreign_key,alias,lft,rght)values(9,102,'test_regular',8, 9);
insert into aros (id, foreign_key,alias,lft,rght)values(10,103,'test_premium',12, 13);

Similarly, we can model the ACO's tree with the following:
Download code
insert into acos (id, object_id,alias,lft,rght)values(1,1,'/',1, 10);
insert into acos (id, object_id,alias,lft,rght)values(2,2,'/authentications',2, 3);
insert into acos (id, object_id,alias,lft,rght)values(3,3,'/users',4, 5);
insert into acos (id, object_id,alias,lft,rght)values(4,4,'/posts',6, 7);
insert into acos (id, object_id,alias,lft,rght)values(5,5,'/pages',8, 9);

If you want to check whether you modeled them correctly in the database with the above inserts, you can either do some sql query (again need to understand how MPTT works), or use the acl.php script as follows:
Download code
cake\scripts>php acl.php view aro

Aro tree:
------------------------------------------------
[1]group.all
  [2]group.anonymous
    [7]anonymous
  [3]group.member
    [4]group.regular
      [9]testreg
    [5]group.premium
      [10]testpre
    [6]group.admin
      [8]admin
------------------------------------------------
and:
Download code
cake\scripts>php acl.php view aco
Aco tree:
------------------------------------------------
[1]/
  [2]/authentications
  [3]/users
  [4]/posts
  [5]/pages
------------------------------------------------
Both tree show the desired structure.

Now let's model the permissions. We can either start with allowing all and gradually take away permissions, or the other way around, denying all and then add permission. I think it depends on the type of site you're trying to build. I chose the first approach for this example.

So first we grant all access of "/" to everyone:
Download code
insert into aros_acos(id,aro_id,aco_id,_create,_read,_update,_delete)values(1,1,1,1,1,1,1);
We then require that "/users" and "/posts" are only accessible to members. To
do this, we deny access to the "group.anonymous":
Download code
insert into aros_acos(id,aro_id,aco_id,_create,_read,_update,_delete)values(2,2,3,-1,-1,-1,-1);
insert into aros_acos(id,aro_id,aco_id,_create,_read,_update,_delete)values(3,2,4,-1,-1,-1,-1);
We then further require that "/users" can only be accessed by the "group.admin":
Download code
insert into aros_acos(id,aro_id,aco_id,_create,_read,_update,_delete)values(4,4,5,-1,-1,-1,-1);

With these in place, we expect the permission to behave correctly. That is, among others:
  1. Acl->check("anonymous","/pages","*") ====> true
  2. Acl->check("anonymous","/posts","*") ====> false
  3. Acl->check("anonymous","/users","*") ====> false
  4. Acl->check("test_regular","/posts","*") ====> true
  5. Acl->check("test_regular","/users","*") ====> false
  6. Acl->check("test_admin","/users","*") ====> true

To hook this into you application, the easiest is to put the permission checking into the app_controller.php, something like the following:

Controller Class:

Download code <?php 
class AppController extends Controller {
    var 
$beforeFilter = array('checkAccess');

    var 
$components = array('Acl');

    function 
checkAccess(){
        
// This part not required. It shows one way to
        // integrate this permission with authentication: login/logout
        // We always put the login_name in the session under
        // the key USER_LOGIN_KEY, even for anonymous users.
        // So whether a user is logged in or not depends on
        // whether this value is ANONY_USER or not. You may
        // choose to implement it some other way (e.g., whether it's
        // set or not.)
        
if (!$this->Session->valid()) {
            
$this->Session->renew();
        }
        if (!
$this->Session->check(USER_LOGIN_KEY)) {
            
$this->Session->write(USER_LOGIN_KEY,ANONY_USER);
        }

        
// here we check the permissions based on
        // username and controller name (which is
        // is the first part of the URL)
        
$user $this->Session->read(USER_LOGIN_KEY);
        
$aco $this->params['controller'];
        if (
$this->Acl->check($user"/$aco"'*')) {
            return; 
        }else{
            
// if anonymous, redirect to login
            // otherwise, give permission error
            
if( $user == ANONY_USER){
                
$this->redirect("/authentications/login");
            }else{
                
$this->redirect("/pages/permission_denied");
            }
        }
    }
}
?>

In order to test/use the above setup, you will need to code/mockup the controller/models/views for the "/users" and "/posts" part. To completely integrate with user management, your "user" model needs to have a modifed "save/delete" method to update the aros table.

One nice way to see whether your permissions are called correctly (besides the fact the page accesses behave correctly) is to turn on DEBUG = 3, you can then see all the SQL that the ACL component calls to figure out the permission. This requires/helps your understanding of the MPTT. The side effect is that you can also see that if your tree is deep, the current ACL implmentation is not efficient ( to check a permission for a ARO node, one needs to make depth(node) + 2 queries in the worst case, as in our example.)

In the next version of this article (hopefully), I'll try to make this part of the User permission into a component, to make it easily reusable.

 

Comments 171

CakePHP Team Comments Author Comments
 

Question

1 need help

Hi,

i'm trying to build simple webapp using your tutorial but i need some help.
Can you write with SQL statement how User table will look like?

Can i use other name for this table, not 'authentications'? when i try to load webapp in firefox i've got:

Missing Database Table

No Database table for model Authentication (expected "authentications"), create it first.
Posted Dec 29, 2006 by peceka
 

Comment

2 test

test
Posted Jan 19, 2007 by Jericho Escobar
 

Comment

3 Excellent tutorial

Thanks for this excellent tutorial! Made me understand ACL a lot better. The part on MPTT is quite amazing (to me).

Anyway, how's the work going on with the component? Is it any where near completion? ;)
Posted Jan 20, 2007 by Chua Chee How
 

Comment

4 something I ran into

I've used something similar to your approach. I have a checkAccess() method and I called it inside the beforeFilter() callback. So something like

function beforeFilter(){
parent::beforeFilter();
...
if($this->checkAccess(...)){
//everything is fine
}else{
$this->redirect("/pages/denied");
exit;
}
...
}

I had to add the "exit" statement because if I called a delete action and I didn't have the right to access to, the code flow in the right branch but instead of rendering the /pages/denied, it will execute the delete method. This only happen with the delete method, not edit, add or index.

Following the example of a delete method

function delete($id){
if($this->Model->delete($id)){
$this->Session->setFlash("ok");
}else{
$this->log("Error while deleting. {$id}");
$this->log($this->_getLastSQLError());
$this->Session->setFlash("ko. " . $this->_getLastSQLError());
}
$this->redirect("/controller/");
}
Posted Dec 31, 1969 by Davide G
 

Question

5 The users right background.

In your example you do:
"So first we grant all access of "/" to everyone:
Download code
insert into aros_acos(id,aro_id,aco_id,_create,_read,_update,_delete)values(1,1,1,1,1,1,1); "
And the users right background says:
everybody do nothing and also say who and what do.
Posted Dec 31, 1969 by Nicolicioiu Liviu
 

Comment

6 phpGACL

I haven't dug into yet but it looks like phpGACL is better than Cake's Acl component. The manual is 100x better and it has a web interface for the access control system.
Posted Mar 25, 2007 by Eric Winchell
 

Comment

7 phpGACL in CakePHP

@Eric: then you may be interested in the phpACL plugin for CakePHP:

http://dev.sypad.com/installing-phpgacl-plugin-cakephp
Posted Mar 25, 2007 by Mariano Iglesias
 

Comment

8 Phodu and excellent Tutorial

(In our College we call an extraordinary thing "PHODU") Thanks
a lot for this phodu tutorial!
Posted Aug 7, 2007 by gaurav tiwari
 

Comment

9 multiple groups

Is it possible to have a user in multiple groups?
Posted Sep 27, 2007 by Subhas
 

Comment

10 Great tutorial Thanks I also want to make a note re Cake 1 2

I just want to point out that CakePHP 1.2 has moved the "tool" PHP scripts -- like bake.php -- to the "Bake Shell". So the lines in this article that refer to "php acl.php" still work for 1.2, but they must be changed to be similar to:

php $CAKEPATH/console/cake.php acl
Where $CAKEPATH is your Cake install directory

later...
Posted Oct 19, 2007 by Jeffrey Silverman
 

Question

11 Can I have a good example for ACL

I want to one example to understand about ACL in Cakephp
email: one_cart@yahoo.com thanks.
Posted Dec 21, 2007 by one cart
 

Question

12 can not pass null when call aco or aro

Hello friends :) why i can't pass null value when i want to create an aro or aco ? i'am using database structure from the tutorial. no modification i did at the structure. i just copy and paste form the tutorial. So, the field that contain parent_id is allowed null value. i read tutorial from IBM.

i try this command :
php acl.php create aco 1 null "Tor Johnson School Of Drama"
and appear error that state parent_id can't be a null value

but as i read in this bakery tutorial, i didn't see the tutorial author passing null to object_id when defining ACO and passing null to foreign_key when defining ARO. i got confused by IBM tutorial, why they must pass null value ?

thank you
Posted Jan 3, 2008 by I Gusti Ngurah Oka Prinarjaya
 

Question

13 Adding to the Tree

Hi All, This article has been very helpful since there is not a lot of documentation on cakephp 1.2 yet. Is there an easy way to add elements to the ARO and ACO trees? If I want to add an element wouldn't I need to alter all the lft/rgt values of the other elements?
Posted Jan 24, 2008 by David Dear
 

Question

14 Adding an aco

Hi everybody, i have some problems when adding an aco, when i add an aco, i get null in the parent_id, and null in the alias, in fact, i have the function parentNode in the model and the $components=array('Acl'), in fact if I comment the lines where i add aco, the aco is inserted anyway in the database, but with those fields in NULL,can somebody explain me, what i am doing wrong? Thanks
Posted Mar 31, 2008 by alfredo
 

Question

15 Granting permissions

Hello everybody,

I have been reading some tutorials around including this as I am trying to build a subscription based website. So far I have got most of the work working properly apart from the permissions.

My problem is:

My aros are organized in the follwoing order:

Aros(Groups):
#Aros:
cake acl create aro root guests
cake acl create aro guests members
cake acl create aro members bronze
cake acl create aro bronze silver
cake acl create aro silver gold
cake acl create aro gold managers
cake acl create aro managers admins

Which generates:

[1]guests
[2]members
[3]bronze
[4]silver
[5]gold
[6]managers
[7]admins

As you can see permissions are an inherited from top to bottom.

Acos:

#Acos:
cake acl create aco / Root
cake acl create aco Root Accounts
cake acl create aco Root Users
cake acl create aco Root Subscriptions
cake acl create aco Root Payments
cake acl create aco Root Messages
cake acl create aco Root Albums
cake acl create aco Root Images
cake acl create aco Root Videos
cake acl create aco Root Menus
cake acl create aco Root Groups
cake acl create aco Root Comments

Which generates:

[1]Root
[2]Accounts
[3]Users
[4]Subscriptions
[5]Payments
[6]Messages
[7]Albums
[8]Images
[9]Videos
[10]Menus
etc...

I am just not sure how to grant permissions to reflect the follwing:

A user in the system can view any user profile, but can only edit his/her own. Also anything a user creates(post) on the site can only be deleted/edited by him/her and site managers and admins.

Any help would be very much appreciated...

Cheers
marcus

Posted Aug 30, 2008 by Marcus
 

Comment

16 Works great

@marcus:
I'd use normal application code and save the ACL for either general user class abilities, or special needs. Doing it with just ACL makes me worried about bloating the ACL tables.

@Theshz:
Thanks for the tutorial. I'm currently using the most recent release of CakePHP 1.2, but the format of your ACL tutorial helped me the most of those I read.
Posted Jan 27, 2009 by Michael Clark
 

Comment

17 Can you share your experience in cakephp 1.2.x.x.x?

i use the most recent 1.2 version but iam new in cakephp and i need some help to go fast, so can you share your experience?
Posted Apr 2, 2009 by cherif
 

Comment

18 Easey to understand ACL

Thanks theshz, your tutorial is so great.
It's very simple and easy to understand.
It helps me understand more about MPTT
Thanks a lot!
Posted Nov 3, 2009 by long nguyen
 

Comment

19 I clearly understand the concept now

Thank you it works, i got your tutorial clearly and i could create one on my web application. Good job!
Posted Feb 15, 2010 by Jolly Lengkono