Mike Pearson Mike Pearson - 6 months ago 89
SQL Question

Yii2 RBAC Multiple Assignments for Each User Based on Groups

My application technically has two areas, a global area (feedback, user profile, user settings, etc) and a group area (contacts, projects, group profile, group settings, etc).

I am using the RBAC DBManager for the global area, and it works just fine, but I am having issues implementing an authorization mechanism for the group area.

The reason, is that groups can be shared among the users, and a user may have multiple assignments in the group_access table (id, group_id, user_id, item_name) as they may be members of multiple groups, and they may have different permission levels for those groups.

Here is my auth setup:

$auth = Yii::$app->authManager;

// group permissions
$manageGroupUsers = $auth->createPermission('manage_group_users');
$manageGroupUsers->description = 'Manage Group Users';
$auth->add($manageGroupUsers);

$manageGroupSettings = $auth->createPermission('manage_group_settings');
$manageGroupSettings->description = 'Manage Group Settings';
$auth->add($manageGroupSettings);

// app permissions
$manageAppUsers = $auth->createPermission('manage_app_users');
$manageAppUsers->description = 'Manage App Users';
$auth->add($manageAppUsers);

$manageAppGroups = $auth->createPermission('manage_app_groups');
$manageAppGroups->description = 'Manage App Groups';
$auth->add($manageAppGroups);

$manageAppSettings = $auth->createPermission('manage_app_settings');
$manageAppSettings->description = 'Manage App Settings';
$auth->add($manageAppSettings);

$manageAppFeedback = $auth->createPermission('manage_app_feedback');
$manageAppFeedback->description = 'Manage App Feedback';
$auth->add($manageAppFeedback);

// group roles
// -- create role
$groupUser = $auth->createRole('group_user');
$groupUser->description = 'Group Users';
$auth->add($groupUser);

// -- create role
$groupAdmin = $auth->createRole('group_admin');
$groupAdmin->description = 'Group Administrators';
$auth->add($groupAdmin);
// add permissions
$auth->addChild($groupAdmin, $manageGroupUsers);
$auth->addChild($groupAdmin, $manageGroupSettings);
// inherit permissions
$auth->addChild($groupAdmin, $groupUser);

// -- create role
$groupCreator = $auth->createRole('group_creator');
$groupCreator->description = 'Group Creators';
$auth->add($groupCreator);
// inherit permissions
$auth->addChild($groupCreator, $groupAdmin);

// app roles
// -- create role
$appUser = $auth->createRole('app_user');
$appUser->description = 'App Users';
$auth->add($appUser);

// -- create role
$appSupport = $auth->createRole('app_support');
$appSupport->description = 'Support Users';
$auth->add($appSupport);
// add permissions
$auth->addChild($appSupport, $manageAppFeedback);

// -- create role
$appAdmin = $auth->createRole('app_admin');
$appAdmin->description = 'App Administrators';
$auth->add($appAdmin);
// add permissions
$auth->addChild($appAdmin, $manageAppUsers);
$auth->addChild($appAdmin, $manageAppGroups);
$auth->addChild($appAdmin, $manageAppSettings);
// inherit permissions
$auth->addChild($appAdmin, $appUser);
$auth->addChild($appAdmin, $appSupport);

// -- create role
$appCreator = $auth->createRole('app_creator');
$appCreator->description = 'App Creators';
$auth->add($appCreator);
// inherit permissions
$auth->addChild($appCreator, $appAdmin);


My group_access table has the same schema as the auth_assignment table, with the exception that it has a group_id column, and the user_id column is NOT unique.

The user will only have one assignment concerning the global area, but may have many different assigments on the group area as they might have admin privelidges on group a, but only user privielidges on group b.

My DB is set up like:


  1. Users (status_id, username, auth_key, password_hash, email, etc)

  2. Groups (status_id, name, description, etc)

  3. Group_Access (group_id, user_id, item_name) Each user gets one assignment for each group they have access to.

    sample_group_access_records [
    [
    'id' => 1,
    'user_id' => 35,
    'group_id' => 17,
    'item_name' => 'group_admin'
    ],
    [
    'id' => 2,
    'user_id' => 35,
    'group_id' => 356,
    'item_name' => 'group_user'
    ],
    [
    'id' => 3,
    'user_id' => 35,
    'group_id' => 211,
    'item_name' => 'group_creator'
    ],
    ];



The checkAccess function can qualify the userID, and I can even use the shorter "can" version which works great for the logged in user, but I need to check access based on a user option like below:

Option::getOption('user', 'active_group_id')


This is a custom function that pulls the active group id from a user options table. If a user switches groups, this will be changed. My options model has three types 'app', 'user', 'group'.

It would be nice if I could figure out a function that works the same was as the native checkAccess but be called checkGroupAccess and automatically get the active_group_id and pull the user assignments from the group_access table and perform the permission check.

I hope this makes sense.

Thank you for your time.

Mike

** UPDATED **

So, I have a solution, that uses custom checkAccess functions to check for proper permissions on the group or global areas.

I have two tables (user_access, group_access) that have a similar schema to the default {{auth_assignment}} table, of which I am not using now. I am using the {{auth_item}}, {{auth_item_child}}, and {{auth_rule}} tables.

I have two models, one for each of the access tables GroupAccess => group_access, and UserAccess => user_access.

I also have a model for the access functions and have mapped it to the components configuration.

Here is my access model:

<?php

namespace app\models;

use Yii;

class Access
{

public function canUser($type, $permissionName, $params = [])
{

switch ($type) {

case 'group':

$userID = Yii::$app->user->identity->id;
$groupID = Yii::$app->options->getOption('user', 'active_group_id');

$queryAll = GroupAccess::find()
->where('user_id = :user_id and group_id = :group_id', [':user_id' => $userID, ':group_id' => $groupID])
->asArray()
->all();

$assignments = [];
foreach ($queryAll as $queryItem) {
$assignments[$queryItem['item_name']] = [
'userId' => $queryItem['user_id'],
'roleName' => $queryItem['item_name'],
'createdAt' => $queryItem['created_date'],
];
}

$result = self::checkAccess($userID, $permissionName, $assignments, $params);

return $result;

break;

case 'user':

$userID = Yii::$app->user->identity->id;

$queryAll = UserAccess::find()
->where(['user_id' => $userID])
->asArray()
->all();

$assignments = [];
foreach ($queryAll as $queryItem) {
$assignments[$queryItem['item_name']] = [
'userId' => $queryItem['user_id'],
'roleName' => $queryItem['item_name'],
'createdAt' => $queryItem['created_date'],
];
}

$result = self::checkAccess($userID, $permissionName, $assignments, $params);

return $result;

break;

}

}

public function checkAccess($userID, $permissionName, $assignments, $params = [])
{

$auth = Yii::$app->authManager;

$auth->loadFromCache();

if ($auth->items !== null) {
return $auth->checkAccessFromCache($userID, $permissionName, $params, $assignments);
} else {
return $auth->checkAccessRecursive($userID, $permissionName, $params, $assignments);
}
}

public function assign($type, $role, $userID = null, $groupID = null)
{

switch ($type) {

case 'group':

// clear existing assigments
self::revoke('group', $userID, $groupID);

$groupAccess = new GroupAccess();
$groupAccess->group_id = $groupID;
$groupAccess->user_id = $userID;
$groupAccess->item_name = $role;
$groupAccess->created_date = time();

return $groupAccess->save();

break;

case 'user':

// clear existing assignments
self::revoke('user', $userID);

$userAccess = new UserAccess();
$userAccess->user_id = $userID;
$userAccess->item_name = $role;
$userAccess->created_date = time();

return $userAccess->save();

break;

}

}

public function revoke($type, $userID, $groupID = null)
{

switch ($type) {

case 'group':

GroupAccess::deleteAll('user_id = :user_id and group_id = :group_id', [':user_id' => $userID, ':group_id' => $groupID]);

break;

case 'user':

UserAccess::deleteAll('user_id = :user_id', [':user_id' => $userID]);

break;

}

}

}


And here are some sample uses to access the functions:

// get the user option
echo Yii::$app->options->getOption('user', 'active_group_id');

// assign group role
Yii::$app->access->assign('group', 'group_creator', 22, 18);
// assign user role
Yii::$app->access->assign('user', 'app_user', 22);

// revoke group access
Yii::$app->access->revoke('group', 22, 18);
// revoke user access
Yii::$app->access->revoke('user', 22);

// test user permission
var_dump(Yii::$app->access->canUser('user', 'manage_app_settings'));
// test the group permission
var_dump(Yii::$app->access->canUser('group', 'manage_group_settings'));


In essence, I copied the checkAccess function from the DbManager and reworked it a little to check for user access based on group.

The only issue, is that I had to make a change to the actual source DbManager class to make the $items (property), checkAccessFromCache (function), and checkAccessRecursive (function) all public so they can be accessed outside of the class. The main drawback is updateability...

Any way around this?

Thanks.

Answer

Here is a working final solution.

So, another day, more refactoring.

My final solution uses the checkAccess function in the DbManager/ManagerInterface source files, but I added the $assignments parameter to be passed. The main issue is that I had to build my own assignments list for checking. Make sure you comment out the lines where the $assignments variable is set.

Here is my new access model:

<?php

namespace app\models;

use Yii;

class Access
{

public function canUser($type, $permissionName, $params = [])
{

    $auth = Yii::$app->authManager;

    switch ($type) {

        case 'group':

        $userID = Yii::$app->user->identity->id;
        $groupID = Yii::$app->options->getOption('user', 'active_group_id');

        $queryAll = GroupAccess::find()
        ->where('user_id = :user_id and group_id = :group_id', [':user_id' => $userID, ':group_id' => $groupID])
        ->asArray()
        ->all();

        $assignments = [];
        foreach ($queryAll as $queryItem) {
            $assignments[$queryItem['item_name']] = [
            'userId' => $queryItem['user_id'],
            'roleName' => $queryItem['item_name'],
            'createdAt' => $queryItem['created_date'],
            ];
        }

        $result = $auth->checkAccess($userID, $permissionName, $assignments, $params);

        return $result;

        break;

        case 'user':

        $userID = Yii::$app->user->identity->id;

        $queryAll = UserAccess::find()
        ->where('user_id = :user_id', [':user_id' => $userID])
        ->asArray()
        ->all();

        $assignments = [];
        foreach ($queryAll as $queryItem) {
            $assignments[$queryItem['item_name']] = [
            'userId' => $queryItem['user_id'],
            'roleName' => $queryItem['item_name'],
            'createdAt' => $queryItem['created_date'],
            ];
        }

        $result = $auth->checkAccess($userID, $permissionName, $assignments, $params);

        return $result;

        break;

    }

}

public function assign($type, $role, $userID = null, $groupID = null)
{

    switch ($type) {

        case 'group':

        // clear existing assigments
        self::revoke('group', $userID, $groupID);

        $groupAccess = new GroupAccess();
        $groupAccess->group_id = $groupID;
        $groupAccess->user_id = $userID;
        $groupAccess->item_name = $role;
        $groupAccess->created_date = time();

        return $groupAccess->save();

        break;

        case 'user':

        // clear existing assignments
        self::revoke('user', $userID);

        $userAccess = new UserAccess();
        $userAccess->user_id = $userID;
        $userAccess->item_name = $role;
        $userAccess->created_date = time();

        return $userAccess->save();

        break;

    }

}

public function revoke($type, $userID, $groupID = null)
{

    switch ($type) {

        case 'group':

        GroupAccess::deleteAll('user_id = :user_id and group_id = :group_id', [':user_id' => $userID, ':group_id' => $groupID]);

        break;

        case 'user':

        UserAccess::deleteAll('user_id = :user_id', [':user_id' => $userID]);

        break;

    }

}

}

And here is the modified checkAccess function in DbManager:

public function checkAccess($userId, $permissionName, $assignments, $params = [])
{
    //$assignments = $this->getAssignments($userId);
    $this->loadFromCache();
    if ($this->items !== null) {
        return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);
    } else {
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
    }
}

And here is the modified checkAccess function in ManagerInterface.php:

public function checkAccess($userId, $permissionName, $assignments, $params = []);

I did not change the $items, checkAccessFromCache, and checkAccessRecursive functions to public from protected.

And here is my UserAccess model:

<?php

namespace app\models;

use Yii;
use yii\db\ActiveRecord;

 /**
 * This is the model class for table "app_user_access".
 *
 * @property integer $id
 * @property integer $user_id
 * @property string $item_name
 * @property integer $created_date
 *
 * @property AppAuthItem $itemName
 * @property AppUsers $user
 */
class UserAccess extends ActiveRecord
{
/**
 * @inheritdoc
 */
public static function tableName()
{
    return 'app_user_access';
}

/**
 * @inheritdoc
 */
public function rules()
{
    return [
        [['user_id', 'item_name', 'created_date'], 'required'],
        [['user_id', 'created_date'], 'integer'],
        [['item_name'], 'string', 'max' => 64]
    ];
}

/**
 * @inheritdoc
 */
public function attributeLabels()
{
    return [
        'id' => 'ID',
        'user_id' => 'User ID',
        'item_name' => 'Item Name',
        'created_date' => 'Created Date',
    ];
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getItemName()
{
    return $this->hasOne(AppAuthItem::className(), ['name' => 'item_name']);
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getUser()
{
    return $this->hasOne(AppUsers::className(), ['id' => 'user_id']);
}
}

And here is the the GroupAccess Model:

<?php

namespace app\models;

use Yii;
use yii\db\ActiveRecord;

 /**
 * This is the model class for table "app_group_access".
 *
 * @property integer $id
 * @property integer $group_id
 * @property integer $user_id
 * @property string $item_name
 * @property integer $created_date
 *
 * @property AppUsers $user
 * @property AppAuthItem $itemName
 * @property AppGroups $group
 */
class GroupAccess extends ActiveRecord
{
/**
 * @inheritdoc
 */
public static function tableName()
{
    return 'app_group_access';
}

/**
 * @inheritdoc
 */
public function rules()
{
    return [
        [['group_id', 'user_id', 'item_name', 'created_date'], 'required'],
        [['group_id', 'user_id', 'created_date'], 'integer'],
        [['item_name'], 'string', 'max' => 64]
    ];
}

/**
 * @inheritdoc
 */
public function attributeLabels()
{
    return [
        'id' => 'ID',
        'group_id' => 'Group ID',
        'user_id' => 'User ID',
        'item_name' => 'Item Name',
        'created_date' => 'Created Date',
    ];
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getUser()
{
    return $this->hasOne(AppUsers::className(), ['id' => 'user_id']);
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getItemName()
{
    return $this->hasOne(AppAuthItem::className(), ['name' => 'item_name']);
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getGroup()
{
    return $this->hasOne(AppGroups::className(), ['id' => 'group_id']);
}
}

And once again, some useful samples:

// assign group role
Yii::$app->access->assign('group', 'group_creator', 24, 20);
// assign user role
Yii::$app->access->assign('user', 'app_user', 24);

// revoke group
Yii::$app->access->revoke('group', 22, 18);
// revoke user
Yii::$app->access->revoke('user', 22);

// test user permission
var_dump(Yii::$app->access->canUser('user', 'manage_app_settings'));
// test the group permission
var_dump(Yii::$app->access->canUser('group', 'manage_group_settings'));