MongoDB's RBAC support class extending BaseManager

You are viewing revision #1 of this wiki article.
This is the latest version of this article.

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;
	}


}