5

This is a question about RBAC usage in Yii2.

So far I have found it to work rather well and satisfactory, however there is one key feature that I am missing: The ability for Yii2 Rules to provide "feedback" in a similar way as Yii2 Validators set Error messages to explain why the validation failed. I am looking for a way to provide some sort of feedback as to why the permission was not granted.

In particular, the can() method will return a boolean type, which is fine, but when checking for permission we have no idea why exactly the user was not granted that particular permission.

To give a more practical example. Let's say we are trying to determine whether the current user can submit a comment. We would typically do something like this:

if (Yii::$app->user->can('postComment',['comment'=>$comment])) {
    $comment->post();
} else {
    throw new ForbiddenHttpException('Sorry m8, you cant do this. No idea why tho!');
}

It works great, but as shown in the example we really have no idea why the user isn't able to post the comment. Could be any number of reasons, for example because the thread is locked or because they do not have permission to post in a certain category or because they dont have a high enough reputation etc. But we want to tell the user why! So my question is, how do we get that feedback from Yii2's RBAC?

mae
  • 12,340
  • 8
  • 28
  • 40
  • Normally RBAC gives `Forbidden` exception when user don't have permission or you can write custom message, or use try-catch. – Insane Skull Apr 01 '16 at 09:47
  • RBAC in Yii2 does no such thing. It only returns a YES/NO (boolean) answer. It is then up to the programmer to throw an exception based on that minimal information. – mae Apr 01 '16 at 09:55
  • Only throws error if user didn't have the postComment permission. – stig-js Apr 01 '16 at 11:17
  • Well, thanks, but I'm looking for more than a poor workaround. – mae Apr 01 '16 at 13:04

4 Answers4

0

You would want to create your own AccessRule and set the message exceptions from your scenarios by overriding the current methods in that class. matchRole would be the method you would be overriding. Yii2 doesn't have this in place so you would have to roll your own AccessRule to do so.

Then once its created attach it to your controllers:

public function behaviors()
{
    return [
        'access' => [
            'class' => AccessControl::className(),
            'ruleConfig' => [
                'class' => 'app\components\AccessRule'
            ],
            'rules' => [
                /* my normal rules */
            ],
        ],
    ];
}
tripskeet
  • 176
  • 1
  • 12
  • Unfortunately this isn't really an option as I need to use permission checking everywhere, not only controllers (also in bootstraps, components, widgets, models, etc). – mae Apr 02 '16 at 04:47
  • They all have behaviors or can have behaviors attached. [BehaviorsGuide](http://www.yiiframework.com/doc-2.0/guide-concept-behaviors.html) – tripskeet Apr 04 '16 at 15:06
0

So basically all I did was add

'message' => 'Current password cannot be blank.' 

to my rules.

Make sure you seperate the correct rules, so you don't get that message on multiple fields, where it doesn't make sense. Also make sure you add it on the 'required' rule, unless you want that message to show when it's another rule..

I hope this helped you guys, as I spent a bit too much time searching for it.

Shaig Khaligli
  • 3,165
  • 4
  • 19
  • 31
0

The fastest way to get feedback from RBAC is to pass an object as a parameter. If the check fails then the error text is entered into this object. In this way, the reason for the negative test result can be obtained.

Let's say we want to prevent users from commenting on their posts.

Validation in Rule. This Rule is associated with postComment permission:

class PostCommentRule extends yii\rbac\Rule
{
    public $name = 'PostCommentRuleName';

    public function execute($user, $item, $params)
    {
        $allowed = $params['post']->owner_id !== $user;

        if(!$allowed && isset($params['errorObject']))
            $params['errorObject']->errorText = 'Comments to oneself are not allowed.';

        return $allowed;
    }
}

We check permission and in case of prohibition we have a reason:

$errorObject = new stdClass();

if (Yii::$app->user->can('postComment',['post'=>$post,'errorObject'=>$errorObject])) {
    $comment->post();
} else {
    throw new ForbiddenHttpException($errorObject->errorText);
}
0

In your case I would create a base permission class which will cover an abstraction for specific restriction message with one simple method and it will be extended by all your permissions.

This is the abstract permission blueprint.

abstract class AbstractPermission extends Permission
{
    /**
     * @return string
     */
    abstract public function getRestrictionMessage(): string;
}

Creating custom database manager in order to check if retrieved permission has implement the abstraction.

class CustomDbManager extends DbManager 
{
    /**
     * @throws \Exception
     * @return AbstractPermission|null
     */
    public function getPermission($name): ?AbstractPermission 
    {
        $permission = parent::getPermission($name);

        if ($permission === null) {
            return null;
        }

        if (!$permission instanceof AbstractPermission) {
            throw new \Exception(
                'Your permission class should be derived from ' . AbstractPermission::class
            );
        }

        return $permission;
    }
}

Define CustomDbManager in your configuration file

'components' => [
    'authManager' => [
        'class' => CustomDbManager::class
    ],
    ...
];

Example with your PostCommentPermission.

class PostCommentPermission extends AbstractPermission
{
    /**
     * @return string
     */
    public function getRestrictionMessage(): string
    {
        return 'You cannot post comments!';
    }
}

And finally invoke your manager with specific permission check

$authManager = Yii::$app->getAuthManager();

$postCommentPermission = $authManager->getPermission('postComment');

if (Yii::$app->user->can($postCommentPermission->name, ['comment' => $comment])) {
    $comment->post();
} else {    
    throw new ForbiddenHttpException($postCommentPermission->getRestrictionMessage());
}
G.Spirov
  • 178
  • 9