Powered by Invision Power Board



Pages: (3) [1] 2 3  ( Go to first unread post ) Reply to this topicStart new topicStart Poll

> [HOWTO] Acl/Auth user having multiple roles/groups, Extended Auth, simple solution
francky06l
Posted: Jul 19 2008, 03:13 PM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



Hi All,

I am back on Auth/ACL after a while. I have seen many posts on google groups and many questions about how to manage permissions for user belonging to multiple roles/groups.
Actually it's quite simple using ACL and Auth. There is just a small extension to do on Auth and understand how ACL works.

Goal
  • A user has a primary role and can have other roles
  • Permissions are given at role level
  • Roles are not inherited (they could be also), but the goal was to show the implementation for distinct roles
  • Use ACL and Auth with the type "actions" to control the access of controller/actions
The main difference with the first tutorial (see in this section), is that the roles are not inherited. However I could have made the sample to work in both mode, but I did want to complexify the tutorial. For people familiar with ACL and the first tutorial, it should be straight forward to combine both "mode".
I will not be as "slow" (I have seen comment on this) as in the previous tutorial, so I suggest, for the beginners to start with tutorial one.
Note for people wanted users belonging to multiple groups, they should just replace the word "Role" by "Group" anywhere in the project.
This sample, takes most of the previous code as starting point.

We start with the models, we will get 3
  • Role, very simple but uses the ACL behavior, we define a parentNode method that will always return null, since we do use Role inheritance.
  • User, there is a special association that we create for the FirstRole using the role_id. Since user can have multiple roles, this will have HABTM to role
  • RoleUser, used as the "with" model in the User HABTM Role. Actually we could avoid it, but there is a good reason for it (we will see later).
Role model:
PHP Code
<?php

class Role extends AppModel {

    var 
$name   'Role';    
    var 
$hasMany = array('User' => array('className' => 'User',
                                
'foreignKey' => 'role_id',
                                
'conditions' => '',
                                
'fields' => '',
                                
'order' => '',
                                
'counterCache' => '')
                        );
                        
    var 
$validate = array('name' => array('rule' => 'isUnique''message' => 'already exist'));
    
    var 
$actsAs = array('Acl' => 'requester');
    
    function 
parentNode()
    {
        return 
null;
    }
}

?>


User model
PHP Code
<?php

class User extends AppModel {

    var 
$name 'User';

    
//The Associations below have been created with all possible keys, those that are not needed can be removed
    
var $hasAndBelongsToMany = array(
            
'Role' => array('className' => 'Role',
                                
'foreignKey'            => 'user_id',
                                
'associationForeignKey' => 'role_id',
                                
'joinTable'             => 'user_roles',
                                
'with'                  => 'UserRole',
                                
'conditions' => '',
                                
'fields' => '',
                                
'order' => '',
                                
'counterCache' => ''),
    );

    var 
$belongsTo = array(
            
'FirstRole' => array('className' => 'Role',
                                
'foreignKey'            => 'role_id',
                                
'conditions' => '',
                                
'fields' => '',
                                
'order' => '',
                                
'counterCache' => ''),
    );
    
    var 
$validate  = array('role_id' => array('rule' => VALID_NOT_EMPTY'message' => 'Mandatory'),
                           
'username' => array(array('rule' => VALID_NOT_EMPTY'message' => 'Mandatory''last' => true),
                                               array(
'rule' => 'isUnique''message' => 'already exists'))
                          );
                          
    var 
$actsAs = array('Acl' => 'requester');
    
    function 
parentNode()
    {    
        if(
$this->id)
        {
            
$data $this->read();

            if(
$data['User']['role_id'])
                return array(
'model' => 'Role''foreign_key' => $data['User']['role_id']);
        }
        return 
null;        
    }
}

?>

Notes
  • Note the declaration of FirstRole, indeed the User must a role, the other roles are "optional"
  • We use the model 'UserRole' as associated model for the HABTM declaration. This is easier when we delete a role to remove the eventual associated users.
Model UserRole
PHP Code
<?php

class UserRole extends AppModel {

    var 
$name 'UserRole';
}
?>


Nothing else to add on models, for people preferring "Group" the can adjust the model names and fields accordingly.

For the controllers, the special methods to mention are :

UsersController

PHP Code
<?php

    
function edit($id null) {
        if (!
$id && empty($this->data)) {
            
$this->Session->setFlash(__('Invalid User'true));
            
$this->redirect(array('action'=>'index'));
        }
        if (!empty(
$this->data)) {
            if (
$this->User->save($this->data)) {
                
$this->_setParentAro($this->User->id$this->data['User']['role_id']);
                
$this->Session->setFlash(__('The User has been saved'true));
                
$this->redirect(array('action'=>'index'));
            } else {
                
$this->Session->setFlash(__('The User could not be saved. Please, try again.'true));
            }
        }
        if (empty(
$this->data)) {
            
$this->data $this->User->read(null$id);
        }
        
$roles $this->User->Role->find('list');
        
$this->set(compact('roles'));
    }

    function 
_setParentAro($userid$parent_id)
    {
        
// we first retrieve the Aro of this use
        
        
$aro $this->Acl->Aro->node(array('model' => 'User''foreign_key' => $userid));
        
        if(empty(
$aro))
            return;
            
        
$aronode $aro[0]['Aro'];
        
// parent did not change

        
if($aronode['parent_id'] == $userid)
            return;
        
// we first need to find the aro id of the new parent
        
        
$arop $this->Acl->Aro->node(array('model' => 'Role''foreign_key' => $parent_id));
        
        if(!empty(
$arop))
        {
            
$newid $arop[0]['Aro']['id'];
            
$aronode['parent_id'] = $newid;
            
$this->Acl->Aro->save($aronode);
        }
        else
            echo 
"Error resetting the parent ARO";
    }

?>
  • The edit method calls the _setParentAro method in order to set the proper Role Aro parent if ever it did change.
  • setParentAro, check actual aro and replace the parent_id field if needed
RolesController
PHP Code
<?php

    
function delete($id null) {
        if (!
$id) {
            
$this->Session->setFlash('Invalid id for Role');
            
$this->redirect(array('action'=>'index'), nulltrue);
        }
        
// we bind  an hasmany relation to delete all the user/role associations
        
$this->Role->bindModel(array('hasMany' => array('UserRole' => array('className' => 'UserRole''foreignKey' => 'role_id'))));

        if (
$this->Role->del($id)) {
            
$this->Session->setFlash('Role #'.$id.' and UserRole deleted');
            
$this->redirect(array('action'=>'index'), nulltrue);
        }
    

?>
  • Note the delete method, we bind the UserRole in hasMany, this will ensure that deleting a Role will delete all record of UserRole that are linked to this role_id
  • THE WAY THE PERMISSION is set for an action, use Acl->inherit insted of deny. This is the crucial point, if ever a permission is DENIED or DOES NOT EXIST (means not being set properly) the complete process will not work, because the check will stop at the first Denied..
  • I have done a cleanupAcl method to clean/set the controller/actions. Check code for more details
NOW THE MOST IMPORTANT PART
The original Auth can check our FirstRole because it's the parent Aro of our User Aro. Now since an Aro can have only one parent, we need to find a solution for checking the others. I has the idea of all this when looking at the node function of the db_acl, there is a way to retrieve node(s), by using this syntax :
PHP Code
<?php

array('ModelName' => array('fields' => (values)));

?>

Auth is doing this check for the User with it's FirstRole
PHP Code
<?php

$this
->Acl->check($user$this->action);

?>

This will check that the FirstRole of the user have the right to access the controller/action. Now for the others role, we just need to check :
PHP Code
<?php

$valid 
$this->Acl->check($user$this->action);
if(!
$valid)
   
$valid $this->Acl->check('Role' => array('id', array(other role_id)));

?>


Very simple isn't ? However Auth does not allow to make this so easily, no problem I created a component derive from Auth that I call AuthExt (Auth extended). Here is the code :

PHP Code
<?php

/*
* Extend the Auth component
*
*/

App::import('component''Auth');

class 
AuthExtComponent extends AuthComponent
{
    var 
$parentModel 'Role';
    var 
$fieldKey    'role_id';
    
    
// override, to store the associated role
    
    
function login($data null)
    {
        if(!
parent::login($data))
            return 
$this->_loggedIn;

        
// fetch the assciated role
        
        
$model $this->getModel();
        
        if(isset(
$model->hasAndBelongsToMany[$this->parentModel]['with']))
        {   
            
$with $model->hasAndBelongsToMany[$this->parentModel]['with'];
            if(!isset(
$this->{$with}))
                
$this->{$with} =& ClassRegistry::init($with);                

            
// fetch the associated model
            
$roles $this->{$with}->find('all', array('conditions' => 'user_id = '.$this->user('id')));
            if(!empty(
$roles))
            {
                
$primaryRole $this->user($this->fieldKey);            
                
// retrieve associated role that are not the primary one
                
$roles set::extract('/'.$with.'['.$this->fieldKey.'!='.$primaryRole.']/'.$this->fieldKey$roles);

                
// add the suplemental roles id under the Auth session key
                
if(!empty($roles))
                {
                    
$completeAuth $this->user();
                    
$completeAuth[$this->userModel][$this->parentModel] = $roles;
                    
$this->Session->write($this->sessionKey$completeAuth[$this->userModel]);
                }
            }
        }
        
        return 
$this->_loggedIn;        
    }
    
// override this to find the right aro/aco
    
    
function isAuthorized($type null$object null$user null)
    {
        
$valid parent::isAuthorized($type$object$user);
        
        if(!
$valid && $type == 'actions' && $this->user($this->parentModel))
        {
            
// get the groups from the Session, and set the proper Aro path
            
$otherRoles $this->user($this->parentModel);
            
$valid $this->Acl->check(array($this->parentModel => array('id' => $otherRoles)), $this->action());            
        } 
        return 
$valid;
    }       
}

?>


Notes
The AuhtExt uses 2 member variables call parentModel and fieldKey. In our case, for this sample, this is "Role" and "role_id". This can be turned in anything you fancy.
The login method is overriden, and does the following:
  • call the parent login method, and id the Auth login method fails, we just return false at this point.
  • if we sucessfully login, we check if the Auth "userModel" (ie: default is User), has an HABTM relation with our parent model (ROLE).
    If so, we instanciate the "with" model, UserRole and we retrieve the other Role we have for this user.
    If we have other roles, we store under the Auth sessionKey an array of id's. Note that we remove the FirstRole id, if it was set with the other roles (look at the set::extract syntax used for this).
  • the Auth key will now contain our parentModel as key under in the User array.
The isAuthorized method is called by the Auth component at startup, this check the current action for authorization. We overrite this method, doing the dollowing:
  • call the orginal isAuthorized, indeed Auth will check if the User using it's first role has the right for the action
  • in case the of failure, and if we are in "actions" mode (we can keep Auth for other type of "mode"), and of course if we have other roles, we check the action against the other roles. This is main point of having this working, and be aware not to deny permission but inherit them.
To implement the AuthExt in our application, it really does not require anything special, just replacing $this->Auth by $this->AuthExt.
This is the sample appController
PHP Code
<?php

class AppController extends Controller {
    var 
$components      = array('Acl''AuthExt''RequestHandler');
    var 
$helpers         = array('Javascript''Html''Form');
    
    function 
beforeFilter()
    {
        if(isset(
$this->AuthExt))
        {
            if(
$this->name == 'Pages')
                
$this->AuthExt->allow('*');
            else
            {   
                
$this->AuthExt->loginAction   '/users/login';
                
$this->AuthExt->autoRedirect  false;
                
$this->AuthExt->authorize     'actions';
            }
        }    
    }
}

?>


I have attached the complete project. To run it :
- create your database using the aclrole.sql. Adjust you database name, user, password in /config/database.php accordingly.
- the default database comes with only one user "admin" (password: admin).
- the admin user, has also the role "ItManager". The ItManager role has only the right to "view users", and the admin does not.
- The first test would be to log as an admin, and try to view a user (that should succeed). Then go to ACL menu and "deny" the user view on the ItManager role. Try to view a user, you should be redirected.


Well that's about it, not so complex in fact.
Comments and remarks are welcome.
Franck

Attached File ( Number of downloads: 1315 )
Attached File  aclrole.zip
PMEmail PosterICQ
Top
polakiran
Posted: Jul 20 2008, 05:46 PM
Quote Post


Newbie
*

Group: Members
Posts: 7
Member No.: 554
Joined: 18-July 08



Thank you for your support.... smile.gif


Actually my proj requirement is
Users will be assigned to roles
roles will have set of perms

Users will be added to groups. ex: kiran added to sales group or finance group. But groups will not have permissions. It is just to group logically.

It is possible. If possible can u pls extend your support.

Thank you in advance
PMEmail Poster
Top
francky06l
Posted: Jul 20 2008, 05:53 PM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



QUOTE

Actually my proj requirement is
Users will be assigned to roles
roles will have set of perms

This is exactly what the tutorial does. Now if you user belongs to a group, logical group, it's just a User belongsTo Group property, nothing to do with permissions...
You would have a group_id in the User model (unless user can belongs to many groups), but this a nothing to do with permission, it's more an organization/display views story..
PMEmail PosterICQ
Top
penfold_99
Posted: Aug 11 2008, 09:08 PM
Quote Post


Newbie
*

Group: Members
Posts: 3
Member No.: 531
Joined: 9-July 08



Hi Franck

Great tutorial and I have been able to get up and running straight away.
I have a couple of questions.

I have Users, Editors, Admins

In this scenario users can view and create posts.
Editors have view, create and edit.
Admins have view, create, edit, delete.

I would like to be able to give users edit rights over their own records only not all records. Does your demo support this already?

Would it be possible to integrate groups of users, so an editor role can only edit records belonging to users within the group?

Thanks





PMEmail Poster
Top
francky06l
Posted: Aug 14 2008, 04:52 PM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



The demo does not support this, however it's easy to implement. I suppose you already have the user_id in the post (the creator), so you could check in case of user if the user_id is matching the current user.
For group of users, if you mean having "groups" it's a bit has "roles". But in such case you have to manage the groups also (no action permission at this level).
I suppose then the easiest would to check the current group of the user against the group of the user that did create the post.
There is many way to implement this, some easier and more open as other one.

Of course you could also use the ACL in "model" / "crud" mode to manage all the rights on records (create an ACO for each record) and make a call to ACL->check in the actions.
PMEmail PosterICQ
Top
penfold_99
Posted: Sep 19 2008, 03:42 PM
Quote Post


Newbie
*

Group: Members
Posts: 3
Member No.: 531
Joined: 9-July 08



Hi Francky06l

Is there any real need to have first role and additional roles?
couldn't first role and additional roles be group together as roles?



PMEmail Poster
Top
francky06l
Posted: Sep 23 2008, 09:28 AM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



Hi penfold,

No actually you could avoid the primary role. I did in fact for several reasons:

- you would set primary role as the most used
- that would let Auth doing most of the work
- the search for the ACL is more costive when having multiple roles

But again, it's easy to implement. Feel free to post your solution ..

cheers
PMEmail PosterICQ
Top
fly2279
Posted: Mar 2 2009, 01:18 PM
Quote Post


Newbie
*

Group: Members
Posts: 1
Member No.: 1092
Joined: 2-March 09



Great tutorial. ACL is hard for me to wrap my head around and it's taking a few days to get it. I downloaded the code and ran with a test database to try it out. It all works great except when I try to add a new role I get a few errors. The role is getting added to the aros table and I can assign the new role to acos using adjustperms. Why is there an error here?

CODE

Notice (8): Undefined index:  Aro [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): array_key_exists() [function.array-key-exists]: The second argument should be either an array or an object [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): Cannot modify header information - headers already sent by (output started at /mydir/cake/basics.php:111) [CORE/cake/libs/controller/controller.php, line 615]
PMEmail Poster
Top
karsten_l
Posted: Mar 18 2009, 02:29 PM
Quote Post


Newbie
*

Group: Members
Posts: 1
Member No.: 1134
Joined: 18-March 09



Hi francky06l,

I like this solution, but I had a little problem...

There are some functions in my app_controller, for example some filter-functions. I use them from all my controllers. This works fine without your roles-handling.
If I use your solution, I can "allow" the controller-funtions, but not the functions in the app_controller. I think your component should look (when valid==false) if the function in the app_controller is "allowed".

Thanks for any tips to solve this.

best regards
Karsten
PMEmail Poster
Top
francky06l
Posted: Mar 24 2009, 07:34 AM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



I did something to fix this problem, I really have to update the code (but really busy).
Another easy solution would be to set a function in controller and call the parent function. Thus you can "allow/deny" the controller function and authorize the app_controller one.
cheers
PMEmail PosterICQ
Top
francky06l
Posted: Mar 24 2009, 05:02 PM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



Sorry for the late answer on this:

Notice (8): Undefined index: Aro [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): array_key_exists() [function.array-key-exists]: The second argument should be either an array or an object [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): Cannot modify header information - headers already sent by (output started at /mydir/cake/basics.php:111) [CORE/cake/libs/controller/controller.php, line 615]


You have to run the cleanAcl method, I think that should fix. I can add roles without problems
PMEmail PosterICQ
Top
double07
Posted: Apr 15 2009, 03:24 AM
Quote Post


Member
**

Group: Members
Posts: 15
Member No.: 228
Joined: 19-February 08



[QUOTE=francky06l,Mar 24 2009, 05:02 PM] Sorry for the late answer on this:

Notice (8): Undefined index: Aro [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): array_key_exists() [function.array-key-exists]: The second argument should be either an array or an object [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): Cannot modify header information - headers already sent by (output started at /mydir/cake/basics.php:111) [CORE/cake/libs/controller/controller.php, line 615]


I get this error too but only in debug mode and it doesn't seem to affect anything. When debug mode is set to zero, there's no problem.
PMEmail Poster
Top
francky06l
Posted: Apr 17 2009, 09:20 AM
Quote Post


Advanced Member
***

Group: Super Moderator
Posts: 457
Member No.: 29
Joined: 4-November 07



Having debug to 0 suppresses all the output messages, but the problem is there. It seems to be in the core tree behavior.
What version are you running ? Maybe give a try with the nightly build ..

Cheers
PMEmail PosterICQ
Top
3pling
Posted: Apr 23 2009, 02:40 PM
Quote Post


Newbie
*

Group: Members
Posts: 4
Member No.: 1208
Joined: 17-April 09



QUOTE (fly2279 @ Mar 2 2009, 01:18 PM)
Why is there an error here?

CODE

Notice (8): Undefined index:  Aro [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): array_key_exists() [function.array-key-exists]: The second argument should be either an array or an object [CORE/cake/libs/model/behaviors/tree.php, line 167]
Warning (2): Cannot modify header information - headers already sent by (output started at /mydir/cake/basics.php:111) [CORE/cake/libs/controller/controller.php, line 615]

Change in the add function of the roles_controller.php this rule:
CODE
$arop['alias'] = $this->data['Role']['name'];

to this:
CODE
$arop['Aro']['alias'] = $this->data['Role']['name'];


And the error is gone.. smile.gif
PMEmail Poster
Top
senseBOP
  Posted: May 17 2009, 01:59 PM
Quote Post


Newbie
*

Group: Members
Posts: 5
Member No.: 1267
Joined: 11-May 09



Hey everyone,

I'm trying to customize this code to work with UUID's, as my app is already set up with UUID's instead of auto-incremented ID's, but am having a hard time. The user gets authenticated just fine (because the AuthExt doesn't care what data you give it about the user), but then, the authorization step fails as it tries to compare user_id to a column name instead of a string.

I.e. Say your UUID is X1X2X3X4X5X... You'd think that Cake will do this:
CODE
WHERE user_id = 'X1X2X3X4X5X'

But instead it does this:
CODE
WHERE user_id = X1X2X3X4X5X

Which of course fails with this error:
CODE
Unknown column 'X1X2X3X4X5X' in 'where clause'


I tried setting up the associations for the User and Role models myself (instead of just counting on Cake to do everything) but the results remain the same.

Any ideas anyone? Thanks!
PMEmail Poster
Top
0 User(s) are reading this topic (0 Guests and 0 Anonymous Users)
0 Members:

Topic Options Pages: (3) [1] 2 3  Reply to this topicStart new topicStart Poll

 

Disclaimer:
This forum is in no way affiliated with the Cake Software Foundation
The CakePHP name and icon is a trademark of the Cake Software Foundation


Lo-Fi Version