Auth and ACL an end to end tutorial pt.2

Jul 13 2008

If you are looking for part one go here

In the last article we created the basic models, controllers and views for our Auth and Acl controlled app as well as initialized the Acl tables. We also bound our groups and users to the Acl through the use of the AclBehavior. In this article we will be inputting our existing controllers into the Acl and setting permissions for our groups and users, as well as enabling login / logout.

Creating ACO objects.

Our ARO are automatically creating themselves when new users and groups are created. What about a way to auto-generate ACOs from our controllers and their actions? Well unfortunately there is no magic way in CakePHP’s core to accomplish this. The core classes offer a few ways to manually create ACO’s though. You can create ACO objects from the Acl shell or You can use the AclComponent. Shell usage looks like:

Show Plain Text
  1. cake acl create aco root controllers
  2.  

While using the AclComponent would look like:

Show Plain Text
  1. $this->Acl->Aco->create(array('parent_id' => null, 'alias' => 'controllers'));

Both of these examples would create our root ACO which is going to be called ‘controllers’. The purpose of this root node is to make it easy to allow/deny access on a global application scope. As we will be using a global root ACO we need to make a small modification to our AuthComponent configuration. AuthComponent needs to know about the existence of this root node, so that when making ACL checks it can use the correct node path. In AppController add the following:

Show Plain Text
  1. $this->Auth->actionPath = 'controllers/';

Is there an easier way?

As I mentioned before, there is no way pre-built way to input all of our controllers and actions into the Acl. However, I hate doing repetitive things like typing in what could be hundreds of actions in a large application, so programming to the rescue! I whipped up an automated function to build my Aco table. This function will look at every controller in app/controllers. It will add any non-private, non Controller methods to the Acl table, nicely nested underneath the appropriate controller. You can add and run this in your AppController or any controller for that matter, just be sure to remove it before going into production.

Show Plain Text
  1. /**
  2.  * Rebuild the Acl based on the current controllers in the application
  3.  *
  4.  * @return void
  5.  */
  6.     function buildAcl() {
  7.         $log = array();
  8.  
  9.         $aco =& $this->Acl->Aco;
  10.         $root = $aco->node('controllers');
  11.         if (!$root) {
  12.             $aco->create(array('parent_id' => null, 'model' => null, 'alias' => 'controllers'));
  13.             $root = $aco->save();
  14.             $root['Aco']['id'] = $aco->id;
  15.             $log[] = 'Created Aco node for controllers';
  16.         } else {
  17.             $root = $root[0];
  18.         }   
  19.  
  20.         App::import('Core', 'File');
  21.         $Controllers = Configure::listObjects('controller');
  22.         $appIndex = array_search('App', $Controllers);
  23.         if ($appIndex !== false ) {
  24.             unset($Controllers[$appIndex]);
  25.         }
  26.         $baseMethods = get_class_methods('Controller');
  27.         $baseMethods[] = 'buildAcl';
  28.  
  29.         // look at each controller in app/controllers
  30.         foreach ($Controllers as $ctrlName) {
  31.             App::import('Controller', $ctrlName);
  32.             $ctrlclass = $ctrlName . 'Controller';
  33.             $methods = get_class_methods($ctrlclass);
  34.  
  35.             // find / make controller node
  36.             $controllerNode = $aco->node('controllers/'.$ctrlName);
  37.             if (!$controllerNode) {
  38.                 $aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $ctrlName));
  39.                 $controllerNode = $aco->save();
  40.                 $controllerNode['Aco']['id'] = $aco->id;
  41.                 $log[] = 'Created Aco node for '.$ctrlName;
  42.             } else {
  43.                 $controllerNode = $controllerNode[0];
  44.             }
  45.  
  46.             //clean the methods. to remove those in Controller and private actions.
  47.             foreach ($methods as $k => $method) {
  48.                 if (strpos($method, '_', 0) === 0) {
  49.                     unset($methods[$k]);
  50.                     continue;
  51.                 }
  52.                 if (in_array($method, $baseMethods)) {
  53.                     unset($methods[$k]);
  54.                     continue;
  55.                 }
  56.                 $methodNode = $aco->node('controllers/'.$ctrlName.'/'.$method);
  57.                 if (!$methodNode) {
  58.                     $aco->create(array('parent_id' => $controllerNode['Aco']['id'], 'model' => null, 'alias' => $method));
  59.                     $methodNode = $aco->save();
  60.                     $log[] = 'Created Aco node for '. $method;
  61.                 }
  62.             }
  63.         }
  64.         debug($log);
  65.     }

You might want to keep this function around as it will add new ACO’s for all of the controllers & actions that are in your application any time you run it. It does not remove nodes for actions that no longer exist though. You have to solve that one yourself. Now that all the heavy lifting is done, we need to set up some permissions, and remove the code that disabled AuthComponent earlier.

Setting up permissions.

Permissions like ACO’s have no magic solution, nor will I be providing one. To allow ARO’s access to ACO’s from the shell interface use cake acl grant $aroAlias $acoAlias to allow with the AclComponent do the following:

Show Plain Text
  1. $this->Acl->allow($aroAlias, $acoAlias);

We are going to add in a few allow/deny statements now. Add the following to a temporary function in your UsersController and visit the address in your browser to run them. If you do a SELECT * FROM aros_acos you should see a whole pile of 1’s and 0’s. If you don’t something didn’t work. Once you’ve confirmed your permissions are set remove the function.

Show Plain Text
  1. function initDB() {
  2.     $group =& $this->User->Group;
  3.     //Allow admins to everything
  4.     $group->id = 1;    
  5.     $this->Acl->allow($group, 'controllers');
  6.  
  7.     //allow managers to posts and widgets
  8.     $group->id = 2;
  9.     $this->Acl->deny($group, 'controllers');
  10.     $this->Acl->allow($group, 'controllers/Posts');
  11.     $this->Acl->allow($group, 'controllers/Widgets');
  12.  
  13.     //allow users to only add and edit on posts and widgets
  14.     $group->id = 3;
  15.     $this->Acl->deny($group, 'controllers');       
  16.     $this->Acl->allow($group, 'controllers/Posts/add');
  17.     $this->Acl->allow($group, 'controllers/Posts/edit');       
  18.     $this->Acl->allow($group, 'controllers/Widgets/add');
  19.     $this->Acl->allow($group, 'controllers/Widgets/edit');
  20. }

We now have set up some basic access rules. We’ve allowed administrators to everything. Managers can access everything in posts and widgets. While users can only access add and edit in posts & widgets.

We had to get a reference of a Group instance and modify its id to be able to specify the ARO we wanted, this is due to how AclBehavior works. AclBehavior does not set the alias field in the aros table so we must use an object reference or an array to reference the ARO we want.

What about view and index?

You may have noticed that I deliberately left out index and view from my Acl permissions. We are going to make view and index public actions in PostsController and WidgetsController. This allows non-authorized users to view these pages as well. However, at any time you can remove these actions from AuthComponent::allowedActions and the permissions for view and edit will revert to those in the Acl.

Take out the references to Auth->allowedActions in your users and groups controllers. Then add the following to your posts and widgets controllers:

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

This removes the ‘off switch’ on the users and groups controllers, and gives public access on the index and view actions in posts and widgets controllers.

Login

Our application is now under access control, and any attempt to view non-public pages will redirect you to the login page. However, we will need to create a login view before anyone can login. Add the following to app/views/users/login.ctp if you haven’t done so already.

Show Plain Text
  1. <h2>Login</h2>
  2. <?php
  3. echo $form->create('User', array('url' => array('controller' => 'users', 'action' =>'login')));
  4. echo $form->input('User.username');
  5. echo $form->input('User.password');
  6. echo $form->end('Login');
  7. ?>

You may also want to add a flash() for Auth messages to your layout. Copy the default core layout – found at cake/libs/views/layouts/default.ctp – to your app layouts folder if you haven’t done so already. In app/views/layouts/default.ctp add

Show Plain Text
  1. $session->flash('auth');

You should now be able to login and everything should work auto-magically. When access is denied Auth messages will be displayed if you added the $session->flash('auth')

Logout

Now onto the logout. In the last tutorial we left it at //Auth magic. However, a little bit more is needed to make the magic happen. Add the following to your UsersController::logout():

Show Plain Text
  1. $this->Session->setFlash('Good-Bye');
  2. $this->redirect($this->Auth->logout());

This sets a Session flash message and logs out the User using Auth’s logout method. Auth’s logout method basically deletes the Auth Session Key and returns a url that can be used in a redirect. If there is other session data that needs to be deleted as well add that code here.

All done

You should now have an application controlled by Auth and Acl. Users permissions are set at the group level, but you can set them by user at the same time. You can also set permissions on a global and per-controller and per-action basis. Furthermore, you have a reusable block of code to easily expand your ACO table as your app grows.

If you are having any troubles at this point or your app doesn’t work as I’ve described, download the files I used and see what’s going on.

I Hope you have enjoyed this article and found if helpful. I’d love to hear any feedback you have about it.


Comments

Alberto on 14/7/08

Thank you so much for the tutorial, I was looking for this during the last days.

I miss in the tutorial the register action for the users.

which are the passwords you have used for the users in the file?

mark story on 14/7/08

@Alberto: I thought I put the passwords in the README.txt but every user’s password is ‘pass’. As for the register action. I never made one, as in the first part I directly added the users to the database via users/add. But there is no reason that users/add could not be adapted to make a users/register action.

mike on 17/7/08

Hi!
Great tutorial, very useful!
What I am missing here, is the ability to restrict users to edit only their own post and not everyone’s.

Joel on 17/7/08

Hi Mark, great follow up post on ACL. However, is there a reason why you did not use Configure::listObjects(‘controller’) to get the list of controllers? In the end it doesn’t really matter since this is run-once code…

Andrew Allen on 17/7/08

Mark,

Looks great, thanks for this. You sent me the proof and I thought I got it then but you’ve hammered it home now. The one thing holding me back from jumping in with ACL and sticking with my own isAuthorized schema is that I can’t piece together how to manage ACL so that each individual record in the DB can have view/edit/delete set individually based on per user or per group settings. Any ideas? Maybe a part 3 hint hint

Thanks so much.

Sincerely,
~Andrew Allen

Joel on 17/7/08

@Andrew: Just add an additional layer to the ACO tree, so that you have something like:
controllers->Posts->{edit/view/delete}->individual_post_identifier.

mark story on 17/7/08

@Joel I originally wrote this script for CakePHP 1.1 so that would explain the absence of Configure::listObjects(). I’ve updated the function on the page to reflect the addition of Configure::listObjects().

grncdr on 25/7/08

@Joel and/or mark story I’m confused about “Just add an additional layer to the ACO tree”

Wouldn’t you end up with a lot of extra records in your aco table? for example, if there are 6 actions in a posts controller, you would end up with 6 ACO records for every post…

Rafael Bandeira aka rafaelbandeira3 on 28/7/08

You made it man…
there wasn’t such well explained ACL usage til now, and you know many people tried it already…

now, about Andrews question: Andrews : Acl got a per model support, so you could fill your aco's table just as Mark filled the aro's. And permission's table (acos_aros) got the CRUD corresponding fields. I'm not sure how to implement it on the a app, but it's a lot of thing to get your head on already. Mark : you could cover it! You did it so nice…

Jason on 5/8/08

Mark – still enjoying this set of tutorials. Thanks again for putting them together. You said to “Take out the references to Auth->allowedActions in your users and groups controllers.” When I do this and then try to access a restricted controller/action with a user who doesn’t have the rights to do so an endless loop is created. (They are sent to the login action which they don’t have access to and thus the loop.) I believe the login and logout actions need to be permitted to all users to prevent this. Can you comment? Thanks again.

Sandy on 8/8/08

shouldn’t
cake acl create aco null controllers

be
cake acl create aco root controllers

at least the first doesn’t work for me.

mark story on 9/8/08

Sandy: You are correct, I’ve updated the tutorial, thanks :)

grncdr: I would strongly advise against adding an aco for each action for each record. This will balloon your aco table, and make an utter mess. A better solution would be to make a separate tree for models, and set CRUD permissions for each record. I’m planning on covering this in the future.

Jason: I haven’t been able to reproduce that issue. But if you keep having issues e-mail me.

cem on 24/8/08

Hi ;

Great tutorial something really confused me . Whi I run the function buildAcl() it created many fields in the acos table . Thats great but since every controller has an index how can I know which index record belongs to which controller ?

HeathNail on 26/8/08

Mark,

Great tutorial :) Thanks for taking the time to write it.

I don’t think its any fault of the tutorial, but does anyone else get infinite redirect loops? When logged in as Administrator or Manager and trying to visite /users/add I get infinite redirects.

HeathNail on 26/8/08

Excuse me I meant to say User or Manager.

HeathNail on 26/8/08

Figured it out. I had to allow the display action in PagesController.

Mark sorry for the mess of comments please condense/edit/delete as you please :(

Jason 4 weeks ago

@HeathNail – I was having the same problem. Thanks for noting your fix.

Jason 4 weeks ago

Sorry for all of the comments but I came up with another solution for my redirect problem. For reasons I have yet to figure out, Cake was trying to redirect to ‘/’ when a User or Manager tried to access a Admin level ACO. This caused the infinite loop. By going to the routes.php file and changing the ‘/’ location to Users::login I stopped the looping. I’m not totally sure why Cake tried to redirect to ‘/’ when the loginAction was set to ‘users/login’. It seems like a hack of a fix since others haven’t mentioned the need for such a tweak—but at least it’s working.

mark story 4 weeks ago

Jason: Don’t worry I think its good that there is an ongoing conversation about this. As for redirecting to ‘/’. It could be the result of an empty referer. If a user is not authorized, and accesses a page they do not have access to via direct input into the browser address bar there will not be a valid http referer. The way that Cake handles this is the referer becomes ‘/’ . This can also happen when the browser doesn’t send referer headers.

Jason 4 weeks ago

Yeah, I think that’s it. I was always entering a non-authorized ACO into the address bar to see what would happen. I tried adding a link to see what would happen when a referrer was present. I expected to be directed to the loginAction but I’m redirected to my current location. I believe this is the result of already being logged in so I’m sent back to the referring page. The behavior of the Auth component is slowing coming to light.

Dome 3 weeks, 5 days ago

Hi. Thanks for this great tutorial. I followed everything on this series. I’m having a weird problem, administrators (those with group_id =1) can only access /admin/controller/index action. It denies access to /admin/controller/other_action (eg /admin/posts/view)

Doesn’t $this->Acl->allow($group, ‘controllers’); provides access to eveything?

Thanks again

Luke 3 weeks, 4 days ago

hi Jason and mark.

But is that a good hack to make the router.php file put / to User::login? Will that only be for when Auth has a refuse-access response to a request without referer ?

Confused :/

Thanks for this great tutorial by the way – I have it 99% working other than this redirect thingy!

Luke 3 weeks, 3 days ago

hiya – Mark, I just wanted to say thanks once again for this great tutoiral – it is by far the best if followed through all the way, in helping one get up and running with the Auth / ACL system.

I am still slightly confused by certain re-directs the system performs when I lgoin – am I right in thinking it sometimes redirects to the last visited page of the last user who has logged out??

But your guide is really great – it took a while for it to sink in but I now think I get the acos / aros / acos_aros thing.

I used to use othAuth which worked very intuitively and I liked looking at my permission table and seeing the actions written out for each permission so I knew who had what more easily . But it feels good to use the cake cor e version which clearly is well thought out – just very powerful and so on.

mark story 3 weeks, 2 days ago

Luke: If you try to visit a page you are not authorized to visit before logging in, Auth will redirect you to that place once you login. And you don’t need to hack the routes.php to get Users::login connected to ‘/’ you just make it like a regular route.

Router::connect('/', array('controller' => 'users', 'action' => 'login'));

Should do the trick.

Lambiato 3 weeks, 2 days ago

How is it possible that the autocreating of aco works for you?
In my case
$controllerNode = $aco->node(‘controllers/’.$ctrlName);

Returns first node it found. If a/b not present it returns a so the result is always set. It returns ‘controllers’ node. So it never creates and acos for controllers and methods

mark story 3 weeks, 2 days ago

Lambiato: If an entire node path is not found AclNode::node() returns false. I just ran the db_acl tests which cover this and they are all passing. Are you not seeing this behavior?

phunkk 2 weeks, 3 days ago

simply A-MA-ZING!
The first tutorial that actually works, after going through a massive load of crappy tut’s out there. People, look no further: this is the one to use!

Sandy 2 weeks ago

Hey Mark,

thanx for the tutorial – rude me forgot to say that last time. ^^
In the initDB function – shouldn’t it say

$this->Acl->allow($group, ‘controllers/Posts’,‘create’); $this->Acl->allow($group, ‘controllers/Posts’,‘update’); $this->Acl->allow($group, ‘controllers/Widgets’,‘create’); $this->Acl->allow($group, ‘controllers/Widgets’,‘update’);

for group 3?
Took me a while to figure out that in my aros_acos table the fields are not named edit and add but instead create and update.

Greetz,
Sandy

Sandy 2 weeks ago

Forget what i just said – think i misunderstand something here.

Sandy 2 weeks ago

Standard baked functions are named add(), edit(), view(), index(), delete() yet in the aros_acos table they are named _create, _read, _update, _delete?
Beats me why that is so. Sorry for spamming your page. :$

mark story 1 week, 6 days ago

Sandy: I think you are getting confused by the crud fields in the aros_acos table. Those fields are used when setting permissions per CRUD perm. When setting permissions for the actions I decided to not use individual CRUD fields as the AuthComponent doesn’t use them unless it is in crud mode. Using AuthComponent in crud mode was something out of this tutorials scope though. If you are following the tutorial ignore the CRUD fields.

John 1 week, 6 days ago

Hi, great tutorial, especially for a newbie like me. I seem to have a few problems though, on the permissions part if you guys can help me.

cake acl grant $aroAlias $acoAlias all

I get the following error when I try the above command:
PHP Warning: DbAcl::allow() – Invalid node in C:\htdocs\cake\cake\libs\controller\components\acl.php on line 367

Warning: DbAcl::allow() – Invalid node in C:\htdocs\cake\cake\libs\controller\components\acl.php on line 367
Permission was not granted.

I also tried running the alternative, $this->Acl->allow($aroAlias, $acoAlias);, but it gave me an error saying $aroAlias and $acoAlias do not exist.

Help! What can I do?

John 1 week, 3 days ago

Please disregard my last post, I found out what I was doing wrong.

But on my aros_acos table, I see a bunch of 1s and -1s instead of the 1s and 0s, is this normal?

mark story 1 week, 2 days ago

John: Yes the 1’s and -1’s are normal. 1’s are allow permissions, 0 is inherit, and -1 is deny.

Michel 6 days, 9 hours ago

@John: I’ve got the same error, how did you fix it?

Michel 6 days, 7 hours ago

Never mind my previous post. Already fixed it. Problem was that i was adding permissions over and over again. Cleaned out the cache too


Have Something to say?

*
* (Never published, I promise)
* You can use Textile markup, but be reasonable

Recent Artwork

  • CakePHP Test suite icons
  • Shuriken 2
  • Clumsy Penguins
  • Balloon Animals
  • Metal in the air! (vertical)
  • Cleavers