CakePHP Auth Component - Users, Groups & Permissions
Published: on 20/5/08 | Comments (32)
UPDATE:
following on from the great response I have received to this article and numerous suggestions and requests left in the comments, I have now published an updated version of this article, which you can find here, the updated version contains all the code found here plus it has been refactored and improved to add features such as CakePHP 1.2.0.7125RC1 support, along with Permissions Caching and Controller Wide Permissions Wildcard support.
Ok, I've been using CakePHP for about three years now, and I am hooked, it has not only helped me build applications quicker and more efficiently for my clients, but also allowed me to concerntrate on writing the business logic rather than getting bogged down with structural plumbing and security.
So, I thought it was high time to give something back to the community and here it is with my first ever tutorial. Hopefully you'll find it usefull and leave a few comments, or better still, click the Share this link at the bottom of the article to help spread the word.
I'm going to walk you through creating a complete Authentication and Authorization system using CakePHP's Auth component, the tutorial was built using CakePHP 1.2.0.6311 beta, and allows for Users to belong to multiple Groups and for each Group to have multiple Permissions, without going anywhere near the ACL component.
So, without further ado, heres the code:
The Database
You can of course extend this in any way you choose for your application, but I will give you the basics needed to get the system up and running:
CREATE TABLE users (
id int (11) NOT NULL auto_increment,
email_address varchar (50) NOT NULL,
password varchar (50) NOT NULL,
active tinyint(1) NOT NULL,
created datetime NOT NULL,
modified datetime NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE groups (
id int(11) NOT NULL auto_increment,
name varchar(50) NOT NULL,
created datetime NOT NULL,
modified datetime NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE permissions (
id int(11) NOT NULL auto_increment,
name varchar(50) NOT NULL,
created datetime NOT NULL,
modified datetime NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE groups_users(
group_id int(11) NOT NULL,
user_id int(11) NOT NULL
);
CREATE TABLE groups_permissions(
group_id int(11) NOT NULL,
permission_id int(11) NOT NULL
);
The Models
User Model
app/models/user.php
class User extends AppModel {
var $displayField = 'email_address';
var $name = 'User';
var $useTable = 'users';
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 $useTable = 'groups';
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 $useTable = 'permissions';
var $hasAndBelongsToMany = array(
'Group' => array('className' => 'Group',
'joinTable' => 'groups_permissions',
'foreignKey' => 'permission_id',
'associationForeignKey' => 'group_id',
'unique' => true
)
);
}
The Controllers
Users Controller
app/controllers/users_controller.php
class UsersController extends AppController {
var $name = 'Users';
var $scaffold;
function login(){}
function logout(){
$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
app/app_controller.php
class AppController extends Controller {
var $admin = array();
var $allowedActions = null;
var $components = array('Auth');
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('*');
//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 marked as active
$this->Auth->userScope = array('User.active = 1');
//Pass auth component data over to view files
$this->set('Auth',$this->Auth->user());
}
function beforeRender(){
//If we have an authorised user logged in then set up the admin element
if($this->Auth->user()){
$controllerList = Configure::listObjects('controller');
foreach($controllerList as $controllerItem){
if($controllerItem <> 'App'){
if($this->__permitted($controllerItem,'index')){
$this->admin[] = $controllerItem;
}
}
}
}
$this->set('admin',$this->admin);
}
function isAuthorized(){
return $this->__permitted($this->name,$this->action);
}
function __permitted($thisController,$thisAction){
//Ensure checks are all made lower case
$thisController = low($thisController);
$thisAction = low($thisAction);
//Firstly users:logout is never restricted so set up a bypass
if($thisController.':'.$thisAction == 'users:logout'){
return true;
}
//Now if allowedActions has not yet benn created, create it
if(!$this->allowedActions){
//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){
$this->allowedActions[]=$thisPermission['name'];
}
}
}
//Now check the cache to see if we have permission set
foreach($this->allowedActions as $allowedAction){
if($allowedAction == '*'){
return true;//Super Admin Bypass Found
}
if($allowedAction == $thisController.':'.$thisAction){
return true;//Specific permission found
}
}
return false;
}
}
The Views
The login page
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');
Setting the system up
Assuming you have this set up on localhost:
- Go to http://localhost/permissions
- Click on New Permission and enter '*' for the permission name
- Click the submit button.
- Click on New Group and enter 'System Developers' for the group name
- Select the '*' permission.
- Click the submit button.
- Now click on New User
- Enter your email address and a password
- Ensure that Active is ticked
- Select System Developers group
- Click the Submit button
Now change the line of code in app_controller.php that reads:
$this->Auth->allow('*');
to read:
$this->Auth->allow('display');
Now save app_controller.php and refresh your browser, and you should now find you need to log in to access the system. That's it, you now have the basics of an extremely flexible and powerfull Authentication and Authorization system.
Basic Useage
Now you have the system in place, it's pretty easy to use, so here is a quick run through:
Permissions
By default any action named display is not protected, all other actions are.
To grant permission to any group to access an action other than display, add a new permission in the format controller:action and add it to the group for which you wish to grant access.
To allow unristricted access to other actions, for example if you wish to add an open action at users/home, add the following code to your users_controller.php file.
function beforeFilter(){
parent::beforeFilter();
$this->Auth->allow('display','home');
}
This will inherit and override the basic beforeFilter adding the new action. (Don't forget to include any requestAction functions here that you may call in your elements).
Detecting users within your view files.
If a valid user is logged into the system a variable called $Auth is passed to the view in the app_controller:beforeFilter so, if for example you wish to display a link in any of your views or elements to log the current user out, simply do this:
if(isset($Auth)){
echo ' | '.$html->link('Logout',array('controller'=>'users','action'=>'logout'))."\n";
}
Try using:
debug($Auth);
within any of you views to find out all the information you have at your disposal within your views.
An Admin menu for free
As a bonus, the application passes over a variable to view files called $admin, which you can test within your view files or elements, if it's not empty, then the currently logged in user has permission to access some controller:index actions and you can then iterate through the $admin array to create a list of admin links.
Wrapping up
Well that's about all for now, if you don't wont to enter all the code yourself, Download the code here. In a future article I will expand on this to show you a pretty cool admin menu bar I've used in a couple of applications but for now, I hope you've found this usefull, till next time...
happy baking!
UPDATE
Just added the next instalment to this series where I show you how to extend this system with an automatically generated admin menu.
Comments
-
1:
Gustavo Cardoso says
on 25/5/08
Hello Peter,
first, thank you to write this article, it's very useful.
Now, can you explain me why you use "var $admin = array();" (second line of the app_controller's file), i didn't understand it? Why i have to set this variable?
P.S: I'm brazilian abd i'm learling english? :)
-
2:
Peter Butler says
on 25/5/08
Hi Gustavo, glad you enjoyed the article and found it usefull, the $admin variable is processed during beforeRender() at which point it finds out if there are any controller->index permissions set for the current user, adds them to this array and then passes it over to the view file.
In my own applications I then have an element that checks whether this variable is set, and if so, iterates through it generating a list of links.
Just makes for a very easy way of adding an admin menu and something I have seen asked on many forums and pages is how to get a list of controllers defined within an application.
Hope that helps, see the section at the end of the article with the heading An Admin Menu For Free
-
3:
mkhDev says
on 25/5/08
Thank you Peter for this useful tutorial.
I think the correct path for the login page is : /app/views/users/login.ctp
and this line in app_controller : $this->Auth->allow('display');
should be : $this->Auth->allow('*'); at the first, then change it back after creating the first permission.I have a question, why do you load the whole permissions list at each request ? Isn't there any other better solution?
-
4:
Peter Butler says
on 26/5/08
@mkhDev: Hi, thanks for your comment and yes, you caught me out on both typo's which I have corrected.
The permissions list is loaded only on restricted pages as the code is never called for open requests, and this leaves a system wide array ($this->allowedActions) available so that you can check against it in your views and controllers when creating links etc, so that users only see links to code they can actually access, I will give an example of this in a follow up article I am planning for today.
The permissions could be stored in session rather than regenerated, and I will show how that is done in the article later today, however that does have the disadvantage that any changes made to a users membership to groups and permissions would only become available after they have ended their current session.
-
5:
stcggtc says
on 26/5/08
Great work Peter. It works perfect.
I want to associate the table user with another called table people with a foreign key peope_id (The relations in model already created) to take the name from the person in $Auth array.
How can i modificate the code "$this->set('Auth',$this->Auth->user());" in AppController class or function __permitted() or function isAuthorized() to incorporate people name in $Auth array?
Thanks in advance
-
6:
Justin Crossman says
on 27/5/08
Your SQL script includes an 'id' field within the 'users' table of type "int(1)", it looks like you wanted that to be at least "int(11)". Also, I'm experiencing issues if any of the early ID's are missing and not sequential. For example, if the the first user in the DB has an 'id' of 2 and there is no 1 (additionally if the first entry's id is 2 in the lookup tables, permissions & groups tables), there are problems. You can test this by following your instructions, deleting your first entries and following your instructions again. Follow up with me if you'd like to discuss it further.
-
7:
Peter Butler says
on 28/5/08
@stcggtc: Many thanks, glad it's working fine for you, I'm working on a follow up article at the moment and will put something in there for the modification you need.
@Justin: Thanks for pointing out the typo, which I have now corrected, I've not been able to replicate your other problem, are you deleteing and creating users directly within the system or within an external tool (Should really be done in the system to maintain the HABTM tables) and are you ensuring that the new user you created is set to active and assigned to a group with the appropriate permissions?
-
8:
Tim Burton says
on 5/6/08
Thanks for writing this guide,
I am new to cakePHP and glad that I found your guide. I am a bit confused on how to set permissions.
let's say i have a PostsController and I only want user 'foo@bar.com' to have access to Posts and nothing else how would that permission look? I tried Posts:* and it did not work.
-
9:
Peter Butler says
on 5/6/08
@Tim - Glad you found this post usefull, permissions are on a per action basis, so create permission 'posts:index', 'posts:add', 'posts:edit', 'posts:view' and 'posts:delete' if you want to give access to al the scaffolded actions.
The code doesn't have any wildcard checking within the permissions beyond the system wide developer account, but if anyone wants this functionality, let me know and I will include it in a future post
-
10:
John says
on 12/6/08
Thanks Peter
This is great tutorial. I've been using a home brewed User / Group system for a while on all my 1.1 sites but have now moved to 1.2 and wanted to re-write it to make use of the built in Auth Component, your code is fairly similar.
The way I manage permissions in my current system is for ecah group just to show a list of all the controllers and actions and manage it using check boxes - you just tick the actions you want to allow permissions on - it is very easy to understand and use, so I thought I might have a go at attaching that kind of interface to your code.
Personally I find it makes much more sense to use this kind of permissions system in the majority of cases. The built in ACL just seem unnecessarily complex (although clearly sometimes the row based permissions that it is capable of are necessary) - and more to the point it is very hard to make sense of ACL permissions just from looking at the database.
The thing is when I run into problems with something like this, the often first place I look is the database - if you know your data is good then the problem must be code and vice-versa.
Cheers
-
11:
John says
on 12/6/08
The other thing I meant to say was that it might be a good idea to cache the permissions - then you can just regenerate them when you update any of the permissions. It would save quite a number of queries and you wouldn't have to worry about permissions stored in a session going out of date.
Cheers
-
12:
Howie says
on 17/6/08
Thanks for this article. Really useful. I'm new to Cake, coming from a strong Java background. I've spent ages trying to get my head round the ACL and Auth documentation, and it does seem overly complex for most scenarios. I much prefer Role-Based Access Control (RBAC) like you have modelled.
I'll have a go at implementing similar tonight, but I have a question re: your examples.
$this->Auth->allow('<em>'); if($allowedAction == '</em>'){
What does '<em>' represent?
Most applications I have worked on load a User object at login, and store it in session, along with the User's Roles (Groups). I think I'll go for this approach. It is quite normal to expect changes to a User'ss privileges to take effect upon next logon.
Nice work. Lord knows how you gleaned this info from the confusing documentation and examples out there!
-
13:
Peter Butler says
on 17/6/08
@Howie - Glad you found the article useful, the is the result of using markdown for editing my posts and should actually be an asterix, many apologies for any confusion. And yes storing the user object in session would considerably improve performance with the only cost being that a user would have to log out then back if their permissions were changed. I have a few follow up articles planned shortly, so don't forget to subscribe, been a little tied up with a couple of projects the last couple of weeks but planning to update regularly again soon
-
14:
Martin Bavio says
on 17/6/08
Your tutorial is great. But, isnt a better apport to the community to teach new bakers how to use core ACL? I mean, core ACL will work with next versions of the framework, and it will be always there. Also, what you are trying to do sounds like "reivent the wheel", since ACL is there to do exactly what you are trying to do here.
-
15:
Baz L says
on 17/6/08
I really really love this :)
I've fought with ACL for (well, if I say how long, it would be a bit too embarrassing) a long while and haven't gotten it to work for what I need.
I kept thinking that I'd just write my own, then I stumbled onto your site. Funny I found it by searching: "Cakephp ACL"
There's just one slight thing I would modify. In the database, I'd either add 'controller' to the permissions table or allow entry of the permission as 'controller/action' or something, then that way you'd be able to control controllers as well as actions in the database.
-
16:
Peter Butler says
on 19/6/08
@Baz Glad you enjoyed the post, especially as I am a fan of your own blog (and a subscriber to your blog).
The system already uses controller:action for the permission system so you have control over both controllers and actions
-
17:
Josh says
on 8/7/08
Hey,
Thanks for the great tutorial unfortunately I cant get it to work, when I refresh the display its blank, when I go to /permissions its still blank... Any ideas?
-
18:
Peter Butler says
on 11/7/08
@Josh - Sorry you are having a problem and many thanks for your comment and the private email you sent with more details, I have replied directly to your email to try and ascertain a little more info and will do all I can to try and help you get your app up and running.
-
19:
Paul McClean says
on 28/7/08
Hi Peter, Loving your component, it works perfectly for my purposes. One thing I'm having trouble getting my head around. Newly registered users on my site need to be authenticated by email, that is, their active field isn't set to 1 until they get an email and click on a link. This works perfectly, but in my login action, I'd like to let unconfirmed users (active = 0 users) know via an error message on login that their accounts aren't confirmed yet (they currently just get the default error message). I'm having some trouble implementing this in my login action, is there any this can be done?
Sorry if this sounds like a noob question! Thanks, Paul
-
20:
Matthew Miller says
on 29/7/08
Last year I was attempting to make my own framework, but lacking the knowledge and insight to create something robust was beyond my skills since I'm a one man show. CakePHP is greatly along the lines of what I ultimately wanted, but is much better. Last week I spent a few days trying to create a GUI solution to use ACL, but found that the entire process was quite tedious. While I understand how the ACL component works, I need to give myself and my clients access to manage their users in an easy manner. I came across this tutorial, which was very similar to something I created in my own framework last year. I've modified a good bit of it so that it functions like the ACL component in that you can grant or deny a specific controller:action for a Group or User, and it even includes inheritance for permissions. I want to use Cake's built in functionality as much as I can, but you can't have everything. Now to move my users, groups, and permissions away from scaffolding... yay!
-
21:
ndlm says
on 16/9/08
Hello!
i'm new in php and cakephp things.
i implement the code that you have in the tutorial folder but it doesn't work. I can't log in.
i don't know what is missing.
best regards
-
22:
Rick Torzynski says
on 2/10/08
Peter,
Great tutorial - only I can't get it to work. I've tried over half a dozen tutorials on using auth, and not one of them has worked for me. I'm new to cake, but have been using php for a long time.
I followed your tutorial, and added the permission, group and user as you say. But then when I replace:
$this->Auth->allow('*');
with $this->Auth->allow('display');it allows me to login successfully it takes me to my home page at /pages/home.ctp and from there I have a link to /projects/index, but it just takes me back to the login page when I click on that link (that's the other thing I haven't been able to get to work - redirecting to /projects/index after successful login). And when I go into /permissions it also does that. I put the '*' back in there and added the permission projects:index and gave access to System Developer to that permission. But when I change it back to 'display', it still sends me right back to the login page.
I'm using cake 1.2.0, php 4.3.9, and mysql 8.41.
Hopefully this is just something simple I am overlooking - any help would greatly be appreciated.
Rick Torzynski
-
23:
Louis says
on 12/11/08
Hi Thanks for tutorial. I also had trouble with getting logged in when setting permissions other than * then I tried this: I added this in the app_controller: if($allowedAction == $thisController.':*'){ return true;//Specific permission found }
Then I made a permission users:* And now I can log in and access users but not groups, perfect it works, but when I try adding more than one permission like so: users:,groups: or 'users:','groups:' This does not work. What is the correct way of adding multiples here?
-
24:
Ayoub says
on 16/11/08
Hello, am trying to use this solution am having this error : Fatal error: Cannot redeclare class Permission in /var/www/mm/cake/libs/model/db_acl.php on line 321
any idea please?
-
25:
Ayoub says
on 16/11/08
Hello, am trying to use this solution am having this error : Fatal error: Cannot redeclare class Permission in /var/www/mm/cake/libs/model/db_acl.php on line 321
any idea please?
-
26:
wilson says
on 22/11/08
thanks, this article help me alot
-
27:
paul says
2 weeks, 5 days ago
after going to /permissions it automaticly redirects to /users/login, no permissions can be created.
what to do?
cakephp v 1.2.0.7945 RC4
-
28:
Peter says
2 weeks, 4 days ago
@Paul - First thing to check is that you have set $this->Auth->allow('*') in the beforeFilter of your AppController, while you are setting the system up
-
29:
Will says
2 weeks, 3 days ago
@Peter - Same problem as Paul has. The "*" allow doesn't seem to do anything. Possibly something with the new RC.
-
30:
Tom Chapin says
2 weeks, 1 day ago
I can’t seem to get Auth->allow(’*') to work. No matter what I do, it always redirects me to the users/login screen.
This is extremely frustrating, as I can’t even figure out how to create the first initial user, which would let me log in, in the first place.
Also, what if I want to allow the public to access certain sections without logging in? No matter what I put in the Auth->allow() method, it always redirects me to the login screen.
Have you tried using your code on the new CakePHP RC4 release? That's the codebase I’m working with, and I wonder if that might somehow be the problem?
Thank you very much for your time!
-
31:
Pete says
2 weeks, 1 day ago
@Paul, Will & Tom - Could well be something to do with the new release candidate, as this codes been up since May and this problem has not occured previously, and from past feedback and private mails received, the system has been used successfully by a very large number of people. Personally I haven't downloaded the latest release candidate yet simply because I'm putting in 16 hours a day on a couple of client projects at present. As soon as Christmas is out of the way I will take a look at the new release, but for now it might be worth your while having a look to see if the Auth component allow function has changed from the last release.
If anyone else with this system already running has encountered the same problem with RC4, and identified the cause, please post a comment here, similarly if any of the three of you have found the answer since posting, please leave a comment to help everyone out.
Meantime keep checking back, or subscribe using the feed link at the top of the page and as soon as I get a chance I will download the new release and see if I can replicate your problem, then publish a result back here.
Also, if you check the very beginning of this article, there is a note there that this code was replaced and updated with a link to the newer article, with a few additions such as the use of caching on permissions and some refactoring which was done at the time of RC1, so do try the updated code and see if this works for you.
-
32:
bradley says
1 week, 3 days ago
@Tom Chapin
Alrighty.....After googling for the last 3 hours, I decided to roll up the sleeves and take a look.
To fix the problem with RC4 use $this->Auth->allow(array('*'));
:)
This post is now closed to new comments
