ACL with Groups

19 07 2007

In a previous article I explained how to use the AclBehavior to create models that can be used for ACL.

Now the situation is – I want to assign permission to groups and have Users be a member of a group. I thought this would be easy using the AclBehavior. Turns out I was right, but it took me while to realise I was right.

So we are going to use the following Group model. It should look familiar. The database has a parent_id field in the groups table. This model allows us to create a heirachy of group roles.

<?php
class Group extends AppModel {
  var $name = 'Group';
  var $actsAs = array('Acl');

  function parentNode(){
    if (!$this->id) {
      return null;
    }
		
    $data = $this->read();
		
    if (!$data['Group']['parent_id']){
      return null;
    } else {
      return $data['Group']['parent_id'];
    }
  }

}
?>

Now, for ACL to work the easy way, we need to have our users in the Aro table as well. In your users table you need a field called group_id, and then use the belongsTo association to relate the user to a group. We also need to use the AclBehavior to get our users into the aro table.

Thats all pretty simple and straight forward. The trick is making the user aros children of a group aro. The parentNode() function is where the magic happens. Instead of returning a single id, you return an array with a two keys: model and foreign_key. This will allow the AclBehavior to locate the appropriate parent Aro node.

<?php
class User extends AppModel {
  var $name = 'User';
  var $belongsTo = array('Group');
  var $actsAs = array('Acl');

  function parentNode(){
    if (!$this->id) {
      return null;
    }
		
    $data = $this->read();
		
    if (!$data['User']['group_id']){
      return null;
    } else {
      return array('model' => 'Group', 'foreign_key' => $data['User']['group_id']);
    }
  }

}
?>

Now your User Aros are children of Group Aros. Simply assign permissions to the Group Aros and the Users will automatically inherit them.





Using AuthComponent and ACL in CakePHP 1.2

19 07 2007

The uniqueness issues and also an issue with inheritence has been solved in changset 5588

Important Update (2007-07-24) : Brian brought to my attention a problem with multiple actions that have the same name. This problem is due to an incorrect Sql query in db_acl.php. I have filed Trac Ticket #2976 which includes a patch, unit test and test fixtures.

So I’ve shown you how to create a User Model that can be used for ACL, but what is next?

The AuthComponent allows you to both authenticate and authorize your users in a relatively simple manner – once you know how. This took me a lot of source code digging, mail list browsing and googling to find the way that “just works”.

Setting up Aco’s, Aro’s and Permissions

First of, we need to have some Aro’s and Aco’s. Aro’s are the User model from above and are simple enough – you can get away with scaffolding if you want.

Aco’s took me a little longer to figure out. There are going to be controller action pairs, naturally, but the catch was how to put these into the database. It turns out that you need to have Controllers at one level, with actions children of there respective controllers. This might seem obvious but I was trying to save the actions with an alias of “controller/action” when they should just be “action”.

Your tree of Aco aliases should look like the following:

ROOT
|-Controller1
  |-index
  |-action1
  |-action2
|-Controller2
  |-index
  |-action1

Pretty straight forward once you know how. This way the path can be followed down (or up actually) by the AclComponent. It also allows you to create wholesale permissions such as denying a whole controller by setting the permissions on the controller level.

Then all you need to do is grant Users access to actions. Because we are using User models the easist way is with a series of commands like:

$this->Acl->allow(array('model' => 'User', 'foreign_key' => $id), 'Controller1/action');

Using AuthComponent for Authentication

The AuthComponent is a really great tool and is very flexible. This post will only show one way to use it, but there are many other ways to put it to use.

First up, we will use AuthComponent for authentication. This part is pretty straight forward, but requires a little configuration. The best place to do this is in AppController::beforeFilter().

You need to include the AclComponent and the AuthComponent in AppController.

var $components = array('Acl', 'Auth');

It is very import that you include the AclComponent before the AuthComponent otherwise you will get this funky error message.

Fatal error: Call to a member function check() on a non-object in /var/www/geoff/cake/cake/libs/controller/components/acl.php on line 87

The reason for this is that Components are started in the order they appear in the $components list, and the AuthComponent does all its magic at startup, so when Auth tries to use Acl::check() there is no Acl.

For authentication we need to set the loginAction and loginRedirect properties.

function beforeFilter(){
  if (isset($this->Auth)) {
    $this->Auth->loginAction = '/users/login';
    $this->Auth->loginRedirect = '/users/account';
  }
}

You can also set the fields that are used for authentication if your user model does not use the default of ‘username’ and ‘password’.

$this->Auth->fields = array('username' => 'login_name', 'password' => 'p4sswrd');

It is worth noting that the AuthComponent will hash the password for security reasons. This is default behaviour and it can not be overriden. Where I got caught out (again) was that I had a before save on my user model that hashed the password in the same manner as AuthComponent. Do not hash your passwords at the model level if you have included the AuthComponet at the AppController level. The AuthComponent will hash it before it gets to your beforeSave and you will end up with double hashed passwords in the database and therefore unable to login.

Another good config option is the userScope property. It allows you to add other conditions to the authentication query, such as checking a disabled field.

 $this->Auth->userScope = array('User.disabled' => 0);

Using AuthComponent for Authorization

So that was how to set your authentication. Setting up AuthComponet for authorization against ACL is actually easier. One extra line of code.

$this->Auth->authorize = 'actions';

This will use the User Aro’s created above that corresponds to the User just authenticated, and test it against the current controller and action. Surprisingly easy. 🙂

Final Result

The final AppController should look like:

class AppController extends Controller {
  var $helpers = array('Html', 'Javascript', 'Form');
  var $components = array('Acl', 'Auth');
  var $publicControllers = array('pages');
	
  function beforeFilter(){
    if (isset($this->Auth)) {
      $this->Auth->userScope = array('User.disabled' => 0);
      $this->Auth->loginAction = '/users/login';
      $this->Auth->loginRedirect = '/users/account';
      $this->Auth->fields = array('username' => 'username', 'password' => 'passwrd');
      $this->Auth->authorize = 'actions';

      if (in_array(low($this->params['controller']), $this->publicControllers)) {	
	    $this->Auth->allow();	
      }
    }
  }
}

$this->publicControllers allows you to assign controllers which do not require authentication or authorization. I mainly use it to grant access to the pages contoller.