RBAC Bug? CheckAccessRecursive returns false if rule fails without checking parents

Hello, I am developing an app with several roles and I think I have found a bug. I think I have narrowed down the issue to the fact that in DbManager in the function checkAccessRecursive() it returns false if executeRule() fails without checking the parents.

I have two roles I am working on with the issue in question. They are Admin, and its child role, Affiliate. Admin is an assigned role that works by adding the role to the user manually. Affiliate is a defaultRule which is assigned by virtue of the user having an entry in the affiliate table in the database.

I have created a page that restricts access to users that are an Affiliate. Based on assigning Affiliate as a child of Admin I’d expect a user with the admin role to pass the check, but they do not. I have found the issue here (https://github.com/yiisoft/yii2/blob/a702e9be14f8c08488087364844046908d1777fd/framework/rbac/DbManager.php#L206) because for some reason if there is a rule to execute and it returns false it does not check any of the parents. So, my users that have Admin assigned don’t even have that check run because the function returns false before it gets to checking the parents.

This seems like a bug to me, but I am honestly not certain if I have just configured my app wrong and there is some practice that I should be following which I am not. I will follow up in a comment with more of my exact code, but I wanted to be concise in my original post. So, does anyone have any insights into this?

Here is my code:


        // --- Add rule for determining if user is an affiliate
        $auth = Yii::$app->authManager;
        $affiliateRule = new AffiliateRule;
        $auth->add($affiliateRule);

        // --- Add role for affiliates
        $affiliateRole = new AffiliateRole;
        $auth->add($affiliateRole);

        // --- Make affiliate a child of admin
        $adminRole = $auth->getRole(AdminRole::ROLE_NAME);
        $auth->addChild($adminRole, $affiliateRole);

        // --- Make a permission to perform affiliate actions
        $performAffiliateActionsPermission = new PerformAffiliateActionsPermission;
        $auth->add($performAffiliateActionsPermission);

        // --- Assign it to affiliate role
        $auth->addChild($affiliateRole, $performAffiliateActionsPermission);
class AffiliateRule extends Rule
{
    /**
     * String constant representing the rule that identifies an affiliate
     * @var string
     */
    const RULE_NAME = 'isAffiliate';

    /**
     * @inheritdoc
     */
    public $name = self::RULE_NAME;

    /**
     * Checks the database to see if there is a record in the affiliate table
     * with the id of the currently logged in user
     * {@inheritdoc}
     */
    public function execute($user, $item, $params)
    {
        return Yii::$app->db->createCommand('SELECT `user_id` FROM `affiliate` WHERE `user_id`=:user')
            ->bindValue(':user', $user)
            ->queryScalar();
    }
}
class AffiliateRole extends Role
{
    /**
     * String constant for the affiliate role
     * @var string
     */
    const ROLE_NAME = 'affiliate';

    /**
     * @inheritdoc
     */
    public $name = self::ROLE_NAME;

    /**
     * Name of the rule that determines if this role applies
     * @var string
     */
    public $ruleName = AffiliateRule::RULE_NAME;

    /**
     * @inheritdoc
     */
    public $description = 'Allows users access to affiliate pages on the site.'; 
}
class PerformAffiliateActionsPermission extends Permission
{
    /**
     * String constant representing the permission
     * @var string
     */
    const PERMISSION_NAME = 'performAffiliateActions';

    /**
     * @inheritdoc
     */
    public $name = self::PERMISSION_NAME;

    /**
     * @inheritdoc
     */
    public $description = 'Allows a user to perform affiliate actions such as upload a W9, view their referred users, add referral link on all card, request withdrawals, and more.';
}
	/**
	 * User can edit their profile
	 * @return string
	 */
	public function actionIndex()
	{
		if(Yii::$app->user->can(PerformAffiliateActionsPermission::PERMISSION_NAME)){
			exit;
		}
		$referredUsers = User::find()
			->withActiveSubscriptionPlan()
			->withChargesForNetRevenue()
			->where([
				'referring_user_id' => Yii::$app->user->id
			])->asArray()
			->all();
		return $this->render('index', [
			'referredUsers' => $referredUsers
		]);
	}

When I visit the page that runs this action in the browser while logged in as an admin user, it runs the action and doesn’t hit the exit; line. Users assigned Admin since they are a parent of Affiliate users should have pass the test for that permission and the line exit; should be hit. However, it seems since Affiliate role has a rule associated with it, it never goes on to check the parent.

I would also like to note that I have read this (https://www.yiiframework.com/doc/guide/2.0/en/security-authorization#using-default-roles) but do not understand why they suggest to set up the parent-child relationship in both the DbManager as well as in the rule class. Isn’t this redundant? If checkAccessRecursive() didn’t return false after executeRule() returned false, and it checked the parents, wouldn’t this be unnecessary?

It seems like doing this would resolve my issue, which I have done for now as a hack, but it just doesn’t feel like I am doing the right thing. My circumstance is also a bit different from their example so I’m not 100% sure it applies to what I’m doing.

I have the same issue and I agree it seems like a bug. I don’t know maybe they didn’t want to check parents for performance reasons.
In my case I could implement the execute() method in a ‘recursive’ way because every parent had the same rule.
In your case I would override the checkAccessRecursive() method in a way that always checks for parents, even when rule returns false.