Auth and ACL an end to end tutorial pt.2

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. Be sure to run the function, in a browser by visiting /users/buildAcl if you don’t, you won’t have an ACO’s to work with. 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

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?

anonymous user 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.

Mark Story on 14/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.

anonymous user 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…

anonymous user 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

anonymous user 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.

anonymous user 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().

Mark Story on 17/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…

anonymous user on 25/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…

anonymous user on 28/7/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.

anonymous user on 5/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.

anonymous user on 8/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.

Mark Story on 9/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 ?

anonymous user on 24/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.

anonymous user on 26/8/08

Excuse me I meant to say User or Manager.

anonymous user 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 :(

anonymous user on 26/8/08

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

anonymous user on 8/9/08

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.

anonymous user on 8/9/08

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.

mark story on 8/9/08

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.

anonymous user on 8/9/08

< prev1234567

Have your say:

*
* You can use Textile markup, but be reasonable