The more roles an application has, the more complex its Access Control Layer becomes. Luckily, one of the authentication schemes provided by the Auth
component allows us to easily define which actions are accessible by certain roles (known as groups), using command-line tools. In this recipe, you will learn how to set up ACL on your application.
We should have a table to hold the roles, named groups
.
If you do not have one already, create it using the following statement:
CREATE TABLE `groups`( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, PRIMARY KEY(`id`) );
If you do not have any records in your groups
table, create some by running the following SQL statement:
INSERT INTO `groups`(`id`, `name`) VALUES (1, 'Administrator'), (2, 'Manager'), (3, 'User'),
We must also have a users
table to hold the users, which should contain a field (named group_id
) to contain a reference to the group a user belongs to. If you do not have such a table, create it using the following statement:
CREATE TABLE `users`( `id` INT NOT NULL AUTO_INCREMENT, `group_id` INT NOT NULL, `username` VARCHAR(255) NOT NULL, `password` CHAR(40) NOT NULL, PRIMARY KEY(`id`), KEY `group_id`(`group_id`), CONSTRAINT `users__groups` FOREIGN KEY(`group_id`) REFERENCES `groups`(`id`) );
We also need to have the ARO / ACO tables initialized. Using your operating system console, switch to your application directory, and run:
../cake/console/cake schema create DbAcl
..cakeconsolecake.bat schema create DbAcl
The following initial steps are very similar to what is shown in Setting up a basic authentication system. However, there are some differences between the two that are crucial, so make sure you go through these instructions carefully.
User
model (in a file named users_controller.php
placed inside your app/controllers
folder), which should contain the following:<?php class UsersController extends AppController { public function login() { } public function logout() { $this->redirect($this->Auth->logout()); } } ?>
login.ctp
in your app/views/users
folder (create the folder if you do not have one already), with the following contents:<?php echo $this->Form->create(array('action'=>'login')); echo $this->Form->inputs(array( 'legend' => 'Login', 'username', 'password' )); echo $this->Form->end('Login'), ?>
app_controller.php
in your app/
folder. Make sure it contains the following:<?php class AppController extends Controller { public $components = array( 'Acl', 'Auth' => array( 'authorize' => 'actions', 'loginRedirect' => array( 'admin' => false, 'controller' => 'users', 'action' => 'dashboard' ) ), 'Session' ); } ?>
UsersController
class and add the following code before its login()
method:public function beforeFilter() { parent::beforeFilter(); $this->Auth->allow('add'), } public function add() { if (!empty($this->data)) { $this->User->create(); if ($this->User->save($this->data)) { $this->Session->setFlash('User created!'), $this->redirect(array('action'=>'login')); } else { $this->Session->setFlash('Please correct the errors'), } } $this->set('groups', $this->User->Group->find('list')); }
app/views/users
by creating a file named add.ctp
with the following contents:<?php echo $this->Form->create(); echo $this->Form->inputs(array( 'legend' => 'Signup', 'username', 'password', 'group_id' )); echo $this->Form->end('Submit'), ?>
group.php
and place it in your app/models
folder with the following contents:<?php class Group extends AppModel { public $actsAs = array('Acl' => 'requester'), public function parentNode() { if (empty($this->id) && empty($this->data)) { return null; } $data = $this->data; if (empty($data)) { $data = $this->find('first', array( 'conditions' => array('id' => $this->id), 'fields' => array('parent_id'), 'recursive' => -1 )); } if (!empty($data[$this->alias]['parent_id'])) { return $data[$this->alias]['parent_id']; } return null; } } ?>
user.php
and place it in your app/models
folder with the following contents:<?php class User extends AppModel { public $belongsTo = array('Group'), public $actsAs = array('Acl' => 'requester'), public function parentNode() { } public function bindNode($object) { if (!empty($object[$this->alias]['group_id'])) { return array( 'model' => 'Group', 'foreign_key' => $object[$this->alias]['group_id'] ); } } } ?>
Take note of the IDs for all the records in your groups
table, as they are needed to link each group to an ARO record.
../cake/console/cake acl create aro root Groups ../cake/console/cake acl create aro Groups Group.1 ../cake/console/cake acl create aro Groups Group.2 ../cake/console/cake acl create aro Groups Group.3
..cakeconsolecake.bat acl create aro root Groups ..cakeconsolecake.bat acl create aro Groups Group.1 ..cakeconsolecake.bat acl create aro Groups Group.2 ..cakeconsolecake.bat acl create aro Groups Group.3
UsersController
class definition:public function dashboard() { $groupName = $this->User->Group->field('name', array('Group.id'=>$this->Auth->user('group_id')) ); $this->redirect(array('action'=>strtolower($groupName))); } public function user() { } public function manager() { } public function administrator() { }
app/views/users/user.ctp
app/views/users/manager.ctp
app/views/users/administrator.ctp
.For example the contents for user.ctp
could simply be:
<h1>Dashboard (User)</h1>
../cake/console/cake acl create aco root controllers ../cake/console/cake acl create aco controllers Users ../cake/console/cake acl create aco controllers/Users logout ../cake/console/cake acl create aco controllers/Users user ../cake/console/cake acl create aco controllers/Users manager ../cake/console/cake acl create aco controllers/Users administrator
..cakeconsolecake.bat acl create aco root controllers ..cakeconsolecake.bat acl create aco controllers Users ..cakeconsolecake.bat acl create aco controllers/Users logout ..cakeconsolecake.bat acl create aco controllers/Users user ..cakeconsolecake.bat acl create aco controllers/Users manager ..cakeconsolecake.bat acl create aco controllers/Users administrator
../cake/console/cake acl grant Group.1 controllers/Users all ../cake/console/cake acl grant Group.2 controllers/Users/logout all ../cake/console/cake acl grant Group.2 controllers/Users/manager all ../cake/console/cake acl grant Group.3 controllers/Users/logout all ../cake/console/cake acl grant Group.3 controllers/Users/user all
..cakeconsolecake.bat acl grant Group.1 controllers/Users all ..cakeconsolecake.bat acl grant Group.2 controllers/Users/logout all ..cakeconsolecake.bat acl grant Group.2 controllers/Users/manager all ..cakeconsolecake.bat acl grant Group.3 controllers/Users/logout all ..cakeconsolecake.bat acl grant Group.3 controllers/Users/user all
We now have a fully working ACL based authentication system. We can add new users by browsing to http://localhost/users/add
, logging in with http://localhost/users/login, and finally logging out with http://localhost/users/logout.
Users should only have access to http://localhost/users/user
, managers to http://localhost/users/manager
, and administrators should be able to access all those actions, including http://localhost/users/administrator
.
When setting the authorize
configuration option of the Auth
component to actions
, and after adding Acl
to the list of controller-wide components, CakePHP will check to see if the current action being accessed is a public action. If this is not the case, it will check for a logged-in user with a matching ACO record. If there is no such record, it will deny access.
Once there is a matching ACO for the controller action, it will use the bindNode
method in the User
model to see how a user record is matched to an ARO. The method implementation we added specifies that a user record should be looked up in the aros
table by means of the group that the user belongs to.
After having both the matching ACO and ARO, it lastly checks to see whether there is a valid permission set up (in the aros_acos
table) for the given ARO and ACO records. If it finds one, it allows access, otherwise it will reject authorization.
It is of vital importance that each record in the groups table has a matching ARO record. We set that association by issuing aro create
commands to link each group ID to an ARO record of the form Group.ID
, where ID is the actual ID.
Similarly, all controller actions that are not within the defined public actions should have a matching ACO record. Just as with AROs, we create the association between controller's actions and ACOs issuing aco create
commands, setting the ACO name to be the action name, and making them child of an ACO which name is the controller name.
Finally, to grant the permission of an ARO (group) to an ACO (controller's actions), we issue acl grant
commands, specifying as the first argument the ARO (Group.ID) and the second argument either a whole controller (such as controllers/Users
), or a specific controller action (such as controllers/Users/logout
). The last argument to the grant command (all) simply gives a further control of the type of access, and makes more sense when using ACL to control access to custom objects, or when using the crud
authentication scheme.
While developing an application, the task of matching each controller action to an ACO may be somewhat troublesome. Fortunately, several people in the CakePHP community felt the need for an easier solution. One of the solutions that I'd recommend is adopting acl_extras
, a plugin developed by Mark Story, the lead developer of the CakePHP 1.3 release. By using this plugin, you will be able to continuously synchronize your controllers with the acos
table. Find more about it, including its installation instructions, at http://github.com/markstory/acl_extras.
3.17.181.61