Brett Brett - 7 months ago 29
PHP Question

Setting custom denyCallback even when returning false from matchCallback with Yii2 behaviours

I'm using

Yii2
and utilising their behaviors within my controllers.

I am building my own permissions system and because the permissions are rather complex I need to make use of a matchCallback.

Here is an example:

public function behaviors() {
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['view'],
'rules' => [
[
'allow' => true,
'actions' => ['view'],
'matchCallback' => function ($rule, $action) {
return Yii::$app->authManager->can($rule, $action);
}
],
// everything else is denied
],
],
];
}


Now, unfortunately the way the
matchCallback
works is by returning
true
or
false
on if it should continue to execute the rule, rather than being able to return true or false of they are allowed or not.

So if I return
false
that it shouldn't continue (and hence disallow them) then I am unable to customise the
denyCallback
as it quits executing the rule.

Is there anyway I can customise the
denyCallback
even if I return
false
from the
matchCallback
- or should I be handling my situation in a different way?

Answer

You can define denyCallback as property of AccessControl instead of defining it in AccessRule. It will get called if allow after rule check returns null. It has the same signature as denyCallback in AccessRule:

public function behaviors() {
    return [
        'access' => [
            'class' => AccessControl::className(),
            'only' => ['view'],
            'rules' => [
                [
                    'allow' => true,
                    'actions' => ['view'],
                    'matchCallback' => function ($rule, $action) {
                        return Yii::$app->authManager->can($rule, $action);
                    }
                ],
            'denyCallback' => function ($rule, $action){...}
            // everything else is denied
            ],
        ],
    ];
}  

As another option you can extend AccessRule class and override allows() method to return false instead of null when match check fails, and your rule's denyCallback will be called then:

class MyAccessRule extends AccessRule
{
    public function allows($action, $user, $request)
    {
        $allows = parent::allows($action, $user, $request);
        if ($allows === null) {
            return false;
        } else {
            return $allows;
        }

    }
}

matchCallback only determines should rule be applied or not, and if matchCallback returns true and other parameters are match(e.g. roles, verbs etc.), call to allows() will return rule's allow parameter true or false as you set it in configuration. And if matchCallback returns false - allow will be null and rule's denyCallback will not be called, but AccessControl denyCallback will be called instead if it is set in configuration.

As you mentioned in the comments third option is to make allows() return result of callback.

class MyAccessRule extends AccessRule
{
    public $allowCallback;
    public function allows($action, $user, $request)
    {
        if(!empty($this->allowCallback) {
            return call_user_func($this->allowCallback);
        }
        $allows = parent::allows($action, $user, $request);
        if ($allows === null) {
            return false;
        } else {
            return $allows;
        }

    }
}