RBAC with roles

Hi there,

I’m using YII 2.0’s advanced template and having hard time making my RBAC to work.

I have three roles, in ascending order of powers: agent, staff, admin.

Only staff and admin can login at my backend (access gets managed through RBAC and it works fine).

Once logged in, admin can edit any user, while staff members should only be able to edit agents and their own profile. Here I’m tragically failing… (tragedy comes from how many times I’ve re-re-re-read The Guide and a ton of pages about RBAC).

I’ve built two rules, once to check if a user (staff member) is editing his/her own profile, the second to check if a staff member is trying to edit an agent. In logs, I see both rules executed, but the outcome is not as expected.

So, this is my self check rule:




namespace backend\rbac;


use Yii;

use yii\rbac\Rule;


/**

 * Checks if userID matches user passed via params

 */

class SelfcheckRule extends Rule

{

    public $name = 'selfcheck';


    /**

     * @param string|int $user the user ID.

     * @param Item $item the role or permission that this rule is associated with

     * @param array $params parameters passed to ManagerInterface::checkAccess().

     * @return bool a value indicating whether the rule permits the role or permission it is associated with.

     */

    public function execute($user, $item, $params)

    {

        /return check if current user is the same going to be edited

        return Yii::$app->user->id == $user;

    }

}



And this is the staff vs. agent editing rule:




namespace backend\rbac;


use Yii;

use yii\rbac\Rule;


/**

 * Checks if userID matches user passed via params

 */

class AgentcheckRule extends Rule

{

    public $name = 'agentcheck';


    /**

     * @param string|int $user the user ID.

     * @param Item $item the role or permission that this rule is associated with

     * @param array $params parameters passed to ManagerInterface::checkAccess().

     * @return bool a value indicating whether the rule permits the role or permission it is associated with.

     */

    public function execute($user, $item, $params)

    {

        // requestor is a Staff member

        $isRequestorStaff = Yii::$app->authManager-> getAssignment('staff', Yii::$app->user->id) != null;

        // target user is an Agent

        $isTargetAgent = Yii::$app->authManager-> getAssignment('agent', $user) != null;


        return  $isRequestorStaff && $isTargetAgent;

    }

}



In my RBAC controller I set up auth as follows:




        ...

        ...

        // add "editUser" permission

        $editUser = $auth->createPermission('editUser');

        $editUser->description = 'Edit Users';

        $auth->add($editUser);


        // add self check rule

        $selfrule = new SelfcheckRule;

        $auth->add($selfrule);


        $editSelf = $auth->createPermission('editSelf');

        $editSelf->description = 'Update own profile';

        $editSelf->ruleName = $selfrule->name;

        $auth->add($editSelf);


        // add role rule

        $agentrule = new AgentcheckRule;

        $auth->add($agentrule);


        $editAgent = $auth->createPermission('editAgent');

        $editAgent->description = 'Update agent profiles';

        $editAgent->ruleName = $selfrule->name;

        $auth->add($editAgent);


        // add "staff" role and give this role above permissions

        $staff = $auth->createRole('staff');

        $auth->add($staff);

        // allow "staff" to update their own and agent profiles

        $auth->addChild($staff, $editSelf);

        // "editSelf" will be used when "editAgent" fails

        $auth->addChild($editSelf, $editAgent);

        // "editAgent" will be used when "editUser" fails

        $auth->addChild($editAgent, $editUser);


        // add "admin" role and give this role all remaining permissions

        $admin = $auth->createRole('admin');


        $auth->addChild($admin, $editUser);

        $auth->addChild($admin, $staff);



Finally, in my UserController, my understanding is that I just need to set up behaviors like this to make things work, without need to check again permissions in actions’ code:




    public function behaviors()

    {

        return [

            'verbs' => [

                'class' => VerbFilter::className(),

                'actions' => [

                    'delete' => ['post'],

                ],

            ],

            'access' => [

                'class' => \yii\filters\AccessControl::className(),

                'rules' => [

                    [

                        'allow' => true,

                        'actions' => ['index', 'view'],

                        // loginBackend permission only allows staff and admin to log into from backend

                        'roles' => ['loginBackend']

                    ],

                    [

                        'allow' => true,

                        'actions' => ['update'],

                        // admin only has this permission, staff members should go through selfcheck/editAgent rules

                        'roles' => ['editUser']

                    ],

                    [

                        'allow' => true,

                        'actions' => ['create', 'delete'],

                        // admin only has this permission

                        'roles' => ['createUser']

                    ],

                    [

                        'allow' => false

                    ]

                ]

            ]

        ];

    }



With this setup, admin gets the right powers (can edit every user) but staff members get those powers too! As I said, in logs I see both rules checked, so I suspect something is wrong in there, like both rules always return true.

Does anybody have an idea about what am I doing wrong?

Edit::

Ok, my rules are definitely wrong: my understandings were wrong (I was considering rules’ $user argument wrong). I’ve realized I miss the id of the user model I’m going to edit, when I’m in rules. But how do I pass it to my rule?

Well, I could check for permissions in my UserController’s Update action, like the example in The Guide shows:


if (\Yii::$app->user->can('updateUser', ['user' => $user])) {

    // update user

}

But then The Guide says:

So now my question is: how do I pass arguments to my rules, using AccessControl?

TIA,

rash*




/**

 * Checks if userID passed via params matches the current user

 */

class SelfcheckRule extends Rule

{

    public $name = 'selfcheck';


    /**

     * @param string|int $user the user ID ... the current user ID.

     * @param Item $item the role or permission that this rule is associated with

     * @params array $params parameters passed to ManagerInterface::checkAccess().

     * @return bool a value indicating whether the rule permits the role or permission it is associated with.

     */

    public function execute($user, $item, $params)

    {

        // return check if current user is the same going to be edited

        // return Yii::$app->user->id == $user;

        return $user == $params['user_id'];

    }

}



Note that you have to use $params parameter to pass the user id that you want to compare with the current user id.

In the controller, you can check the access right like the following:




$model = UserProfile::find()->...->one();

if (\Yii::$app->user->can('editProfile', ['user_id' => $model->id])) {

    ...

}



The code above assumes that ‘editProfile’ permission has a parent permission ‘editOwnProfile’ which has the ‘selfcheck’ rule.

You may assign ‘editOwnProfile’ to ‘staff’ role, and ‘editProfile’ to ‘admin’.

And, yes, you have to write some code in the controller (or somewhere else) in order to check these kinds of access rights. Access Control Filter is not enough in this case.

@softark, thanks for replying, I’ll go with your suggestions, 'cause it seems the wisest way to go.

Btw, I assume you have seen my edit, just a minute before your reply; I understand then that The Guide is (say) misleading a bit by saying "you can check in controller… OR use yii/filters/AccessControl" and showing a very simple example: trying to get things to work with a similar barebone config has driven me crazy last few days.

Meanwhile I’ve also tried to encapsulate my filtering logic in an AccessRule but (apart from an initialization issue with it) I’m afraid I would face the same question at some point, i.e. how to pass arguments to the AccessRule: the easiest way would be through ACF but it would require writing a custom one, right?).

Thanks again for your help!

rash*

No, it was just a moment after I had posted my reply. :lol:

I realized that I had written too much, because you had already taken the right path. But I decided to leave my reply as it was, mostly because I didn’t have much time.

IMO, collecting a complicated and/or heavy object in the ACF might not be a good idea. I’d rather want to keep the filter light and thin.

I see… you’re one of those thin-controller-fat-model fanatics, uh? :D :D :D I do share the same view though and it proved a wise choice so far.

This is what makes your help unvaluable.

Cheers!

rash*

Umm, not really, I guess.

I’m just a second-rated programmer who wants to avoid DRY approach when it is going to make things too difficult for me to understand. I’d rather want to repeat plain and boring code in order to keep things simple and easy.

In fact I have many lines of code that handles the access controlling in controller methods and in the view scripts. Something like:




// in a controller

if (!\Yii::$app->user->can('edit', ['owner' => $model->created_by])) {

    throw new ForbiddenHttpException('You are not allowed to edit this data.');

}


// in a view script

<?= Html::a('View', ['view', 'id' => $model->id]) ?>

<?php if (\Yii::$app->user->can('edit', ['owner' => $model->created_by])) : ?>

    <?= Html::a('Edit', ['update', 'id' => $model->id]) ?>

<?php endif; ?>



There might have been a smarter way to accomplish these kinds of things, but I couldn’t come up with a good solution.

Well, I’ve restored the initial code, written following The Guide.

Now the problem is: in my rule I don’t get any parameter, not even passing it from the controller when calling can() method.

In my controller’s Update action I have now (and it wasn’t too much to write, @softark :)):




        $model = $this->findModel($id);


        if (Yii::$app->user->can('editUser', ['user'=>$model])) {

             

            ...update user here...

 

        } else {

            throw new ForbiddenHttpException(Yii::t('app', 'You are not allowed to edit this user.'));

        }



This should in turn check EditUser (permission), then checkAgent (Rule) and eventually selfCheck (rule).

Dunno why when it gets in first rule, I have no ‘user’ parameter, to be precise I have no parameters at all, i.e.:




    public function execute($user, $item, $params)

    {

        // Yii::trace("User " . var_dump($user), 'trace');

        // Yii::trace("Item " . var_dump($item), 'trace');

        // Yii::trace("Params " . var_dump($params), 'trace');

        // die();


        // target user

        $target_id = $params['user']->id;


        // is requestor a Staff member?

        $isRequestorStaff = Yii::$app->authManager->getAssignment('staff', $user) != null;

        // is target user an Agent?

        $isTargetAgent = Yii::$app->authManager->getAssignment('agent', $target_id) != null;


        return  $isRequestorStaff && $isTargetAgent;

    }



The rule throws an exception, telling me ‘$params[‘user’]’ does not exist; if I uncomment trace lines, I get an empty array for ‘$params’, while ‘$user’ and ‘$item’ arguments are fine.

WTH am I doing wrong this time?

TIA,

rash*

Is $model populated?

Yes. When I log in as admin, I can edit my model no problem. When I log as non-admin, I don’t even get to the edit form, the exception get thrown while processing my first rule.

If I trace and kill the app while in the rule (or cheat my rules by returning true no matter what), I get correct values for $user and $item arguments, while $params is traced as an emtpy array.

rash*

Probably you have to review the ACF settings. I guess all the users without “editUser” permission are not allowed to access “update” action, if you haven’t changed what you wrote in the first post.




            'access' => [

                'class' => \yii\filters\AccessControl::className(),

                'rules' => [

...

                    [

                        'allow' => true,

                        'actions' => ['update'],

                        // 'roles' => ['editUser']

                        'roles' => ['editUser', 'editSelf', 'editAgent']

                    ],

...

                ]

            ]



Uhm, ok, I’ll try that and let you know.

Thanks a ton,

rash*

Tsk, nope, I receive the same error: when I get into my rule code, passed argument has disappeared.

If I look at error log, I see yii\web\User::can(‘editUser’) being called without arguments from vendor/yiisoft/yii2/filters/AccessRule.php while code flow goes correctly through permission tree: has user editUser permission assigned? No, then check if respects AgentCheck rule and… at this point throws the error for the argument missing…

So it looks like the argument passed to the AuthManager from my controller, like this,




        if (Yii::$app->user->can('editUser', ['target'=>$model])) {

            ... edit user here ...

        } else {

            throw new ForbiddenHttpException(Yii::t('yiinet', 'You are not allowed to edit this user.'));

        }



it never gets passed through.

How can this happen?

TIA

rash*

I’m trying to accomplish almost the same:

I have an admin account, multiple company accounts and multiple employee accounts which can have multiple posts.

My question is how I can accomplish the following:

  1. Admin can add/edit/delete all the accounts and posts

  2. Company can add/edit/delete self account and posts and employee accounts and posts

  3. Employee can only edit account info and add/edit/delete posts

Ah, OK, I got it. I was wrong.

Your rule must handle the situation where ‘params’ could be empty.

‘editSelf’ and ‘editAgent’ can check the rules without providing params when they are called from ACF.




class SelfcheckRule extends Rule

{

    public $name = 'selfcheck';


    public function execute($user, $item, $params)

    {

        if (empty($params)) {

            return true;

        } else {

            return $user->id == $params['user']->id;

        }

    }

}

Hi scieri,

I’m sorry but I can’t provide you with a quick solution, because your question is not specific enough. So my answer has to be a very rough sketch …

  1. Create 3 roles … admin, company, employee.

  2. Create necessary permisons … they can be very many

    add_accounts, edit_accounts, delete_accounts, add_posts, edit_posts, delete_posts,

    edit_own_account, delete_own_account, edit_own_posts, delete_own_posts, …

  3. Create necessary rules … isOwnAccount, isOwnPost

  4. Connect the rules and the permissions … edit_own_account - isOwnAccount, delete_own_account - isOwnAccount,

    edit_own_posts - isOwnPost, delete_own_post - isOwnPost

  5. Establish an RBAC hierarchy … edit_own_account is a parent of edit_accounts, admin has edit_accounts permission, company has edit_own_account, … etc.

  6. Assign an appropriate role to each and every user.

And, you have to read the following section of the guide before you proceed.

http://www.yiiframework.com/doc-2.0/guide-security-authorization.html

The fact is that I have a single call to Yii::$app->user->can() and I do pass an argument. More, I’m sure the parameter is populated, because if I take away code related to RBAC, that argument (the model) is updated regularly. Beside that call, I do not use the two rules directly. As specified in The Guide, I do set the rules as children of updateUser permission: it should be AuthManager to recursively access rules along the appropriate tree branch of permissions, up to either a matching rule or a final verdict of no-permission.

As stated in The Guide, in my controller I should start checking permissions from the bottom (editUser), passing a possibly needed additional argument to can(), up to the role or the rule allowing or denying the action.

This is the sense of these settings in my RBAC controller:




        // staff permissions

        // add "editUser" permission

        $editUser = $auth->createPermission('editUser');

        $editUser->description = 'Edit Users';

        $auth->add($editUser);


        // add self check rule

        $selfrule = new SelfcheckRule;

        $auth->add($selfrule);


        $editSelf = $auth->createPermission('editSelf');

        $editSelf->description = 'Update own profile';

        $editSelf->ruleName = $selfrule->name;

        $auth->add($editSelf);


        // add role rule

        $agentrule = new AgentcheckRule;

        $auth->add($agentrule);


        $editAgent = $auth->createPermission('editAgent');

        $editAgent->description = 'Update agent profiles';

        $editAgent->ruleName = $agentrule->name;

        $auth->add($editAgent);


        // add "staff" role and give this role above permissions

        $staff = $auth->createRole('staff');

        $auth->add($staff);


        // allow "staff" to update their own and agent profiles

        $auth->addChild($staff, $editSelf);

        // "editSelf" will be used when "editAgent" fails

        $auth->addChild($editSelf, $editAgent);

        // "editAgent" will be used when "editUser" fails

        $auth->addChild($editAgent, $editUser);


        // admin permissions

        $admin = $auth->createRole('admin');

        $auth->add($admin);


        $auth->addChild($admin, $editUser);



Now, I’m able to treat cases when my passed argument is empty (following this: http://www.yiiframework.com/forum/index.php/topic/60439-yii2-rbac-permissions-in-controller-behaviors/#entry269913) even if I don’t see it as a robust solution. It does work, I don’t like it but it works. It does not explain why my argument doesn’t get passed along but it works.

Now, while implementing the latter link, I’ve realized why my RBAC settings are wrong. I’ve set rules as children of staff, along a tree branch: as soon as the first rule fails, instead of processing upper rule/role, authmanager stops and throws a no-permission.

I had to reassign each rule as child of staff role indipendently to make things work.

In the image below you can find my wrong and then right permission structure.

So, the right way to setup rbac is the following (@scieri, you could mimic this):




        // add "editUser" permission

        $editUser = $auth->createPermission('editUser');

        $editUser->description = 'Edit Users';

        $auth->add($editUser);


        // add self check rule

        $selfrule = new SelfcheckRule;

        $auth->add($selfrule);


        $editSelf = $auth->createPermission('editSelf');

        $editSelf->description = 'Update own profile';

        $editSelf->ruleName = $selfrule->name;

        $auth->add($editSelf);


        // add role rule

        $agentrule = new AgentcheckRule;

        $auth->add($agentrule);


        $editAgent = $auth->createPermission('editAgent');

        $editAgent->description = 'Update agent profiles';

        $editAgent->ruleName = $agentrule->name;

        $auth->add($editAgent);


        // add "staff" role and give this role above permissions

        $staff = $auth->createRole('staff');

        $auth->add($staff);

        // allow "staff" to update their own and agent profiles

        $auth->addChild($staff, $editSelf);

        $auth->addChild($staff, $editAgent);

        // "editAgent" and "editSelf" will be used when "editUser" fails

        $auth->addChild($editSelf, $editUser);

        $auth->addChild($editAgent, $editUser);


        $admin = $auth->createRole('admin');

        $auth->add($admin);

        $auth->addChild($admin, $editUser);




We can close this issue, 'cause things now work. I’d like an explanation for the argument though.

I’m also thinking about creating a wiki from this, 'cause I believe this issue could affect various people.

Softark, thanks for your time and help!

Cheers,

rash*

Hi rash*,

Thanks for your sharing.

I’d like to add a few more things about the subject.

  1. ACF and RBAC should not be considered as alternative options. They are in two different layers.

ACF can use RBAC items as its filters.




            'access' => [

                'class' => \yii\filters\AccessControl::className(),

                'rules' => [

...

                    [

                        'allow' => true,

                        'actions' => ['update'],

                        'roles' => ['editUser']

                    ],

...

                ]

            ]



In the above, ACF will use ‘editUser’ RBAC item (role) to check access rights, which will call “checkAccess()” without parameters. That’s why you’ll sometimes find ‘params’ empty.

  1. About nesting parent-child relations of RBAC items with rules.

I didn’t think that you were wrong when you made “editSelf” a parent of “editAgent”. But I was wrong.

The following is from the source code of yii\rbac\DbManager




    /**

     * Performs access check for the specified user.

     * This method is internally called by [[checkAccess()]].

     * @param string|integer $user the user ID. This should can be either an integer or a string representing

     * the unique identifier of a user. See [[\yii\web\User::id]].

     * @param string $itemName the name of the operation that need access check

     * @param array $params name-value pairs that would be passed to rules associated

     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,

     * which holds the value of `$userId`.

     * @param Assignment[] $assignments the assignments to the specified user

     * @return boolean whether the operations can be performed by the user.

     */

    protected function checkAccessRecursive($user, $itemName, $params, $assignments)

    {

        if (($item = $this->getItem($itemName)) === null) {

            return false;

        }


        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);


        if (!$this->executeRule($user, $item, $params)) {

            return false;

        }


        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {

            return true;

        }


        $query = new Query;

        $parents = $query->select(['parent'])

            ->from($this->itemChildTable)

            ->where(['child' => $itemName])

            ->column($this->db);

        foreach ($parents as $parent) {

            if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {

                return true;

            }

        }


        return false;

    }



As you may notice, recursive checking of the parents will be stopped when the execution of the rule fails.

An item without a rule won’t be affected, since “executeRule” returns “true” when there’s no rule.

That’s why “editSelf” and “editAgent” could not make a valid parent-child relation, while “editUser” could have them as parents.

I’m looking forward to reading your wiki on the subject. :)

@softark: thanks to you! I hope to be at a good-enough level for the wiki, I’ve moved to Yii 2.0 recently and still grasping to better understand things.

Cheers,

rash*

PS: thanks to the solution you helped finding, I’ve slept like a kid tonight, after a week or so…