Note: This is for CakePHP 2.x, for 3.x please see the bottom of the article.
The CakePHP built in row based CRUD auth is way too powerful and way too slow and memory consuming.
In 99% of all cases there is no need for that. Also, I never really used the groups + roles of the core ACL. Basic groups usually do the trick.
If you just want to have some basic control over the access to specific actions, you need something else.
Here it comes.
Demo (NEW)
A live implementation can be found in my fun app cakefest.
Note how lean and clean the User model and the controllers are. Separation of concerns. DRY. Cool π
Preparations
Please make sure the Tools Plugin is properly loaded (see the plugin readme for details).
If you plan on using prefixed routing (admin, …), enable those in your core.php or bootstrap.php.
I assume you already got the AuthComponent included in the $components array of your AppController.
You probably also excluded all public views with something like
$this->Auth->allow('contact_form'); // in beforeFilter() of the specific controllers
This here (in the contact controller) makes Auth skip this action completely. The action will be accessible to everybody right away.
This is especially important for your login/register actions:
// UsersController
public function beforeFilter() {
parent::beforeFilter();
$this->Auth->allow('login', 'logout', 'register', ...);
}
Those actions should never trigger any Authorize module. All other actions then use our ACL to determine if access is granted or not.
You probably got a Role model (User belongsTo Role / User hasAndBelongsToMany Role) attached to the User.
If you don’t want this, use Configure to store your keys like so:
// in your config.php if applicable or using Configure::write('Role', array(...))
$config['Role'] = array(
// slug => identifier (unique magical number or maybe better a constant)
'superadmin' => 1,
'admin' => 2,
'moderator' => 3,
'helper' => 4,
'user' => 5,
);
You should at least have a user
– and maybe an admin
role – for it to make sense.
The advantage here: At any time you can switch from Configure to a Role model + roles table and vice versa without having to change much.
You must also have some kind of Authentication in your AppController:
$this->Auth->authenticate = array('Form'); // Uses username and password for login
Important: At least one type of authentication is necessary for any Authorize
module to be usable.
So far so good. You can login/logout and once you are logged in browse all non-public pages.
Even admin pages, of course. Thats where the TinyAuth class comes in.
TinyAuth
The code can be found at github.
First of all include it in your beforeFilter() method of the AppController:
$this->Auth->authorize = array('Tools.Tiny');
Alternatively, you could pass it as component settings right away:
public $components = [
...
'Auth' => [
'loginRedirect' => ...,
'logoutRedirect' => ...,
'authenticate' => ['Form'],
'authorize' => ['Tools.Tiny']
],
];
Now create a file in /Config/ called acl.ini like so:
[Tools.Countries]
* = superadmin ; this is a comment
[Account]
edit,change_pw = *
[Activities]
admin_index,admin_edit,admin_add,admin_delete = admin,superadmin
index = *
[Users]
index,search = user
* = moderator,admin
The format is normal PHP INI style. I already included all kind of examples. * is a placeholder for "any".
The plugin prefix for controllers is not necessary as of now (maybe for CakePHP 3 where the same controller name is allowed multiple times due to PHP5.3 namespaces).
Comments in INI files start with ";".
Explanations:
- Superadmin can access all Countries actions of the Tools plugin
- Account actions are accessible by all roles (and therefore logged in persons)
- Activities can be modified by all admins and listed by all (logged in persons)
- Users can search and list other users, but only moderators and admins have access to all other actions
That’s it. Really easy, isn’t it?
Some details
TinyAuth expects a Session Auth User like so:
Auth.User.id
Auth.User.role_id (belongsTo - role key directly in the users table)
or so:
Auth.User.id
Auth.User.Role (hasAndBelongsToMany - multi role array containing all role keys)
As you can see it can manage both single and multiple role setup.
That’s something the core one lacks, as well.
The current configuration is cached in the persistent folder by default. In development mode (debug > 0) it will be regenerated all the time, though. So remember that you have to manually clear your cache in productive mode for changes to take effect!
For more insight into the different role setups see this Wiki page.
Quicktips
If you have a cleanly separated user/admin interface there is a way to allow all user actions to users right away;
$this->Auth->authorize = array('Tools.Tiny' => array('allowUser' => true));
Only for admin views the authorization is required then.
If you got a "superadmin" role and want it to access everything automatically, do this in the beforeFilter method of your AppController:
$userRoles = $this->Session->read('Auth.User.Role');
if ($userRoles && in_array(Configure::read('Role.superadmin'), $userRoles)) {
// Skip auth for this user entirely
$this->Auth->allow('*'); // cake2.x: `$this->Auth->allow();` without any argument!
}
What about login/register when already logged in
That is something most are neither aware of, nor does the core offer a out-of-the-box solution.
Fact is, once you are logged in it is total nonsense to have access again to login/register/lost_pwd actions.
Here comes my little trick:
// In your beforeFilter() method of the AppController for example (after Auth adapter config!)
$allowed = array('Users' => array('login', 'lost_password', 'register'));
if (!$this->Session->check('Auth.User.id')) {
return;
}
foreach ($allowed as $controller => $actions) {
if ($this->name === $controller && in_array($this->request->action, $actions)) {
// Flash message - you can use your own method here - or CakePHP's setFlash() - as well
$this->Common->flashMessage('The page you tried to access is not relevant if you are already logged in. Redirected to main page.', 'info');
return $this->redirect($this->Auth->loginRedirect);
}
}
It is also visible in the linked CakeFest app demo.
UPDATE 2012-01-10
The auth model can now be anything you like. It doesn’t have to be Role
or role_id
.
The new CakePHP 2.x uses "groups" per default.
You can easily adjust that now by passing
or aclModel
=> 'Group'
to the Tiny class, for instance.aclKey
=> 'group_id'
UPDATE 2013-03-10
Some will be happy to hear that the deeper "contained" Role array is now supported besides the flat array of role keys. This deep/verbose array of roles has been introduced in CakePHP 2.2 with the new "contain" param for Auth. So it made sense to support this in TinyAuth. See the test case for details.
UPDATE 2013-06-25
A new option allowAdmin
makes it now possible to use TinyAuth even with less configuration in some cases. True
makes the admin role access any admin prefixed action and together with allowUser
(allows all logged in users to allow non admin prefixed URLs) this can be used to set up a basic admin auth. No additional configuration required except for the adminRole
config value which needs to be set to the corresponding integer value.
Of, course, you can additionally allow further actions. But if you just need to quickly enable an admin backend, this could be the way to go.
Notes
NOTE 2012-02-25
It seems that especially new-beys seem to mix up the meaning of *
in the ACL. Although it is already laid out in the above text I will try to make it more clear:
This any
placeholder for "roles" only refers to those users that are logged in. You must not declare your public actions this way!
All those must be declared in your controller using $this->Auth->allow()
(in earlier versions of CakePHP `$this->Auth->allow(‘*’)).
The reason is that Authenticate comes before Authorize. So without Authentication (logged in) there will never be any Authorization (check on roles).
NOTE 2013-02-12
You can use this in conjunction with my Auth class for a quick way to check on the current user and its role(s) anywhere in your application:
App::uses('Auth', 'Tools.Lib'); // In your bootstrap (after plugin is loaded)
if (Auth::id()) {
$username = Auth::user('username');
// do sth
}
if (Auth::hasRole(Configure::read('moderator'))) { // if you used configure slugs
// do sth
}
if (Auth::hasRoles(array(ROLE_ADMIN, ROLE_MODERATOR)) { // if you used configure and constants instead of magic numbers
// do sth
}
See the inline class documentation or the test cases for details.
Upcoming
A shell to quickly modify the INI file (and batch-update for new controllers etc) should be ready some time soon.
There might some day also the possibility to use some CRUD backend to manage the ACL (either via database or modifying the INI file).
If someone wants to help, go for it.
2014-09: CakePHP 3.0
With the release of CakePHP 3.0 I upgraded the TinyAuth code and moved it into an own repository.
Please see the wiki page there for docs and migration guide.
This is really great and something sorely missing from the core Cake distro. My only suggestion is that you do not assume the reader has so much in place already. I suspect most people who are looking for something like this are doing so because they could not get the complicated aro/aco stuff working easily and would love a step-by-step of getting tinyAuth up and running.
Also, the examples and tutorials in the cookbook 2.0 have users/groups tables instead of users/roles. You might want to specify the schema your plugin expects since it is different from those.
There is one thing I am having trouble with. When an action is not allowed, the site just hangs and returns nothing. Any suggestions on how to have it return some kind of page, or return back to where it came from with a flash error message?
Here is what I have in my AppController beforeFilter()
Good points. I will have to refine it.
Ok, so roles in 1.3 have changed to groups in 2.0 then? But they still seem to be 1:N only (multiple roles/groups per user not possible). The documentation doesn’t reveal much.
About your problem:
Your Auth component is responsable for that. The Tiny (or any other Auth for that matter) can only return true/false or the right to access this page. It used to redirect to /. But I opened a ticket (http://cakephp.lighthouseapp.com/projects/42648/tickets/2390-auth-component-should-not-redirect-to-index-if-loginredirect-is-set) because it doesnt make any sense if you are a logged in user (you would want to be redirected to $this->Auth->loginRedirect).
And thats exactly what the core Auth component does:
OK, I think I figured out part of the hanging issue. When a page is not authorized, it is trying to redirect to /, but that is also not authorized. I have not figured out how to specify / in the acl.ini file. It is probably obvious, but I’m still a cake newbie.
Well, that is a misconfiguration π Your homepage (/) should always be public. Use $this->Auth->allow() in your controller for this action.
Does this work in 1.3?
I always get a failure after login:
Mein Model ist so definiert:
Any Hints?
Good work - keep on!
You need to make sure the session contains the role information after logging the user in:
HABTM (multiple roles per user):
User.Role.id1,id2,…,idx,
BT (one role per user):
User.role_id
got it, thanks again for the quick support! Is there a easy way to build a dynamic menu with the acls? (It depends on the role if a user can see the menu item)
Keep up the good work!
I updated the class to allow other parent model relations besides "User<–>Role". It should be closer to the cookbook now, where they use "Groups".
@jetwes
There sure is a way to do that. I would like to use sth like that very much, as well.
Basically we would need a helper to display certain "navigation" blocks and hide the ones you don’t have access to. With caching this should be not too resource-eating…
Hey Mark,
Thanks for the good work.
I am saying its good, because it looks like that & you won’t claim it being the fastest & easiest..
I am a newbie in Cake, I have setup cake2.0 & I tried to follow your instructions but like "Matt" said, you can still make the tutorial more clear & to the point & step by step. I got confused at points like –
"If you donβt want this, use Configure to store your keys like so:"
I took some time to figure out that I need to do this in bootstrap file using Configure::write.. I am still not sure if this is the right way..
Basically, steps starting from we downloading cake & till the point where we are ready with a basic setup will be something that will help this a lot
Thanks & Hoping you help me out
Actually .. Something like the book tutorial – http://book.cakephp.org/2.0/en/tutorials-and-examples/simple-acl-controlled-application/simple-acl-controlled-application.html, will be really good π
Does this work in 1.3?[2]
No. Pretty sure it won’t work with < 2.0. Mainly because Auth greatly improved in 2.0 and works pretty different from what it used to.
But with some effort you might be able to downgrade it.
Hi, thanks for the code. But how do I use it? Where do I put tinyauthdb? https://github.com/justinledwards/tinyauthdb/blob/2.1/app/Controller/Component/Auth/TinyAuthorize.php
Then do I need to configure the Model if I’m not going to use a HABTM table? I’ll be using a static array of roles instead. Could you add a few lines how to set this up and get running? Many of us are new to Cake, but have stringent job time limits π Thanks.
I cannot authorize, although I pass the password authentication step.
authorize() in TinyAuthorize.php receives $user that lacks any joins to roles table:
array(
‘id’ => (int) 6,
‘username’ => ‘rihad’,
‘created’ => ‘2012-05-15 16:30:10’,
‘modified’ => ‘2012-05-17 16:36:24’,
)
so of course it can’t find $user[‘Role’]
Here’s my user class:
class Milli extends AppModel {
public $hasAndBelongsToMany = array(
‘Role’ => array(
‘className’ => ‘Role’,
‘joinTable’ => ‘roles_users’,
‘foreignKey’ => ‘user_id’,
‘assosciationForeignKey’ => ‘role_id’,
‘unique’ => ‘keepExisting’));
}
class Role is empty.
class MilliController extends AppController
public $components = array(
‘Session’,
‘Auth’ => array(
‘loginRedirect’ => array(‘controller’ => ‘milli’, ‘action’ => ‘index’),
‘logoutRedirect’ => array(‘controller’ => ‘milli’, ‘action’ => ‘index’),
‘authenticate’ => array(‘Form’ => array(‘userModel’ => ‘Milli’)),
‘authorize’ => array(‘Tiny’ => array(‘aclModel’ => ‘Role’)),
‘loginAction’ => array(‘controller’ => ‘milli’, ‘action’ => ‘login’)));
Please help…
sorry here’s the code again:
Please help…
tinyauthdb is not my project. so i am not familiar with that.
regarding your second post – did you place it in the plugin or in the app?
make sure recursive is high enough.
I use my own AuthExt class which does that all for me.
You might want to do this, as well.
PS: with cake2.2 you can now set containable for this data, as well.
Thanks for replying, Mark. I put it in my app’s Controllers/Components/Auth/
recursive is 1 by default and should be enough"
"1 Cake fetches a Group, its domain and its associated Users"
I’ll still try setting it to 2 or 3 tomorrow and see.
Other than that should the retrieval of user data and joins happen automagically? All I have is a user model with HABTM to roles through roles_users. No reverse relationships of any kind.
The core itself usually only supports belongsTo (single role) – therefore the user itself should have sufficed so far.
That’s why I recommend the AuthExt component to modify it accordingly.
You mean a class that subclasses TinyAuthorize? Then how would I change "recursive" from there? Isn’t it in the model’s config?
No, subclassing AuthComponent and overriding the part where it only fetches the User record itself.
Or – as mentioned before – ugprade to 2.2 and use contain
Hi, Mark. As you suggested I’ve added recursive => 1
And now findUser() in BaseAuthenticate.php indeed fetches the user deeply with its Role. But TinyAuth still is getting the shallow user in its authorize(). Would you happen to know what’s going on? The session (/tmp/sess* files) is lacking the Role stuff.
Maybe I should be doing manual joins, as described in the docs and as is required for HABTM tables? See "Joining Tables" towards the end of http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html
AFAIK joins can only be specified in the find() calls. But how do I affect the joins if they’re only called internally from within the Auth subsystem? Should I be doing that anyway?
I’ve tried adding joins in my User model beforeFind():
Now Model::find() correctly does receives the join info:
/lib/Cake/Model/Model.php (line 2676)
But the User is still fetched with Role side by side, so
BaseAuthenticate ignores Role.
I’m expecting Role to be nested inside Milli, or something like that.
Please help, it’s a SNAFU, I’ve invested too much of my limited time
to get the job done, it’s too late to go looking for a working
framework π I admit that it’s probably a misconfiguration from my
part. But I can’t fix it.
OK, I’ve worked around this deficiency by doing 2 things:
(1) added ‘recursive’ => 1 to AuthI
(2) writing afterFind() callback in my model:
You bet this is ugly! But works.
There’s one small problem, though. When I remove currently logged in user’s role to access a specific resource, he can still do so. Looks like authorize() accesses Session data, and Session has active user roles cached.
I can’t get to work the part to allow superadmin access everything. Altough superadmin is logged in, it does not allow that role to access everything. Therefore I must setup all actions in acl.ini to make it work. Any ideas?
Did you apply the quicktip from above? Make sure that the Role array in your Session is filled. If you use a single role based approach, you would have to check User.role_id in the session instead (not User.Role)
Sent last post by mistake. What I changed was $this->Auth->allow(‘*’) to $this->Auth>allow() (I’m using 2.2.1). Now it’s working perfectly!
exactly, as noted in "NOTE 2012-02-25 ms" at the bottom π
facepalm
Anyways, you state that "You must not declare your public actions this way! All those must be declared using
."
But here I am not trying to allow access to anyone to public actions, but to allow superadmin (after he has logged in as so) access non-public actions (as showed in the
snippet, that I had to modify). What am I getting wrong? :s
The difference is HOW you do it. If you do it inside the beforeFilter for the superadmin role only it will also only affect users with this role. makes sense, doesn’t it? Just include the check from above:
That’s understood. I think I didn’t express myself well. What I modified was:
To this to make it work:
that’s true. it depends on what cake version you are using.
in earlier versions it was ‘*’ now it is just no arguments.
I updated the tutorial to make it clearer π thank you for pointing that out.
That’s great! Keep up the good work…
Thanks again
Hi. How can i use TinyAuth without user table? I created user model
andi get error:
Error: Call to a member function find() on a non-object
File: /home/chyrus/projekty/seotool/www/app/Plugin/Tools/Controller/Component/Auth/TinyAuthorize.php
Line: 162
You need to make Configure::read($this->settings[‘aclModel’]); work
So provide them in Configure as described above in your bootstrap/config etc:
I see the code of TinyAuth and I want to use the public function validate() to do individual checks for certain roles to show edit buttons etc. But I cant find a way to access validate(), is there any?
@frzfrz: See my updated note at the bottom about Auth class
Hello, thanks for great plugin, but I am stucked at the moment… When I try access for example /users/add, I am redirected to /.
My AppController:
My acl.ini:
Session array:
Any idea, please?
It tries to look for the ids in User.Group directly – as find(list) would return them.
You need to put them in as flat array of ids.
.
or enhance the component.
Wow, what an extremely quick reply! π Thanks. I was afraid of that… Hope I will find solution to convert it to flat array.
You will be happy to hear that the deeper "contained" Role array is now supported π
Mark, many thanks for your work! π I finally got it working.
But I would like to say, that maybe you should mention, that Group.alias (or Role.alias) have to be in lowercase in order to work. Took me few hours to realize, why am I not authorized to action, even if I have it set properly.
Well, now it works, that in my acl.ini I can have:
[Users]
info = admin
OR
info = Admin
and it would work because every role from .ini will be converted to lowercase (because $newRole = Configure::read($this->settings[‘aclModel’] . ‘.’ . strtolower($role));).
But if you have $this->settings[‘aclModel’][‘Admin’], it won’t work, because $newRole is looking for [‘admin’]. I don’t know if it is bug or feature, but I think it should be mentioned for other users to realize. Or maybe you can convert roles to lowercase also when you write them to config (Configure::write($this->settings[‘aclModel’], $availableRoles);).
I am wondering that I am the only one with this issue… π Keep your great work!
I was wondering if anyone would know where the best place would be to put a restriction for something like ‘RolesUser.site_id => 2’ I am working on a SaaS app so I have 1 User to many roles but I need to select only roles pertaining to the domain(site_id) they are on
$this->Auth->authenticate = array(
‘Form’ => array(
‘scope’ => array(‘RolesUser.site_id’ => $result[‘Sites’][‘site_id’]),
‘recursive’ => 1,
)
);
But it doesnt work
Ok I got it sorted and Im posting what I did to make it work.
The only issue Im still having and I dont thinkits tied to Tiny is I cant get the fields to work so I can use email instead of username
Hello Mark,
I followed all the steps in this tutorial but I don’t know why authorization always fails. I’m using version 2.4.0. Can you please help me out?
Here’s what I added to my config.php file:
and include it in my AppController.php
and here’s my acl.ini file:
When I access any actions in UsersController under /users the authorization always fails without any errors. Also I changed the aclModel and aclKey in TinyAuthorize.php to Group and group_id, respectively, but it still doesn’t work.
Thanks for your help,
Thiem
They key question is: What is in your session? If the expected and required data is not there (and in the form that it is supposed to) it will not work π
Please check your session data under the key "Auth".
Typo fix in article (extra parenthesis at end):
$this->Auth->authenticate = array(‘Form’));
Great work, keep it up!
Althought this is not strictly related to Tiny, there’s a small inconvenient when using authenticating via cookie a user.
Let’s suppose the superadmin, logs back in after some time, his session has expired but the "remember me" cookie is active. If he tries to access a non-public action, he well get a non-authorized redirect.
This happens because
Although the "allow all" code is executed in AppController:
The condition will fail because the session data for the user is empty.
After AppController::beforeFilter() the AuthComponent::startup() methods is called, the
Will return false, because no actions were allowed, and also the
Will return false because Tiny hasn’t the superadmin listed in the .ini config.
A workaround I’ve found modifying Tiny was to add this to the top of acl.ini
This way the ACL check is executed after loading the user allowing the superadmin to access all actions.
What do you think?