Does Anyone Have A Working Example Of Rbac?

Hi all!

My site has three different levels of authenticated users. I want to allow/deny controllers and actions based on the user role level.

It’s a fairly simple, linear scheme: each user level had more rights than the one below it. I guess I need to use RBAC to do this, but I can’t find any examples or documentation. I’ve gone through the source code, but I find it hard to understand how I should set it up.

Does anyone have an example of:

[list=1]

[*]Configuring RBAC

[*]Using RBAC in a controller

[*]The way roles are defined and users are assigned

[/list]

Thanks!

Ok. I hope someone will correct me if I’m wrong.

First af all, you modify your config (web.php),


'authManager' => [

    'class' => 'app\components\PhpManager', // THIS IS YOUR AUTH MANAGER

    'defaultRoles' => ['guest'],

],

Next, create the manager itself (app/components/PhpManager.php)


<?php

namespace app\components;


use Yii;


class PhpManager extends \yii\rbac\PhpManager

{

    public function init()

    {

        if ($this->authFile === NULL)

            $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; // HERE GOES YOUR RBAC TREE FILE


        parent::init();


        if (!Yii::$app->user->isGuest) {

            $this->assign(Yii::$app->user->identity->id, Yii::$app->user->identity->role); // we suppose that user's role is stored in identity

        }

    }

}

Now, the rules tree (@app/data/rbac.php):


<?php

use yii\rbac\Item;


return [

    // HERE ARE YOUR MANAGEMENT TASKS

    'manageThing0' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],

    'manageThing1' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],

    'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],

    'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],


    // AND THE ROLES

    'guest' => [

        'type' => Item::TYPE_ROLE,

        'description' => 'Guest',

        'bizRule' => NULL,

        'data' => NULL

    ],


    'user' => [

        'type' => Item::TYPE_ROLE,

        'description' => 'User',

        'children' => [

            'guest',

            'manageThing0', // User can edit thing0

        ],

        'bizRule' => 'return !Yii::$app->user->isGuest;',

        'data' => NULL

    ],


    'moderator' => [

        'type' => Item::TYPE_ROLE,

        'description' => 'Moderator',

        'children' => [

            'user',         // Can manage all that user can

            'manageThing1', // and also thing1

        ],

        'bizRule' => NULL,

        'data' => NULL

    ],


    'admin' => [

        'type' => Item::TYPE_ROLE,

        'description' => 'Admin',

        'children' => [

            'moderator',    // can do all the stuff that moderator can

            'manageThing2', // and also manage thing2

        ],

        'bizRule' => NULL,

        'data' => NULL

    ],


    'godmode' => [

        'type' => Item::TYPE_ROLE,

        'description' => 'Super admin',

        'children' => [

            'admin',        // can do all that admin can

            'manageThing3', // and also thing3

        ],

        'bizRule' => NULL,

        'data' => NULL

    ],


];

And voila, now you can add access control filters to controllers




public function behaviors()

{

    return [

        'access' => [

            'class' => 'yii\web\AccessControl',

            'except' => ['something'],            

            'rules' => [

                [

                    'allow' => true,

                    'roles' => ['manageThing1'],

                ],

            ],

        ],

    ];

}



Have fun.

PS. Right now I don’t understand what is defaultRoles and how I can use them effectively.

Also, I think this can be done a little bit simpler.

I hope Yii2’s core devs will shed some light on it.

Also, this is the simplest way to use roles avoiding RBAC:

http://www.yiiframework.com/forum/index.php/topic/49013-accesscontrol-and-roles/page__p__228758__fromsearch__1#entry228758

Here how it goes:

First of all, we’ll override AccessRule.


<?php


namespace app\components;


class AccessRule extends \yii\web\AccessRule

{

    protected function matchRole($user)

    {

        if (empty($this->roles)) {

            return true;

        }

        foreach ($this->roles as $role) {

            if ($role === '?' && $user->getIsGuest()) {

                return true;

            } elseif ($role === '@' && !$user->getIsGuest()) {

                return true;

            } elseif (!$user->getIsGuest()) {

                // user is not guest, let's check his role (or do something else)

                if ($role === $user->identity->role) {

                    return true;

                }

            }

        }

        return false;

    }

}



Next, we’ll add access control filter, pure role-based


<?php


namespace app\modules\admin\controllers; // whatever


use yii\web\Controller;


class AdminController extends Controller

{

    public function behaviors()

    {

        return [

            'access' => [

                'class' => 'yii\web\AccessControl',

                'ruleConfig' => [

                    'class' => 'app\components\AccessRule' // OUR OWN RULE

                ],

                'rules' => [

                    [

                        'allow' => true,

                        'roles' => ['role1', 'role2'], // ACCESS IS ALLOWED ONLY FOR role1 and role2

                    ],

                ],

            ],

        ];

    }

}

1 Like

If you don’t need to manage permissions via the web, I would strongly recommend forgetting about RBAC and just using methods in your user models. With a setup this simple, you will save yourself the headache that is RBAC.

Why not just do something like this?




<?php

public function accessRules()

{

    return array(

        array(

            'allow',

            'expression' =>'WebUser::LEVEL_ADMIN === Yii::app()->user->level',

        ),

    );

}



I really can’t stress it enough: if you can effectively manage permissions through the code and don’t NEED to use a database, don’t use RBAC. It is massive overkill and will waste your time.

Frankly speaking, I have no idea how to solve the following pretty common task w/o using RBAC:

  1. User can create and modify his own post

  2. User can upload and manage his own images for this post.

  3. Admin can manage everything.

So the post looks like this: [id, owner_id, description]

and the image looks like this: [id, owner_id, post_id, file]

W/o RBAC full rights check would be something like




if (((Yii::$app->user->id == $image->owner_id) && (Yii::$app->user->id == $post->owner_id)) || (Yii::$app->user->identity->role == 'admin')) {

    ... can update image

} 



Looks not good.

Using RBAC, I can do something like


if (Yii::$app->user->checkAccess('editImage', ['record' => $image]) && Yii::$app->user->checkAccess('editPost', ['record' => $post])) {

    ... can edit

}

Any idea how to avoid using RBAC here?

Assuming your user is associated with some AR model:




<?php

// access rules

return array(

    // Admin has full access. Applies to every controller action.

    array(

        'allow',

        'expression' =>'Yii::app()->user->model->isAdmin',

    ),

    // delegate to user model methods to determine ownership

    array(

        'allow',

        'expression' =>'Yii::app()->user->model->isPostOwner(Yii::app()->request->getQuery("postId"))',

        'actions'    =>array('editPost', 'uploadPostImage'),

    ),

    array(

        'allow',

        'expression' =>'Yii::app()->user->model->isImageOwner(Yii::app()->request->getQuery("imageId"))',

        'actions'    =>array('editImage'),

    ),

    array('deny'),

);

...


// In your user model

public function isPostOwner($postId)

{

    $post = Post::model()->findByPk($postId);

    return !$post || ($post->owner_id === $this->id);

}


public function isImageOwner($imageId)

{

    $image = Image::model()->findByPk($imageId);

    return $image && ($image->owner_id === $this->id) && $this->isPostOwner($image->post_id);

}



If you abstract your action params away from the HTTP request, a la my yii-rest extension or the Symfony2 FOSRESTBundle, you can do this more systematically.

In general, I think action parameters should be treated as a model, not as a raw array like Yii does out of this box. This is a good reason why. Suppose the action param $postId was actually just an instance of Post; your access rules would be extremely clear. (See below.) You could also validate them!




// Access rules if action params was an object/model

return [

    [

        'allow',

        'expression' =>'Yii::app()->user->isAdmin',

    ],

    [

        'allow',

        'expression' =>[$this->actionParams->post, 'isOwnedByUser'],

        'actions' =>...

    ],

    [

        'allow',

        'expression' =>[$this->actionParams->image, 'isOwnedByUser'],

        'actions' =>...

    ],

];



Also, if we’re talking about images, what’s the best way to check access to ‘parent’ record (‘post’ in this case)?

The thing is, if we’re not checking access to parent, nasty user can attach images to other users’ posts, that’s insecure. So we need another check for owner or rights.

That includes yet another DB query and checkAccess() call. Is there any way to avoid it, or make less annoying?

PS. One day I was thinking about merging owner_id and primary key, like this:


111111_222222

^----^ ^----^

owner   PK

That’s crazy but no need to query DB for parent :)

I’d say just query the AR relationship. Use “with =>‘post’” if you want to avoid the extra DB call.

I definitely would not combine the keys. That seems like a maintenance nightmare down the road.

You could also validate the post_id attribute in your model. E.g. instead of just validating that the post exists, validate that the current user is owns it.

And to simplify the above further, you could just make your own authmanager component implementing the CDbAuthManager::checkAccess() method/signature. That way if you ever wanted to switch to RBAC you could just swap components.


// In your user model

public function isPostOwner($postId)

{

    $post = Post::model()->findByPk($postId);

    return !$post || ($post->owner_id === $this->id);

}

This doesn’t seem good to me also.

First of all, on real-world examples user model will become bulky very soon.

Next, you use extra query here.

As for me, I’d like to

  1. keep the code as clean as possible

  2. avoid extra queries for rights check.

Let’s look at common update action :


function actionUpdate($id)

{

    $record = Model::find($id);

    if (...POST) {... $record->save();}

}

We’ve already found the record, so the only thing left is to check for rights.


function actionUpdate($id)

{

    $record = Model::find($id); // HERE IT IS

    if (... cannot edit...) throw exception.

    if (...POST) {... $record->save();}

}

Yes, we can store it somewhere (private $_record) and use $this->getRecord($id) to avoid extra query.

But still we need to check for parent… :(

Yes, finally I’ve come to that. Don’t like it either, btw.

Make your own auth manager component implementing checkAccess(). Takes 10 minutes and you consolidate all access checks there. If you switch from RBAC later, you keep the same interface. (Only have to swap component.)

Also you don’t eliminate the multi-db-query problem using RBAC. If anything, RBAC adds a LOT of extra DB calls.

Any decent-sized RBAC implementation will require a web interface to manage. If you’re not planning on taking it that far it’s not worth the effort.

Besides, access checks should not be in your actions. The action should be dead simple. The ideal is something like:




<?php

public function actionSave(CActiveRecord $model, array $data=null)

{

    $model->setAttributes($data);

    $saved = $model->save();

    $this->render('saved', array('saved' =>$saved, 'model' =>$model));

}



That means handling parameter mapping and authorization before the action is ever invoked.

In most cases PhpManager is enough, so no DB calls (except for those for record and parent)

Ok, I’ll think about that. Thanks for a lot of code :)

PS. I hope we didn’t give a fright to this post’s author :)

Haha, yeah! I hope some good ideas came across. I think Yii in general is REALLY bad about keeping controllers/actions thin. Here’s the gist of my thinking. Actions should be EXTREMELY thin. Action parameters should be supplied by a persistent (throughout the request) model, so that they can be filtered. Controllers just link together filters and actions; they should only be minimally involved in the logic required to load a model.

First of all, thank you very much. I really appreciate both the extensiveness of your responses, and the thorough discussion!

ORey: thanks for explaining RBAC. It enables me to make an informed decision. I think it would be a good basis for the RBAC doc on yiisoft github. If you want I can try to submit it, with credit going to you.

In my application, everyone except admin is only able to view (different amounts of) data, so there is no need to assign owners to uploaded assets ets.

Therefore, I think I’ll skip RBAC and go with @danschmidt5189’s suggestion. My roles are expressed as an integer, where a higher number means more acces. This means I can simply check whether the user’s role number is equal to the minimum allowed role number.

Actually this is my very first bite of RBAC (before Yii2 I was using something similar to danschmidt’s approach), so I’m not sure I get it right. This needs to be reviewed and corrected by Yii core devs (or by somebody who knows how to do it the right way).

But in general I don’t mind if you use it for commit or something. Just fix my terrible English :)

Future proof yourself by implementing this through your own AuthManager component that implements checkAccess(). Use the \yii\rbac\Manager::checkAccess() signature. ($itemName, $userId, $params.)

It’s a quick implementation, and should you ever decide to switch to RBAC you will only have to change the component and not all of your authorization checks. IF it ever comes to that, you will be happy you did.

Ok, I’m done. Happy coding. :)

Only one little correction: we’re inside Yii 2.0 thread, so class names and signatures may differ.

Thanks for the advice!

By the way, I added your info to the Yii doc: https://github.com/yiisoft/yii2/pull/1300

I’m not very sure this is the way RBAC is running but I’m just starting trying to understand yii2 RBAC. In the above example, I suppose a role should be assigned to ‘roles’ and not an operation. EG:




[

    'allow' => true,

    'roles' => ['moderator'],

]



Or am I totally wrong?