CakePHP Auth Component - Users, Groups & Permissions Revisited

Published: on 20/6/08 | Comments (58)

In my earlier article CakePHP Auth Component - Users, Groups & Permissions I demonstrated step by step how to create a complete Authentication & Authorization system based around cakePHPs' Auth component.

Judging from the number of visits the article has had and the great comments I have received from the article, it prooved to be a bit of a winner, and as the last couple of weeks I haven't had a chance to post, due to sheer workload, I thought today, I would sit down and refactor the code to reflect some of the comments and requests I have received about it.

Caching Permissions to Session

One of the biggest changes I have made to the system is to move the permissions cache into SESSION, the advantage of this approach is that once the permissions have been cached to Session, the database doesn't have to be re-queried on each page request, thus saving a substantial amount of time and speeding up your application.

Wildcard permissions for controller wide access

Along with the previous permission format of controller:action for each permission, I have added the ability to set permissions in the form of controller:* so that you can set controller wide permissions for groups.

Making use of UUID's

With this version of the code I have also switched to UUIDs (Universally Unique Identifiers) for my database primary ids, this is a personal choice and the system will work just as well using an auto increment int(11) but for larger systems I find that UUIDs are little more versatile, especially when it comes to merging multiple databases.

CakePHP 1.2.0.7125RC1 support

Having begun playing with the new release, I have built this update using the latest release candidate and it's working fine without modification.

On with the code

So, preamble over, here is the code with comments about where I have changed or re-factored things a little.

Database Schema

While this is pretty much the same as my earlier version, I have as outlined above used UUIDs for my primary keys here, which in CakePHP can be implemented simply by making the id field a char(36).

CREATE TABLE 'groups' (
  'id' char(36) NOT NULL,
  'name' varchar(40) NOT NULL,
  'created' datetime NOT NULL,
  'modified' datetime NOT NULL,
  PRIMARY KEY  ('id')
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE 'groups_permissions' (
  'group_id' char(36) NOT NULL,
  'permission_id' char(36) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE 'groups_users' (
  'group_id' char(36) NOT NULL,
  'user_id' char(36) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE 'permissions' (
  'id' char(36) NOT NULL,
  'name' varchar(40) NOT NULL,
  'created' datetime NOT NULL,
  'modified' datetime NOT NULL,
  PRIMARY KEY  ('id')
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE 'users' (
  'id' char(36) NOT NULL,
  'email_address' varchar(127) NOT NULL,
  'password' varchar(40) NOT NULL,
  'active' tinyint(4) NOT NULL default '0',
  'created' datetime NOT NULL,
  'modified' datetime NOT NULL,
  PRIMARY KEY  ('id')
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

All pretty standard so far, note that I am using email_address instead of a user name as these should already be unique for every user (and your user is less likely to forget it), and I have an active flag set in the Users table which indicates whether the user can log into the system, you can use this as a way of implementing an email authentication system, where you send a user an email with a link to click, then once it's clicked set active to 1 and the user will then be able to log in.

Model Code

The models haven't been refactored as they are pretty much identicle to those in my earlier article, but I have put them here for the sake of completeness.

User Model

app/models/user.php

class User extends AppModel {
var $displayField = 'email_address'; var $name = 'User'; var $validate = array( 'email_address' => array('email'), 'password' => array('alphaNumeric'), 'active' => array('numeric') ); var $hasAndBelongsToMany = array( 'Group' => array('className' => 'Group', 'joinTable' => 'groups_users', 'foreignKey' => 'user_id', 'associationForeignKey' => 'group_id', 'unique' => true ) ); }

Group Model

app/models/group.php

class Group extends AppModel {
    var $name = 'Group';
    var $hasAndBelongsToMany = array(
            'Permission' => array('className' => 'Permission',
                        'joinTable' => 'groups_permissions',
                        'foreignKey' => 'group_id',
                        'associationForeignKey' => 'permission_id',
                        'unique' => true
            ),
            'User' => array('className' => 'User',
                        'joinTable' => 'groups_users',
                        'foreignKey' => 'group_id',
                        'associationForeignKey' => 'user_id',
                        'unique' => true
            )
    );
}

Permissions Model

app/models/permission.php

class Permission extends AppModel {
    var $name = 'Permission';
    var $hasAndBelongsToMany = array(
            'Group' => array('className' => 'Group',
                        'joinTable' => 'groups_permissions',
                        'foreignKey' => 'permission_id',
                        'associationForeignKey' => 'group_id',
                        'unique' => true
            )
    );
}

The Controllers

Users Controller

Again, the controllers remain pretty much as per my last article except in the Users Controller, there is now an extra line to delete the permissions array from the session just prior to the user logging out.

app/controllers/users_controller.php

class UsersController extends AppController {
    var $name = 'Users';
    var $scaffold;
    function login(){}
    function logout(){
        $this->Session->del('Permissions');
        $this->redirect($this->Auth->logout());
    }
}

Groups Controller

app/controllers/groups_controller.php

class GroupsController extends AppController {
    var $name = 'Groups';
    var $scaffold;
}

Permissions Controller

app/controllers/permissions_controller.php

class PermissionsController extends AppController {
    var $name = 'Permissions';
    var $scaffold;
}

App Controller

The App Controller is where the majority of the refactoring has taken place, so I have left in a full set of comments with the code below.

app/app_controller.php

class AppController extends Controller {
    /**
     * components
     * 
     * Array of components to load for every controller in the application
     * 
     * @var $components array
     * @access public
     */
    var $components = array('Auth');
    /**
     * beforeFilter
     * 
     * Application hook which runs prior to each controller action
     * 
     * @access public 
     */
    function beforeFilter(){
        //Override default fields used by Auth component
        $this->Auth->fields = array('username'=>'email_address','password'=>'password');
        //Set application wide actions which do not require authentication
        $this->Auth->allow('display');
        //Set the default redirect for users who logout
        $this->Auth->logoutRedirect = '/';
        //Set the default redirect for users who login
        $this->Auth->loginRedirect = '/';
        //Extend auth component to include authorisation via isAuthorized action
        $this->Auth->authorize = 'controller';
        //Restrict access to only users with an active account
        $this->Auth->userScope = array('User.active = 1');
        //Pass auth component data over to view files
        $this->set('Auth',$this->Auth->user());
    }
    /**
     * beforeRender
     * 
     * Application hook which runs after each action but, before the view file is 
     * rendered
     * 
     * @access public 
     */
    function beforeRender(){
        //If we have an authorised user logged then pass over an array of controllers
        //to which they have index action permission
        if($this->Auth->user()){
            $controllerList = Configure::listObjects('controller');
            $permittedControllers = array();
            foreach($controllerList as $controllerItem){
                if($controllerItem <> 'App'){
                    if($this->__permitted($controllerItem,'index')){
                        $permittedControllers[] = $controllerItem;
                    }
                }
            }
        }
        $this->set(compact('permittedControllers'));
    }
    /**
     * isAuthorized
     * 
     * Called by Auth component for establishing whether the current authenticated 
     * user has authorization to access the current controller:action
     * 
     * @return true if authorised/false if not authorized
     * @access public
     */
    function isAuthorized(){
        return $this->__permitted($this->name,$this->action);
    }
    /**
     * __permitted
     * 
     * Helper function returns true if the currently authenticated user has permission 
     * to access the controller:action specified by $controllerName:$actionName
     * @return 
     * @param $controllerName Object
     * @param $actionName Object
     */
    function __permitted($controllerName,$actionName){
        //Ensure checks are all made lower case
        $controllerName = low($controllerName);
        $actionName = low($actionName);
        //If permissions have not been cached to session...
        if(!$this->Session->check('Permissions')){
            //...then build permissions array and cache it
            $permissions = array();
            //everyone gets permission to logout
            $permissions[]='users:logout';
            //Import the User Model so we can build up the permission cache
            App::import('Model', 'User');
            $thisUser = new User;
            //Now bring in the current users full record along with groups
            $thisGroups = $thisUser->find(array('User.id'=>$this->Auth->user('id')));
            $thisGroups = $thisGroups['Group'];
            foreach($thisGroups as $thisGroup){
                $thisPermissions = $thisUser->Group->find(array('Group.id'=>$thisGroup['id']));
                $thisPermissions = $thisPermissions['Permission'];
                foreach($thisPermissions as $thisPermission){
                    $permissions[]=$thisPermission['name'];
                }
            }
            //write the permissions array to session
            $this->Session->write('Permissions',$permissions);
        }else{
            //...they have been cached already, so retrieve them
            $permissions = $this->Session->read('Permissions');
        }
        //Now iterate through permissions for a positive match
        foreach($permissions as $permission){
            if($permission == '*'){
                return true;//Super Admin Bypass Found
            }
            if($permission == $controllerName.':*'){
                return true;//Controller Wide Bypass Found
            }
            if($permission == $controllerName.':'.$actionName){
                return true;//Specific permission found
            }
        }
        return false;
    }
}

Once again, the major change here is that the permissions are cached to session and thereafter retreived from session to reduce the workload on your database and cut down page loading times, I have also refactored some code for passing over a list of controllers to the view for which the current user has authorization to access the index action, and I have implemented the ability for you to set a permission as for example users:*, which would give a user access to all actions for the given controller.

Within the session permissions are set as controller:action, i.e. users:index, users:view, and there is a wildcard permission of *, which allows unrestricted access to all controllers and actions for development purposes.

The login page

In order for the system to work, you'll need a login page as follows:

app/views/users/login.ctp

echo $form->create('User', array('action' => 'login'));
echo $form->input('email_address',array('between'=>'
','class'=>'text')); echo $form->input('password',array('between'=>'
','class'=>'text')); echo $form->end('Sign In');

And that's pretty much all the code you need to get up and running.

Setting the system up

Now to prime the system so that you can access it yourself, start by changing the line in app_controller that reads:

    $this->Auth->allow('display');

to read:

    $this->Auth->allow('*');

Assuming you have this set up on localhost:

Now change the line of code in app_controller.php that reads:

    $this->Auth->allow('*');

back to read:

    $this->Auth->allow('display');

And you are all ready to go.

Using the system

By now you have an application where you can add as many users, groups and permissions as you need, each user can belong to multiple groups and each group can have multiple permissions, permissions are expressed as Controller:Action and there is a widecard Controller:* giving controller wide access along with the * wildcard for giving system wide access.

By default all actions except for any named display are protected, but you can override this for any individual controller by adding

function beforeFilter(){
    parent::beforeFilter();
    $this->Auth->allow('display','and','a','list','of','any','other','actions','here');
}

Every view is passed a variable called $Auth which you can test for to determine whether a user is currently logged in, which is an array of the current users record, along with an array called $permittedControllers which again you can test to see whether it exists and if it does, use it to create a rudimentary admin menu, within an element for example.

That's about it for this time, hope you find this update usefull, if you do, please leave a comment or hit the Share This link to help spread the word, and untill next time...

...happy baking!

Comments

Have your say:





Please enter the code