Published: on 20/6/08 | Comments (93)
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.
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.
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.
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.
Having begun playing with the new release, I have built this update using the latest release candidate and it's working fine without modification.
So, preamble over, here is the code with comments about where I have changed or re-factored things a little.
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.
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.
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
)
);
}
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
)
);
}
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
)
);
}
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());
}
}
app/controllers/groups_controller.php
class GroupsController extends AppController {
var $name = 'Groups';
var $scaffold;
}
app/controllers/permissions_controller.php
class PermissionsController extends AppController {
var $name = 'Permissions';
var $scaffold;
}
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');//IMPORTANT for CakePHP 1.2 final release change this to $this->Auth->allow(array('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.
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.
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('*');
NOTE for CakePHP 1.2 Final The Auth component has changed slightly in the final release so use $this->Auth->allow(array('display')) and $this->Auth->allow(array('*')) respectively if you are working with the final release.
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.
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...
First off, thanks for your previous post on this. I was able to get your system up and running in minutes. Absolutely a godsend.
However, I just tried out your "new" version from this post and it didn't work the way I expected. In particular, I was using the same database as the "old" version since I am still using autoincremented numbering in most of my online sites. I expected the same permissions and lack thereof to be in play. But I did not get that.
I only have two groups:
Systems Developers group with '*' permissions Staff group with 'users:profile' permissions (this function allows them to edit their own profile as the logged in user).
However, now when a staff member logs in, they are able to get into the groups, permissions, etc.
Any suggestions?
Ok. My bad. I somehow forgot to copy the line $this->Session->del('Permissions'); into the logout function of the users controller. Definitely explains why.
Keep up the good work and I look forward to seeing what else you come up with!
Hey ! first thx for the hardwork :) I've been running through all this tutorial, then copy and paste all the files, set up the right folder, follow all the instructions.... Loged in and :
Warning (2): Cannot modify header information - headers already sent by (output started at /home/*****/www/testcake/app/models/user.php:24) [CORE/cake/libs/controller/controller.php, line 576]
or user.php:24 = blank line and end of script
when I click on the warning link this is what I get :
$status = "Location: http://localhost/views/pages/home"
header - [internal], line ?? Controller::header() - CORE/cake/libs/controller/controller.php, line 576 Controller::redirect() - CORE/cake/libs/controller/controller.php, line 557 AuthComponent::startup() - CORE/cake/libs/controller/components/auth.php, line 309 Component::startup() - CORE/cake/libs/controller/component.php, line 98 Dispatcher::start() - CORE/cake/dispatcher.php, line 307 Dispatcher::dispatch() - CORE/cake/dispatcher.php, line 219 [main] - APP/webroot/index.php, line 84
I'm pretty knew to cakephp so forgive me if I ask for help for something maybe I could solved alone if I was more experienced.
is this a problem someone else encountered ? and how to solve it ? thx for your answer :)
Ok just solved the problem :
http://www.justkez.com/cakephp-cannot-modify-header-information/
it is just about blanc space after the closing ?> php tag to remove
pretty wierd, but anyway now I can use your auth app.
Thx again for the good work :)
great tutorial but im having a problem when i login it redict me to the cake php home page and i tried to change the login_redic but it doesnt work can u help me to redict the logged user to an index
@Wendy & @McMapU, Glad you both found the system usefull and enjoyed the tutorial, both of you managed to answer your own questions before I could respond.
@Nicol: You set the redirection path with the line $this->Auth->redirect('/') in the beforeFilter of app_controller, not sure why your site is redirecting to the cakePHP home page, my first suggestion would be to check your code carefully for any typo's as this system is in use in quite a few production sites of mine without problem, plus quite a few of the responses I've had suggest that other people are not experiencing the same problem. If you still have problems, either comment again, or contact me by email and I will be glad to help however I can to get the system working for you. (I have sent you a private email, which you can respond to if I can be of any further help).
Many thanx to everyone for the feedback so far
Also if anyone has any requests or suggestions for other components or code snippets either drop me a line(see my contact page for email address) or leave a comment here.
Best Regards
Peter
Hi, thanks a lot for a great system. I have some troubles with it. The system not always logs in :-( I have an another model/controller - Posts, and when I list the posts all is OK, but when I'm going to add/edit/delete one of them, the system logs me off and I cannot log in again before visiting the host again (browser cache? I don't know...).
Can you help me with this please?
had a nice time goin thru the tutorial... i copy-paste the code as it it, finally when i hit http://localhost/permissions , i get a permissions page with no record , when i hit 'New permission' link , i get a input box for permission and a empty select box. As u said i enter '*' in permission name ,but the page cannot be submitted unless you mention the group and the group selectbox is empty..... plz help
I tried to manually enter the data in the database..but still when i add new user , i get tis message "Please correct errors below"... if anybody can help plz
@John: Have you added some $validation rules to any of the models, or changed any of the field names, and have you checked your SQL code carefully. Also when you are trying to add a new user, are you getting any kind of error message?
@John: One other thing that just occurred to me, the development environment you are using, have you already used it for developing working applications, the only reason I ask is that there is a mySQL gotcha that has caught me out a couple of times after setting up a new development machine where the line: sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
in the mySQL configuration file (my.ini) needs to be commented out and isn't after a fresh installation. May not be the case with you but its always worth checking through everything as the forms should not require a group to be set when creating your permissions and I know it is working on my system and various other visitors who have gone through the tutorial.
Finally, if you can't get it running, then please zip up your app (including a database dump) and send it over by email and I will gladly take a look at the code and test it on my system for you, you can find my email address on the contact page of this site, though do remember it might not be instant, as well as running this blog, and a local IT company here in Gran Canaria, I am also working on three large scale client projects at the moment and my working days are hitting the 12 hour a day mark at present. But if anyone does have problems, I am always willing to take a look and help anyway I can.
Peter
Hey Peter, I 've been waiting before asking you to find solutions to my problems, but my knowledge isn't that accurate on cakephp (I'm a beginner) :
1- I successfully installed your users/groups/permissions management system, between I really appreciate the way you thought it, it might be a bit long to set up every permissions for groups and users while you have a lot of them, but when it is all set up it rocks !
2- I'm learning cakephp and testing it with the simple blog tutorial, so I've added your system to the simple blog app, I really apologize if I sound noobish ;)
3- The Permissions : a- for testing purpose I sat up 2 accounts admin and user, admin is part of the System Developers group and user part of the registered group
b- System developers have the '*' permission that gives full control over any actions (controller/functions ?) and the registered has no permissions set up at all. note : So if I followed well your tutorial registered shouldn't have access to any actions at the moment.
problem : registered can still access function like 'add post' or 'edit' or delete'. It looks like registered have the '*' permission to, but they haven't.
question : what did I miss ? I've copy and paste all your tutorial, followed it all step by steps. the login system works perfectly, I'm asked to login if I want to access to http://localhost/posts. I can create new user/group/permission and link them all through the process you sat up.
4- to set up a permission on 'add post' function from posts_controller.php, for example, in the new permission form what is the right syntax : a- posts:add b- add
5- how do I name an action 'display' so it is always available ?
6- as I know you have a lot of work maybe someone else would be kind enough to help me to set up a registration system based on Peter authcomponent. this is all I see missing in you componet.
Again I know I'm asking a lot but and hope you'll have the time to respond
keep up the good work
regards Mc Mapu
@McMapu: Hi, glad you like the system, OK your item 3b, first thing to check is that you added the line:
$this->Session->del('Permissions');
to users_controller.php function logout, this deletes the session, if you don't put that in then even if you log out and log back in as the same user, you'll keep the first set of permissions. (See Wendy's comment above)
Item 4. the correct permission would be 'posts:add' or 'posts:*' for access to all post actions.
Not quite sure what you mean with item 5 on your list, action names correspond to the functions you create so just name a function display and put what ever you want to appear in there, if you want other actions to be available to everyone, then in the controller add a function called beforeFunction() and inside that add the lines
parent::beforeFilter();
$this->Auth->allow('and','all','the','open','actions','here');
this basically inherits and overrides the allow array of Auth component that is being set in the app_controller version of beforeFilter().
Hope that makes sense (Tis' Friday afternoon), so for example if you wanted 'index' to be available instead of 'display' use
$this->Auth->allow('index');
For index and display use:
$this->Auth->allow('index','display');
For Item 6 Just create an action in your users controller called 'register' and using the blog tutorial as a guide, create a form and form handler. e.g.
function register(){
if(!empty($this->data){
$this->data['Group']['id']=2;//This adds the user to group id 2 replace with your own group id for registered users.
if($this->User->save($this->data)){
$this->redirect(array('controller'=>'pages','action'='registered');
}
}
}
Then add register to your users controller allow array as shown above with
function beforeFilter(){
parent::beforeFilter():
$this->Auth->allow('display','register');
}
add a file called register.ctp to app/views/pages with whatever HTML you want for a message to send users to after they have registered, and create a view called register.ctp in app/views/users with code such as:
echo $form->create('User',array('action'=>'register'));
echo $form->input('email_address');
echo $form->input('password');
echo $form->end('Join Up');
And that should give a basis to begin with. Depending wether you set the default value of User.active to 1 or 0, the user should now be able to log in, or if you want to authenticate or approve their membership you can write code to send email with an authentication code for them to click which will then set active to 1.
Hope that answers your immediate question and gives you a little to go on with the registration functionality you require, May post an article on extending the system with a registration form if anyone else is interested let me know through these comments and I will post something next week.
Peter
Hey Peter,
this is what I call support :) ! I'm not home at the moment to test your solutions and tips, but thx I really appreciate it.
I'm a graphic designer if you're interested I could work on a template for your authcomponent, I think it would be cool for beginers like me, but also why not for pros, to have a all in one pack authcomponent + registration + template, this way you'll deliver a complete module for cakephp.
from all other authcomponent I've seen, yours is the one which is the most complete and offer unlimmited possibilities. This is really a good point because usually authsytems are too limited.
tell me if you're interested, I've worked a lot on icons and magazines layouts.
Regards
Manuel
Hey Peter,
First of all thanks for your great tutorial. It was the first one so far that really worked for me. I think it's really good that you explained all steps newbie compatible.
However, I was wondering how your solution relates to the CakePHP's ACL. Does it replace ACL or can it be combined with it? I'm sorry if this question sounds too obvious to you. But I'm still a starter.
Cheers Paddy
@Paddy: Hi, the system doesn't replace ACL, rather it is an alternative that works in a different way for systems that don't require ACL. The motivation for writing this was for a few sites I had that did not really fit the ACL model and in the past ACL has been quite buggy and difficult to work with, That said, ACL can be extremely powerfull and flexible, but this system is much easier to use for beginners and a hell of a lot easier to get to grips with than ACL.
Hi.. i successfully implemented the system mentioned above...but i have a requirement as in when an user fills the new user registration form and clicks the submit button,the user should be logged-in automatically without having him to go to login page. Is there any way by which we can authenticate the user without having to go to login page i.e pass username and password to Auth variables an redirecting the user..... plz help
@John - Glad you enjoyed the tutorial and had no problems getting everthing up and running, as to your requirement for automatically logging a new user in on registration, first thing would be to change you sql so that User.active is set to true by default, then you can actually use the built in $Auth component method
$this->Auth->login($data);
To accomplish exactly what you are looking to do, check out cake/libs/controller/components/auth.php for full documentation on how to use the call.
Hope that helps
Peter, great work! Of course there's always a followup question...
I'm trying to implement my own "add_user" view, and action within the user_controller. (mainly because I'm adding a confirm password field) Now, in the view, how do I return the Group Choices that are shown in the scaffolding that's being pulled from the model? I'm studying the API, and helper model, but not quite getting it...
echo $form->create('User', array('action' => 'add_user')); echo $form->input('username'); echo $form->input('password'); echo $form->input('password_confirm', array('type' => 'password'));
echo "the group data that's present"
echo $form->submit(); echo $form->end();
Thanks!
I have a dumb problem.
I set Auth to allow '*', but it doesn't want to go to anything in the User model, hence I can't create new users.
Any ideas?
Ok, I solved my above problem (I had two $this->allow('*'), so Auth got confused I guess).
But here's a deeper issue. It's a bit confusing:
Here's the setup: user: admin has no permissions for "groups" controller. And he's not logged in yet. Um, that's about it :)
I've had this problem in my own, cheap permission setup of Auth. It happens when you try to login with a user that doens't have permissions. I'm not sure what the deal is. Any help would be appreciated.
@Peter: I've got that auto login thing worked .Thanks a lot for ur help.
@All
I don't know where am going wrong, i have replicated the entire code mentioned above. Now when I try to add permissions/group , it gives me this error saying "Please correct errors below." From the posts it is evident that many of you have successfully implemented this system, has any one faced this problem . Any help would be highly appreciated.
I tried putting some extra code in dbo_source.php to get the exact error and i got this following error:
DATABASE ERROR: 1110: Column 'id' specified twice
SQL: INSERT INTO permissions (id,name,modified,created,id) VALUES ('','ty','2008-07-15 07:33:22','2008-07-15 07:33:22','487c52c2-fcac-453a-9e23-012c050e8a54')
Why is it taking the column 'id' twice..... any help wld be appreciated.
Its due to uuids, refer the link below: https://trac.cakephp.org/ticket/4123
but how come some of you managed to overcome this ???
finally got it.... referred this to fix the above issue :https://trac.cakephp.org/ticket/4743
Thanks all.....
Hello Peter,
Thanks for your work.
I'm successfully use your code as you shown it above.
But I got a problem when I try to add my test controller which one doesn't use your system.
class TestsController extends AppController { var $name = 'Tests'; var $uses = ''; // No model for this controller
function test ()
{
}
}
I'm able to access this controller where as there is no one permission defined for it. I cannot access to other controller (users, permissions ...)
Could you help me ?
It seems that using the name Tests for a controller is not good... :-/ All right, thx for your work
Hi Peter.
Thanks a lot for your work. It's a great tut. I was developing a similar system but yours safe me a lot of time.
I live in Gran Canaria too. See you.
Hey,
ThanX again for this.
Using this I've developed an Authentication plugin. Hope everyone likes it:
http://code.google.com/p/cakeusermanagement/
Problem with session... Whenever an user logs in he is taken to the page visited by earlier user before logging out instead of taking him to Auth->redirect.
eg: say if user1 logs in and visits pageA before logging out, now when user2 logs in, he is taken to pageA instead of pageIndex . This is happening coz the last visited page is persisted in the session. when i do p_r($this->Session->read()), i get the following:
Array ( [Config] => Array ( [userAgent] => 41499b4277fcf56a9172c0f11b22c639 [time] => 1216636815 [rand] => 12182 [timeout] => 10 ) [Auth] => Array ( [redirect] => /players/add ) )
Now since 'players/add' is stored in the session, next user is take to the same page.
How to destroy that data from session.
But I am bugged with the fact that even after user1 logs out, the data in the session is not destroyed. I tried putting the following code in the logout action, but it was of no use: function logout(){ if ($this->Session->valid()){ $this->Session->destroy(); } }
Thanks in advance
Thanks for the tutorial! You say to use the form to create a user, but you don't include any code to encrypt the password before storing it in the DB. When I go to login, it can't find my user, and the SQL log seems to be looking for an MD5 string. How would you reccomend I have the User's password encrypted when it is saved? Model::beforeSave()?
@Jesse: Sorry for the delay in replying, moving home at the moment and had no internet all last week, CakePHP's built in AUTH component automatically hashes all passwords when they are created, in order to create your first user follow the instructions in the section 'Setting the system up'. Also make sure you have the auth component in your app_controller components array
I have tried this with plugin named admin. Here is the directory structure. "app/plugins/admin/admin_app_controller.php" and "app/plugins/admin/admin_app_model.php". i have written all the appController code into "admin_app_controller.php". rest is as it is. Now i am getting error :: Class 'User' not found in /var/www/html/cake/app/plugins/admin/admin_app_controller.php on line 97
and on line 97 it is App::import('Model','User');
pls help me out.
I was thinking of adding "title" and a boolean "inmenu" column to my permissions table and using it to build an admin menu. It's very similar to what I have done in the past with my home grown framework. Is this a bad idea -- should I just use cakephp ACL at this point? Also I was going to throw in some containable behavior when you're caching permissions to the session in __permitted() and I'm not sure if there was a reason you didn't. My User model has many hasMany with nothing to do with authentication.
A really good tutorial - especially for people (like me) learning both to code and cake at the same time.
The comments are really helpful and documenting the directory structure and use cases is fantastic.
You should consider submitting it to the bakery (if you haven't already) and possibly helping the documentation team develop standards for submitted code and the examples in the manual.
A question though - I don't think I saw anything on the license it's made available under.
Also is this the best approach if most of a site open - as each view is checked to see it the user has permission?
Sorry, but I think this is riddled with holes, even after the refactoring, and would encourage all beginners to discuss this with seasoned Cake users before digging in.
The most glaring is this: $this->set('Auth',$this->Auth->user());
There is no need whatsoever(besides, it is a break of MVC), to access this component from the view. Everything the Auth component does that may be necessary in the view is available in the Session, and reachable using SessionHelper.
I also find the __permitted() to be a kludge. You have a login() method that has access to the User model because it is in the UsersController. Populate the session with your group data then, in 2 lines, and be done with it. Then all of your tests for group data will be possible using either Auth or Session component, without such heavy logic.
Peter, I'm sorry it took me so long to find your blog, and I look forward to more Cake articles.
Hey Peter, great article.
@TommyO:
Though I agree that "$this->set('Auth',$this->Auth->user());" is unnecessary, it does not break MVC. Setting the user array in the view does not equate to "accessing the component from the view"
This is like saying "$this->set('posts', $this->Post->find('all'));" is accessing the model from the view. That's incorrect. All that's being done in both those lines is setting a data array to the view, which is perfectly acceptable and expected.
Hi Peter, Thanks for the great article. I'm just getting started with Cake so maybe I'm missing something. I do have everything installed and operational, but I'm not seeing a way to associate a permission to a group or a user to a group. Did I miss something or am I not understanding a portion of the code? FYI, I can add new users, groups and permissions. Thanks again for taking the time to write this, Rob
Wonderful article, I have one reminder, I had a problem with camel cased controller functions. You simply need to remember that you set low($thisAction) in app controller so that you need add permissions in lower case or it wont work.
Thanks again for your efforts, highly useful!
Sean
@Peter
I wrote some of the earliest documentation on using the Auth component, and this is probably the most comprehensive example I've seen.
Why don't you add this information to the online CakePHP manual over at http://book.cakephp.org and help out the entire Cake community.
Is there an easy way to determine from within the controller what the username of the current logged in user is? I'd like to include it in my audit trail. Thanks!
@Tommy0 I think the "problem" (if you can call it that) is that Auth requires "isAuthorized" to do most of it's heavy lifting.
So trying to put that __permitted functionality in login() would be a weird task.
I think I like Chris' definition of this the best: "How I failed at ACL", lol. ACL is all well and good, when/if you can get it to work, which I haven't been able to.
I'm very grateful for something like this. Also, if you decide to stick this in a CMS or something, your users don't need specific knowledge of ACL to just use it. Things look like URLs, in there, so everyone's happy.
Undefined index: Permission i get this warning while i login first time, and permissions stored in database is not writing in session array.
I think the containable version of $thisGroups is like so:
$thisGroups = $thisUser->find( 'first', array( 'conditions' => array( 'User.id' => $this->Auth->user('id') ), 'contain' => array( 'Group' => array( 'Permission' ) ) ) );
Hey Peter,
Excellent tutorial you have here! I've been able to easily deploy it and use it with out much headaches, save for one, which took me a while to figure out...
In your app_controller, the following should be changed from this:
foreach($permissions as $permission) { if($permission == '*') {
to this:
// Iterate through permissions for a positive match. foreach($permissions as $permission) { $permission = low($permission); if($permission == '*') {
Without converting the $permission variable to lower-case, one will not be able to create permissions with upper-case letters, such as this: "Users:profile".
It was driving me crazy for a couple of hours, until I decided to go through the permissions code line by line and noticed you're not converting EVERYTHING to the same letter case. :)
Keep up the awesome work!
Hi all
I am new to CAKE PHP FRAMEWORK.
Can you guys please give me some ideas on how to make use of this?
I have integrated everything from
cake_1.1.19.6305.zip
but i dont know how to make the further movement....Please give me your review.....
where to place the HTML files ,where i can get the result...Please if you....
Hi Peter, great sequence of articles.
I Was wondering if you had any luck with this and RC 2? Havent yet tried it yet, but am about to try tit with RC1 (which is stable anyway) but was just wondeirng on the overall upgradeability of the system of Permissions you outline here.
Hi Peter, Great article and tutorial as well. I am trying to put this system into plugin. everything works fine. But when i use /users/logout, and then if i try to login i get warning of "Undefined index: Permission [APP/plugins/admin/admin_app_controller.php, line 101]" , and interesting part is if i clear cookies of name "CAKEPHP", i can login and this warning doesnt appear, i am using version RC2 of cakephp.
Hi Peter,
Great article!
But in Firefox 3 I have to login 2 times before I am logged in. IE and Opera works great.
Gr Henk
Hi peter
If i change the value of $this->Auth->loginRedirect to my preferred value it doesnt work . instead of redirecting to my preferred page it goes all the way to the default localhost/permission . i think the cache is not being cleared properly ?
thanks Ivan
I suggest that you also convert $permission to lowercase in this part of the code:
...since $controllerName and $actionName are both in lower case. The values in the DB might contain uppercase characters.
Thanks a lot for this great system.. it works fine .... Now I can use controller:* .. Are there any way to use the *:edit ?
Thanks you again and goodbye for ACL :)
Huge thanks for the article, and hats off to you for responding to all the comments. This is exactly what the cake community needs.
I agree with Nahedh, a wildcard for controllers would also be good e.g. a valid permission value of *:delete
Great work and thanks again, Chris
Hi again, I have managed to get this working now, it was really easy for me to plug this into a large project I'm working on. I then went on to bake the controllers and then the views for the new users, groups and permissions controllers. Now, when I add a user, the password is not hashed when the record is saved. I assume that the scaffolding does this automatically, but once baked that functionality is not persisted.
Do you know what kind of hashing the scaffolding uses so that I can implement it in my baked user controller? I suspect it is salted as well, but any combination of md5, sha1 and my salt I have tried have not produced the same output.
Thanks, Chris
Thank you again for this great code..
I have done more one step to make the edit of Permissions more easy for normal users, by create one extra field 'title' in permissions table to save a normal text for every permission.
then in Permission Model I added :
var $displayField = 'title';
It's easy mow to modify Permissions by any normal user.
I have changed the __permitted function to not loop through every value and instead use array_search. I also just read directly from the session instead of assigning it to a variable. Here is the code:
if (array_search('', $this->Session->read('Permissions')) > 0 ) { return true;//Super Admin Bypass Found }elseif (array_search($controllerName.':', $this->Session->read('Permissions')) > 0 ) { return true;//Controller Wide Bypass Found }elseif (array_search($controllerName.':'.$actionName, $this->Session->read('Permissions')) > 0 ) { return true;//Specific Permission found }elseif (array_search('*:'.$actionName, $this->Session->read('Permissions')) > 0 ) { return true;//Specific *:Action found
}else {
return false;
}
See above also for adding *:action functionality
Under cake 1.2 stable, you will need to use $this->Auth->allowedActions = array('*'); instead of $this->Auth->allow('*'); when when setting up the first accounts and permissions.
As it is, I cannot get this to work with 1.2.0.7962 (cake 1.2 final).
However, I can make it work if: 1. Change the tables to use numeric primary key. 2. skeemer's suggestion (#62)
@skeemer - many thanks for finding the fix for this, and yes it is due to a slight change in the CakePHP final release Auth component, you can also use $this->Auth->allow(array('*')) to achieve the same thing as contributed by Tom on the arlier version of this article and I have put a note in the article above.
@pete: did you manage to successfully edit Groups and add permissions and users?
I can't seem to get it to work unless I changed the PKs to int.
hi Pete, and thanx for this huge great post about auth. i have done what u say above but when i try to login sometimes it does not login and redirects me to login page, when logs in, doesnt show me any page, just redirects me to home page. does anyone have any ideas, i am very confused :S
Great work here. I have tried both your posts on this Auth system. The first system obviously didn't work out for 1.2, i am much too lazy to try it on 1.2 beta. However this one really worked. or so I thought.
After a lot of debugging and playing around. I see that the "Permissions" actually doesnt work. Users can access all the areas once logged in, regardless of permissions.
maybe i am doing something wrong, but doesnt appear to be the case so far.
I feel this is really usable, so I request if you can throw some light on why the permissions are being ignored. After all the whole work is to get group based permissions. Else a simple auth takes care of more than enough.
Any way we can get this working for Postgresql? The boolean type for Postgres is boolean, not tinyint as it is in MySQL, thus the scaffold for add/edit of User shows a text box for the Active field instead of a checkbox. If I change the database from smallint to boolean, your code fails. I think the $this->Auth->userScope = array('User.active = 1'); is the fault. Since the values are storred for boolean as TRUE or FALSE, not 1 or 0.
Any idea if the userScope can be like: User.active =1 OR TRUE?
Also, is it possible to flash a message to the user if they do not have permission to an action, instead of just doing nothing??
Thank you very much.. its really a great article. Keep it up. I have a little query, can you please tell how to show a navigation menu when a use logged in, as per permissions of users ? Say for example administrator should be displayed links for all actions of Users controller, where as some other should be displayed only link for changePassword action of users controller. is there any way to automate this process ? or we have to check in view/layout who logged in and generate its navigation menu from DB or using if conditions. Thanx & Regards Sukhwinder
Thanks for this wonderful article. I setup the Auth Component and User Permissions in a very short time. But I encountered this peculiar problem. When I log in and out with the same user, after the third try, I can't log in again. Even when I clear the cookies. Did any one encounter the same thing?
My Security level is still at low, with timeout set at 12000.
Is there some logic in cakephp that prevents user from logging in and out multiple times within a time frame? :P
Hi, thanks for the article, i'm having a problem, when the session expires, if i click in a ajax link i get a memory error instead of being redirect to the login page, how can i resolve this? Thank you
This rocks. I started down the road of implementing Auth and ACL until I found this. ACL may be powerful but I prefer this solution as it provides me the control I need without all the complexity. Thanks for publishing this.
From the view, how would I check if a user is in a particular group or groups?
e.g if user is in admin group show link to administrative control panel.
any help, from anyone, is greatly appreciated.
cheers
@Richard - Probably the easiest way of making a list of groups available site wide, wouyld be to pass them over in either the beforeFilter or beforeRender functions of the AppController, Use App::import('Model','User');$User = new User;$this->set('user',$User->findById($this->Auth->user('id')); to pass over a complete record of the current logged in user for all your views, or alter the find statement to pass just what you need. Hope that helps
@ Peter - Thanks alot man. Really appreciate your help. But will this cause any problems if a member is in more than one group?
@Peter - Sorry, me again. Groups are now in the $user array, but not 100% on how I check for these in the views. Should I be creating a helper function?
So I could do something like inGroup(array('admin', 'members')) or inGroup('admin').
Goal is to be able to do something like the following;
if (inGroup('admin')) { // show some cool admin-specific stuff }
Cheers man. Great tutorials!
Hi I think that work is amazing, question how I can set the permision when the action have a parameter
function display_article(id=null) {}
how you set permission on articles/display_article/1
@Richard - Hi Richard, yes creating a helper would probably be the cleanest way to achieve what you are looking to do, using your example you would then create a function in your helper call inGroup and pass it both a list of groups you are checking for along with the $user variable that we discussed earlier, the function in your helper could then check fore the presence of the groups within the $user['Group'] array and return either false or true. An alternate way of approaching the problem would be to make use of the __permitted() function in the app_controller, which you can call from any controller, this checks wheteher the logged in user has access to a particular controller/action pair, using this you could pre-process a list of links and send over an array of permissions to check against to your view.
@Jorge - Unfortunately, the system as it stands only authorises down to the controller/action level, if you need to authourise down the the parameter level then you may be better off switching to an ACL based solution. One way of achieving the sort of functionality you require and which I have used is to do a simple check in any controller for whether the object you are working with belongs to the logged in user using some thing like if($object['Object']['user_id']==$this->Auth->user('id'){//then allow}else{//disallow}, That technique has worked for me in systems I've built using this type of Authorisation system, hope that helps.
When the session expires, if you click in a Ajax link it shows a error memory limit exceeded and doesn't redirect to the login page, anyone knows how to fix this?
Is there any way possible you could provide a very step by step tutorial on creating a one-person login admin system to manage the posts from the blog tutorial on the cake site?
I have been looking for a simple tutorial that was just a one-user system (for an admin) to control the posts for the blog tutorial.
Thanks and please let me know!
Hey, first of all, you rock, second, you rock again.
I was coding something very similar to that when I found this post, well, you saved me a lot of work, i just adapted what I was doing to what you did and looks like I have it is working.
I have made some changes to the code however, maybe that help something else.
The first thing is that I have a very large system and my User model is related to a lot of other models through many hasMany relationships. When you get all the User data just to fetch the Group stuff it loaded thousands of unecessary records. So I changed this:
[code] //Now bring in the current users full record along with groups $thisGroups = $thisUser->find(array('User.id'=>$this->Auth->user('id'))); $thisGroups = $thisGroups['Group']; [/code]
to this:
[code] //Now bring in the current users full record along with groups $thisUser->contain(array('Group', 'Group.Permission')); $thisGroups = $thisUser->find('first', array('fields' => array('id'), 'conditions' => array('User.id'=>$this->Auth->user('id')))); $thisGroups = $thisGroups['Group']; [/code]
Using the containable behavior I unbinded all the unecessary models and binded only the Group and Permissions, which seens like is what I need. Also, my users table have 80 fields and I dont need any of those, so using the fields option I fetch only its id.
By now I have made only this minor changes, but I'll have to modify it a little more. I'm not sure yet what I'm going to do, but what I need is:
Sometimes I need to control more than the access to an action, for example: A User can Edit a Post AND A User can Edit only the Posts hes the owner, got it? Once I find a solution I'll come back here and let you know, but if you have any ideas, please let me know.
By now thats it, thanks very much for providing this code and being so kind to everyone, like I said before, you just rock.
My friend on Facebook shared this link and I'm not dissapointed at all that I came here.
Hello, thanks for this absolutly great tutorial! You saved me al lot of work and time!!!
Thanks, Sebastian
VERY USEFUL TUTORIAL! The only thing that doesn't work for me is the login redirect... ??? I still haven't found a workaround for this... Thanks, Alessandra
@ Alessandra - I have replied by email directly asking for a little more information and if you can send me a little more info, specifically which version of cake you are using, exactly what you are trying to accomplish and some examples of the code you have so far, then I'll do my best to help, the login redirect is actually a function of the Auth component itself rather than this system and I have also found the Auth component to be a little quirky from time to time.
Hi Your tutorials are fabulous, to say the least! I have bookmarked studiocanaria.com :)
I am having a problem with editing user accounts. When the admin edits the user detail, the hashed password is displayed in the form (I could live with that). However when the form is submitted, the system reads it as different password and re-encrypts it (there by changing the password altogether).
Yes, I am using scaffolding, which is one reason. Is there any way to circumvent it. I am doing a prototype and dont want to spend time creating controller/view forms.
thanks
@Sabkaraja - Many thanks and glad you like the site, I haven't had a chance to publish anything new for a while simply due to the amount of work I've had on the last few months, but stay tuned as there is a lot of new stuff planned very soon.
In answer to your question, unfortunately, due to the way that the Auth component works, the actual password is never saved in the database and can't be simply converted back as md5 is essentially a one way hash, so any automatically populated form is going to display the hash and on re-saving, hash it again, so the only answer would be to create an edit view without the password field to accomplish what you need, the scaffolding itself is simply not smart enough to know its a hash, alternately if you use scaffolding a lot you could look at creating your own custom scaffold views in the views/scaffold folder, base it on the original in the cake libs, but put a little bit of code in to detect and not display the password field. It's not a perfect solution, but then scaffolding is only really there for quick mock ups and simulations, personally I rarely use it apart from initially testing that I have all the model relationships set up properly.
I have been using your auth system and loving it. I have run into a problem in my app that is separate but related to your component when I am trying to integrate the Security component into my app to force SSL for certain pages like the login page for example.
Here is the situation. I have followed the example code in the Security component section of the Cake Book that redirects to the SSL request for certain controller actions in my app. I can log in just fine, your Auth system works great here. However, when I switch back from the SSL page requests to non-SSL page requests I lose the session data that stores the Auth info and am logged out.
The solution I came up with to solve this was to keep the users login id in a cookie. This way I can use the cookie to log the user back in when I switch to the non-SSL request. I added some extra code to the following files:
file: app_controller.php
function beforeRender() { // if we do not have Auth check to see if we have a login cookie and if so use it to log us back in if(!$this->Auth->user()) { $cookie = $this->Cookie->read('Auth.id'); if (!is_null($cookie)) { debug($cookie); if ($this->Auth->login($cookie)) { // Clear auth message, just in case we use it. $this->Session->del('Message.auth'); $this->redirect($this->Auth->redirect()); } else { // Delete invalid Cookie $this->Cookie->del('Auth.id'); } } }
.... // rest of function
}
file: users_controller.php
function login() { // code to execute after Auth magic if ($this->Auth->user('id')) { // set login cookie $cookie = $this->Auth->user('id'); $this->Cookie->write('Auth.id', $cookie);
$this->redirect($this->Auth->redirect());
} if (empty($this->data)) { $cookie = $this->Cookie->read('Auth.id'); if (!is_null($cookie)) { if ($this->Auth->login($cookie)) { // Clear auth message, just in case we use it. $this->Session->del('Message.auth'); $this->redirect($this->Auth->redirect()); } else { // Delete invalid Cookie $this->Cookie->del('Auth.id'); } } } }
function logout() { // set login cookie $this->Cookie->del('Auth.id'); }
I think I've figured out the weird thing people are seeing on the redirect login. Like why it doesn't redirect to the value set in the controllers beforeFilter()...
//Set the default redirect for users who login $this->Auth->loginRedirect = '/userspage/';
This value is NEVER used because of lines 307-309 in the component file auth.php
What happens is that the code checks for the login url and if it's present AND there's a referrer in the request then it is written to the session. So unless the user is using something like norton privacy (which always blocks the referrer url in browser requests) the 'Auth.redirect' in the session will always be set to the page from which the user clicked a link to the 'login' page.
Since this will rarely be the protected page you want to redirect the user to after they login, the user will basically always be redirect to the 'home' page or some other page (the one they were on before login)
I think this should be changed in Auth myself but your can either hack it (comment out these three lines) or set Auth's var $autoRedirect = false; and handle the redirect yourself on your login controller
At least I think this is what's going on
OK Here's how to fix some of it with hacking the core I didn't want people who clicked on login from the home page to get redirected back there (maybe other pages it makes more sense) so I just check for the '/' in my login method
function login()
{
if ($this->Session->check('Auth.redirect'))
{
$redir = $this->Session->read('Auth.redirect');
if('/' == $redir && $this->Auth->loginRedirect != $redir)
{
$this->Session->write('Auth.redirect', $this->Auth->loginRedirect);
}
}
///....
}
Studio Canaria is the web site of freelance web developer, Peter Butler. Articles on this site relate to designing, developing and marketing modern web applications.
CakePHP Auth Component - Users, Groups & Permissions Revisited
CakePHP Auth Component - Users, Groups & Permissions