Quantcast
Channel: Live News for Yii Framework
Viewing all articles
Browse latest Browse all 3361

[Wiki] MongoDB's RBAC support class extending BaseManager

$
0
0

Here's a possible implementation for MongoDb of the RBAC standard. Create a class "MongoDbManager" in common\components and modify your params.php file like this:

'components.authManager'=>[
    'class'=>\common\components\MongoDbManager::className(),
    'itemChildCollection'=>'rbac.item.child',
    'itemCollection'=>'rbac.item',
    'ruleCollection'=>'rbac.rule',
    'assignmentCollection'=>'rbac.assignment'
]

and the actual class is:

namespace common\components;
 
 
use yii\base\InvalidCallException;
use yii\base\InvalidParamException;
use yii\di\Instance;
use yii\mongodb\Connection;
use yii\mongodb\Query;
use yii\rbac\Assignment;
use yii\rbac\BaseManager;
use yii\rbac\Item;
use yii\rbac\Permission;
use yii\rbac\Role;
use yii\rbac\Rule;
use Yii;
 
class MongoDbManager extends BaseManager
{
    /**
     * @var string|Connection
     */
    public $mongodb='mongodb';
 
    /**
     * @var string $itemChildCollection
     */
    public $itemChildCollection='rbac.item.child';
 
    /**
     * @var string $itemCollection
     */
    public $itemCollection = 'rbac.item';
 
    /**
     * @var string $itemCollection
     */
    public $ruleCollection = 'rbac.rule';
 
    /**
     * @var string $assignmentCollection
     */
    public $assignmentCollection = 'rbac.assignment';
 
    /**
     * Initializes the application component.
     * This method overrides the parent implementation by establishing the database connection.
     */
    public function init()
    {
        parent::init();
        $this->mongodb = Instance::ensure($this->mongodb, Connection::className());
    }
 
    /**
     * Returns the items of the specified type.
     *
     * @param integer $type the auth item type (either [[Item::TYPE_ROLE]] or [[Item::TYPE_PERMISSION]]
     * @return Item[] the auth items of the specified type.
     */
    protected function getItems($type)
    {
        $query = (new Query)
            ->from($this->itemCollection)
            ->where(['type' => $type]);
 
        $items = [];
 
        foreach ($query->all($this->mongodb) as $row) {
            $items[$row['name']] = $this->populateItem($row);
        }
 
        return $items;
    }
 
    /**
     * Adds an auth item to the RBAC system.
     *
     * @param Item $item can be either Item or Rule
     * @return boolean whether the auth item is successfully added to the system
     * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique)
     */
    protected function addItem($item)
    {
        $time = time();
        if ($item->createdAt === null) {
            $item->createdAt = $time;
        }
        if ($item->updatedAt === null) {
            $item->updatedAt = $time;
        }
 
        $this->mongodb->getCollection($this->itemCollection)->update(['_id'=>$item->name],[
            '$setOnInsert'=>[
                'name'=>$item->name,
                'type' => $item->type,
                'description' => $item->description,
                'rule_name' => $item->ruleName,
                'data' => $item->data === null ? null : serialize($item->data),
                'created_at' => $item->createdAt,
                'updated_at' => $item->updatedAt,
            ]
        ],['upsert'=>true]);
 
        return true;
    }
 
    /**
     * Returns the named auth item.
     *
     * @param string $name the auth item name.
     * @return Item the auth item corresponding to the specified name. Null is returned if no such item.
     */
    protected function getItem($name)
    {
        $row = (new Query())->from($this->itemCollection)
                            ->where(['name' => $name])
                            ->one($this->mongodb);
 
        if ($row === false) {
            return null;
        }
 
        if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) {
            $row['data'] = null;
        }
 
        return $this->populateItem($row);
 
    }
 
    /**
     * Updates an auth item in the RBAC system.
     *
     * @param string $name the old name of the auth item
     * @param Item $item
     * @return boolean whether the auth item is successfully updated
     * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique)
     */
    protected function updateItem($name, $item)
    {
        $this->mongodb->getCollection($this->itemCollection)->update(['_id'=>$item->name],[
            '$setOnInsert'=>[
                'name'=>$item->name,
                'type' => $item->type,
                'description' => $item->description,
                'rule_name' => $item->ruleName,
                'data' => $item->data === null ? null : serialize($item->data),
                'created_at' => $item->createdAt,
                'updated_at' => $item->updatedAt,
            ]
        ],['upsert'=>true]);
 
        return true;
    }
 
    /**
     * Removes an auth item from the RBAC system.
     *
     * @param Item $item
     * @return boolean whether the role or permission is successfully removed
     * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique)
     */
    protected function removeItem($item)
    {
        $this->mongodb->getCollection($this->itemChildCollection)->remove([
            'name'=>$item->name
        ],['justOne'=>false]);
 
        $this->mongodb->getCollection($this->itemChildCollection)->update([
            'children'=>[
                '$in'=>[$item->name]
            ]
        ],['$pull'=>[
            'children'=>$item->name
        ]],['upsert'=>false, 'multi'=>true]);
 
        $this->mongodb->getCollection($this->assignmentCollection)->remove(['item_name'=>$item->name],['justOne'=>false]);
 
        $this->mongodb->getCollection($this->itemCollection)->remove(['_id'=>$item->name],['justOne'=>false]);
 
        return true;
    }
 
 
    /**
     * Adds a rule to the RBAC system.
     *
     * @param Rule $rule
     * @return boolean whether the rule is successfully added to the system
     * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique)
     */
    protected function addRule($rule)
    {
        $time = time();
        if ($rule->createdAt === null) {
            $rule->createdAt = $time;
        }
        if ($rule->updatedAt === null) {
            $rule->updatedAt = $time;
        }
 
        $this->mongodb->getCollection($this->ruleCollection)->update(['_id'=>$rule->name],[
            '$setOnInsert'=>[
                'name'=>$rule->name,
                'data' => serialize($rule),
                'created_at' => $rule->createdAt,
                'updated_at' => $rule->updatedAt,
            ]
        ],['upsert'=>true]);
 
        return true;
    }
 
    /**
     * Removes a rule from the RBAC system.
     *
     * @param Rule $rule
     * @return boolean whether the rule is successfully removed
     * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique)
     */
    protected function removeRule($rule)
    {
        $this->mongodb->getCollection($this->itemCollection)->update(['rule_name'=>$rule->name],['rule_name'=>null],['multi'=>true]);
        $this->mongodb->getCollection($this->ruleCollection)->remove(['name'=>$rule->name],['justOne'=>false]);
    }
 
    /**
     * Updates a rule to the RBAC system.
     *
     * @param string $name the old name of the rule
     * @param Rule $rule
     * @return boolean whether the rule is successfully updated
     * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique)
     */
    protected function updateRule($name, $rule)
    {
        $this->mongodb->getCollection($this->itemCollection)->update([
            'rule_name'=>$name
        ],[
            'rule_name'=>$rule->name
        ]);
 
        $rule->updatedAt = time();
 
        $this->mongodb->getCollection($this->ruleCollection)->update([
            'name'=>$name
        ],[
            'name' => $rule->name,
            'data' => serialize($rule),
            'updated_at' => $rule->updatedAt,
        ]);
 
        return true;
    }
 
    /**
     * Returns the rule of the specified name.
     *
     * @param string $name the rule name
     * @return Rule the rule object, or null if the specified name does not correspond to a rule.
     */
    public function getRule($name)
    {
        $row = (new Query)->select(['data'])
                          ->from($this->ruleCollection)
                          ->where(['name' => $name])
                          ->one($this->mongodb);
        return $row === false ? null : unserialize($row['data']);
    }
 
    /**
     * Returns all rules available in the system.
     *
     * @return Rule[] the rules indexed by the rule names
     */
    public function getRules()
    {
        $query = (new Query)->from($this->ruleCollection);
 
        $rules = [];
        foreach ($query->all($this->mongodb) as $row) {
            $rules[$row['name']] = unserialize($row['data']);
        }
 
        return $rules;
    }
 
 
    /**
     * Checks if the user has the specified permission.
     *
     * @param string|integer $userId the user ID. This should be either an integer or a string representing
     * the unique identifier of a user. See [[\yii\web\User::id]].
     * @param string $permissionName the name of the permission to be checked against
     * @param array $params name-value pairs that will be passed to the rules associated
     * with the roles and permissions assigned to the user.
     * @return boolean whether the user has the specified permission.
     * @throws \yii\base\InvalidParamException if $permissionName does not refer to an existing permission
     */
    public function checkAccess($userId, $permissionName, $params = [])
    {
        $assignments = $this->getAssignments($userId);
 
        return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
    }
 
    /**
     * Performs access check for the specified user.
     * This method is internally called by [[checkAccess()]].
     * @param string|integer $user the user ID. This should can be either an integer or a string representing
     * the unique identifier of a user. See [[\yii\web\User::id]].
     * @param string $itemName the name of the operation that need access check
     * @param array $params name-value pairs that would be passed to rules associated
     * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
     * which holds the value of `$userId`.
     * @param Assignment[] $assignments the assignments to the specified user
     * @return boolean whether the operations can be performed by the user.
     */
    protected function checkAccessRecursive($user, $itemName, $params, $assignments)
    {
        if (($item = $this->getItem($itemName)) === null) {
            return false;
        }
 
        Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
 
        if (!$this->executeRule($user, $item, $params)) {
            return false;
        }
 
        if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
            return true;
        }
 
        $query = new Query;
        $parents = $query->select(['name'])
                     ->from($this->itemChildCollection)
                     ->where([
                        'children' => [
                            '$in'=>[$itemName]
                        ]
                    ])->all($this->mongodb);
 
        foreach ($parents as $parent) {
            if ($this->checkAccessRecursive($user, $parent['name'], $params, $assignments)) {
                return true;
            }
        }
 
        return false;
    }
 
    /**
     * Returns the roles that are assigned to the user via [[assign()]].
     * Note that child roles that are not assigned directly to the user will not be returned.
     *
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Role[] all roles directly or indirectly assigned to the user. The array is indexed by the role names.
     */
    public function getRolesByUser($userId)
    {
        if($userId instanceof \MongoId){
            $userId = (string)$userId;
        }
 
        $assignments = (new Query())
            ->from($this->assignmentCollection)
            ->where(['user_id'=>$userId])
            ->select(['item_name'])
            ->all($this->mongodb);
 
        $roles = [];
        foreach ($assignments as $row) {
            $roles[$row['item_name']] = $this->getItem($row['item_name']);
        }
        return $roles;
    }
 
    /**
     * Returns all permissions that the specified role represents.
     *
     * @param string $roleName the role name
     * @return Permission[] all permissions that the role represents. The array is indexed by the permission names.
     */
    public function getPermissionsByRole($roleName)
    {
        $childrenList = $this->getChildrenList();
        $result = [];
        $this->getChildrenRecursive($roleName, $childrenList, $result);
        if (empty($result)) {
            return [];
        }
        $query = (new Query)->from($this->itemCollection)->where([
            'type' => Item::TYPE_PERMISSION,
            'name' => array_keys($result),
        ]);
        $permissions = [];
        foreach ($query->all($this->mongodb) as $row) {
            $permissions[$row['name']] = $this->populateItem($row);
        }
        return $permissions;
    }
 
    /**
     * Returns all permissions that the user has.
     *
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Permission[] all permissions that the user has. The array is indexed by the permission names.
     */
    public function getPermissionsByUser($userId)
    {
        if($userId instanceof \MongoId){
            $userId = (string)$userId;
        }
 
        $permissions = (new Query)
            ->select(['item_name'])
            ->from($this->assignmentCollection)
            ->where(['user_id' => (string)$userId])
            ->all($this->mongodb);
 
        $childrenList = $this->getChildrenList();
        $result = [];
 
        foreach ($permissions as $permission) {
            $this->getChildrenRecursive($permission['item_name'], $childrenList, $result);
        }
 
        if (empty($result)) {
            return [];
        }
 
        $query = (new Query)->from($this->itemCollection)->where([
            'type' => Item::TYPE_PERMISSION,
            '_id' => array_keys($result),
        ]);
        $permissions = [];
        foreach ($query->all($this->mongodb) as $row) {
            $permissions[$row['name']] = $this->populateItem($row);
        }
        return $permissions;
    }
 
    /**
     * Returns the children for every parent.
     * @return array the children list. Each array key is a parent item name,
     * and the corresponding array value is a list of child item names.
     */
    protected function getChildrenList()
    {
        $query = (new Query)->from($this->itemChildCollection);
        $parents = [];
 
        foreach ($query->all($this->mongodb) as $row) {
            $parents[$row['name']] = $row['children'];
        }
 
        return $parents;
    }
 
    /**
     * Recursively finds all children and grand children of the specified item.
     * @param string $name the name of the item whose children are to be looked for.
     * @param array $childrenList the child list built via [[getChildrenList()]]
     * @param array $result the children and grand children (in array keys)
     */
    protected function getChildrenRecursive($name, $childrenList, &$result)
    {
        if (isset($childrenList[$name])) {
            foreach ($childrenList[$name] as $child) {
                $result[$child] = true;
                $this->getChildrenRecursive($child, $childrenList, $result);
            }
        }
    }
 
 
 
    /**
     * Adds an item as a child of another item.
     * @url http://docs.mongodb.org/manual/tutorial/model-tree-structures-with-child-references/
     *
     * @param Item $parent
     * @param Item $child
     * @throws \yii\base\InvalidParamException
     * @throws \yii\base\InvalidCallException
     * @return bool
     */
    public function addChild($parent, $child)
    {
        if ($parent->name === $child->name) {
            throw new InvalidParamException("Cannot add '{$parent->name}' as a child of itself.");
        }
 
        if ($parent instanceof Permission && $child instanceof Role) {
            throw new InvalidParamException("Cannot add a role as a child of a permission.");
        }
 
        if ($this->detectLoop($parent, $child)) {
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
        }
 
        $this->mongodb->getCollection($this->itemChildCollection)->update(['_id'=>$parent->name],[
            'name'=>$parent->name,
        ],['upsert'=>true]);
 
        $this->mongodb->getCollection($this->itemChildCollection)->update(['_id'=>$parent->name],[
            '$addToSet'=>[
                'children'=>$child->name
            ]
        ],['upsert'=>false]);
 
        return true;
    }
 
    /**
     * Removes a child from its parent.
     * Note, the child item is not deleted. Only the parent-child relationship is removed.
     *
     * @param Item $parent
     * @param Item $child
     * @return boolean whether the removal is successful
     */
    public function removeChild($parent, $child)
    {
        $this->mongodb->getCollection($this->itemChildCollection)->update(['_id'=>$parent->name],[
            '$pull'=>[
                'children'=>$child->name
            ]
        ]);
 
        return true;
    }
 
    /**
     * Returns a value indicating whether the child already exists for the parent.
     *
     * @param Item $parent
     * @param Item $child
     * @return boolean whether `$child` is already a child of `$parent`
     */
    public function hasChild($parent, $child)
    {
        return (new Query)
            ->from($this->itemChildCollection)
            ->where([
                '_id' => $parent->name,
                'children' => ['$in'=>[$child->name]]
            ])
            ->one($this->mongodb) !== false;
 
    }
 
    /**
     * Returns the child permissions and/or roles.
     *
     * @param string $name the parent name
     * @return Item[] the child permissions and/or roles
     */
    public function getChildren($name)
    {
        $query = (new Query)
            ->from($this->itemChildCollection)
            ->where([
                '_id' => $name,
            ]);
 
        $result = $query->one();
 
        $children = [];
 
        if($result !== false && !empty($result)){
            foreach ($result['children'] as $child) {
                $children[$child] = $this->getItem($child);
            }
        }
 
        return $children;
    }
 
    /**
     * Assigns a role to a user.
     *
     * @param Role $role
     * @param string $userId the user ID (see [[\yii\web\User::id]])
     * @return Assignment the role assignment information.
     * @throws \Exception if the role has already been assigned to the user
     */
    public function assign($role, $userId)
    {
        if($userId instanceof \MongoId){
            $userId = (string)$userId;
        }
 
        $assignment = new Assignment([
            'userId' => $userId,
            'roleName' => $role->name,
            'createdAt' => time(),
        ]);
 
        $this->mongodb->getCollection($this->assignmentCollection)->update([
            '_id'=>"{$assignment->userId}-$assignment->roleName"
        ],[
            '$setOnInsert'=>[
                'user_id' => $assignment->userId,
                'item_name' => $assignment->roleName,
                'created_at' => $assignment->createdAt,
            ]
        ],['upsert'=>true]);
 
        return $assignment;
    }
 
    /**
     * Revokes a role from a user.
     *
     * @param Role $role
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return boolean whether the revoking is successful
     */
    public function revoke($role, $userId)
    {
        if($userId instanceof \MongoId){
            $userId = (string)$userId;
        }
 
        return $this->mongodb->getCollection($this->assignmentCollection)->remove([
            '_id'=>"{$userId}-{$role->name}"
        ])!==false;
    }
 
    /**
     * Revokes all roles from a user.
     *
     * @param mixed $userId the user ID (see [[\yii\web\User::id]])
     * @return boolean whether the revoking is successful
     */
    public function revokeAll($userId)
    {
        if($userId instanceof \MongoId){
            $userId = (string)$userId;
        }
 
        return $this->mongodb->getCollection($this->assignmentCollection)->remove([
            'user_id'=>$userId
        ],['justOne'=>false])!==false;
    }
 
    /**
     * Returns the assignment information regarding a role and a user.
     *
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @param string $roleName the role name
     * @return Assignment the assignment information. Null is returned if
     * the role is not assigned to the user.
     */
    public function getAssignment($roleName, $userId)
    {
        $row = (new Query)->from($this->assignmentCollection)
                          ->where(['_id'=>"{$userId}-$roleName"])
                          ->one($this->mongodb);
 
        if ($row === false) {
            return null;
        }
 
        return new Assignment([
            'userId' => $row['user_id'],
            'roleName' => $row['item_name'],
            'createdAt' => $row['created_at'],
        ]);
    }
 
    /**
     * Returns all role assignment information for the specified user.
     *
     * @param string|integer $userId the user ID (see [[\yii\web\User::id]])
     * @return Assignment[] the assignments indexed by role names. An empty array will be
     * returned if there is no role assigned to the user.
     */
    public function getAssignments($userId)
    {
        if($userId instanceof \MongoId){
            $userId = (string)$userId;
        }
 
        $query = (new Query)
            ->from($this->assignmentCollection)
            ->where(['user_id' => (string)$userId]);
 
        $assignments = [];
        foreach ($query->all($this->mongodb) as $row) {
            $assignments[$row['item_name']] = new Assignment([
                'userId' => $row['user_id'],
                'roleName' => $row['item_name'],
                'createdAt' => $row['created_at'],
            ]);
        }
 
        return $assignments;
    }
 
    /**
     * Removes all authorization data, including roles, permissions, rules, and assignments.
     */
    public function removeAll()
    {
        $this->removeAllAssignments();
        $this->mongodb->getCollection($this->itemCollection)->remove([],['justOne'=>false]);
        $this->mongodb->getCollection($this->itemChildCollection)->remove([],['justOne'=>false]);
        $this->mongodb->getCollection($this->ruleCollection)->remove([],['justOne'=>false]);
 
    }
 
    /**
     * Removes all permissions.
     * All parent child relations will be adjusted accordingly.
     */
    public function removeAllPermissions()
    {
        $type = Item::TYPE_PERMISSION;
        $names = $this->getAllItemsName($type);
        $this->mongodb->getCollection($this->itemChildCollection)->update([
            'children'=>[
                '$in'=>$names
            ]
        ],[
            '$pullAll'=>[
                'children'=>$names
            ]
        ],['multi'=>true]);
 
        $this->removeAllItems($type,$names);
    }
 
    /**
     * Removes all roles.
     * All parent child relations will be adjusted accordingly.
     */
    public function removeAllRoles()
    {
        $type = Item::TYPE_ROLE;
        $names = $this->getAllItemsName($type);
 
        $this->mongodb->getCollection($this->itemChildCollection)->update([
            'children'=>[
                '$in'=>$names
            ]
        ],[
            '$pullAll'=>[
                'children'=>$names
            ]
        ],['multi'=>true]);
 
        $this->mongodb->getCollection($this->itemChildCollection)->remove([
            'name'=>[
                '$in'=>$names
            ]
        ],['justOne'=>false]);
 
        $this->removeAllItems($type,$names);
    }
 
    protected function getAllItemsName($type)
    {
        $items = (new Query)
            ->select(['name'])
            ->from($this->itemCollection)
            ->where(['type' => $type])
            ->all($this->mongodb);
        $names = [];
        foreach($items as $item){
            $names[]=$item['name'];
        }
        return $names;
    }
 
    /**
     * Removes all auth items of the specified type.
     *
     * @param integer $type the auth item type (either Item::TYPE_PERMISSION or Item::TYPE_ROLE)
     * @param array $names
     */
    protected function removeAllItems($type, array $names)
    {
        $this->mongodb->getCollection($this->assignmentCollection)->remove([
            'item_name'=>[
                '$in'=>$names
            ]
        ],['multi'=>true]);
 
        $this->mongodb->getCollection($this->itemCollection)->remove(['type'=>$type],['multi'=>true]);
 
    }
 
    /**
     * Removes all rules.
     * All roles and permissions which have rules will be adjusted accordingly.
     */
    public function removeAllRules()
    {
        $this->mongodb->getCollection($this->itemCollection)->update([],['ruleName'=>null],['multi'=>true]);
        $this->mongodb->getCollection($this->ruleCollection)->remove([],['justOne'=>false]);
    }
 
    /**
     * Removes all role assignments.
     */
    public function removeAllAssignments()
    {
        $this->mongodb->getCollection($this->assignmentCollection)->remove([],['justOne'=>false]);
    }
 
    /**
     * Populates an auth item with the data fetched from database
     * @param array $row the data from the auth item table
     * @return Item the populated auth item instance (either Role or Permission)
     */
    protected function populateItem($row)
    {
        $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className();
 
        if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) {
            $data = null;
        }
 
        return new $class([
            'name' => $row['name'],
            'type' => $row['type'],
            'description' => $row['description'],
            'ruleName' => $row['rule_name'],
            'data' => $data,
            'createdAt' => $row['created_at'],
            'updatedAt' => $row['updated_at'],
        ]);
    }
 
    /**
     * Checks whether there is a loop in the authorization item hierarchy.
     * @param Item $parent the parent item
     * @param Item $child the child item to be added to the hierarchy
     * @return boolean whether a loop exists
     */
    protected function detectLoop($parent, $child)
    {
        if ($child->name === $parent->name) {
            return true;
        }
 
        foreach ($this->getChildren($child->name) as $grandchild) {
            if ($this->detectLoop($parent, $grandchild)) {
                return true;
            }
        }
        return false;
    }
 
 
}

Viewing all articles
Browse latest Browse all 3361

Trending Articles