From 9158e110dad9a888a9988cfaeca75eb241424ef9 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Thu, 9 May 2013 17:21:18 +0400 Subject: [PATCH 01/19] Auth manager init version --- framework/base/Application.php | 6 +- framework/web/AuthAssignment.php | 97 +++++++++++++++ framework/web/AuthItem.php | 261 +++++++++++++++++++++++++++++++++++++++ framework/web/AuthManager.php | 143 +++++++++++++++++++++ framework/web/IAuthManager.php | 173 ++++++++++++++++++++++++++ framework/web/User.php | 40 ++++-- 6 files changed, 705 insertions(+), 15 deletions(-) create mode 100644 framework/web/AuthAssignment.php create mode 100644 framework/web/AuthItem.php create mode 100644 framework/web/AuthManager.php create mode 100644 framework/web/IAuthManager.php diff --git a/framework/base/Application.php b/framework/base/Application.php index ac7cc6a..6bd31f1 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -305,12 +305,12 @@ class Application extends Module } /** - * @return null|Component - * @todo + * Returns the auth manager for this application. + * @return \yii\web\AuthManager the auth manager for this application. */ public function getAuthManager() { - return $this->getComponent('auth'); + return $this->getComponent('authManager'); } /** diff --git a/framework/web/AuthAssignment.php b/framework/web/AuthAssignment.php new file mode 100644 index 0000000..c08e12d --- /dev/null +++ b/framework/web/AuthAssignment.php @@ -0,0 +1,97 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class AuthAssignment extends Object +{ + private $_auth; + private $_itemName; + private $_userId; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param IAuthManager $auth the authorization manager + * @param string $itemName authorization item name + * @param mixed $userId user ID (see [[User::id]]) + * @param string $bizRule the business rule associated with this assignment + * @param mixed $data additional data for this assignment + */ + public function __construct($auth, $itemName, $userId, $bizRule = null, $data = null) + { + $this->_auth = $auth; + $this->_itemName = $itemName; + $this->_userId = $userId; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * @return mixed user ID (see [[User::id]]) + */ + public function getUserId() + { + return $this->_userId; + } + + /** + * @return string the authorization item name + */ + public function getItemName() + { + return $this->_itemName; + } + + /** + * @return string the business rule associated with this assignment + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this assignment + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveAuthAssignment($this); + } + } + + /** + * @return mixed additional data for this assignment + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value additional data for this assignment + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveAuthAssignment($this); + } + } +} diff --git a/framework/web/AuthItem.php b/framework/web/AuthItem.php new file mode 100644 index 0000000..0d3553d --- /dev/null +++ b/framework/web/AuthItem.php @@ -0,0 +1,261 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class AuthItem extends Object +{ + const TYPE_OPERATION = 0; + const TYPE_TASK = 1; + const TYPE_ROLE = 2; + + private $_auth; + private $_type; + private $_name; + private $_description; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param IAuthManager $auth authorization manager + * @param string $name authorization item name + * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). + * @param string $description the description + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data for this item + */ + public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) + { + $this->_type = (int)$type; + $this->_auth = $auth; + $this->_name = $name; + $this->_description = $description; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * Checks to see if the specified item is within the hierarchy starting from this item. + * This method is expected to be internally used by the actual implementations + * of the [[IAuthManager::checkAccess()]]. + * @param string $itemName the name of the item to be checked + * @param array $params the parameters to be passed to business rule evaluation + * @return boolean whether the specified item is within the hierarchy starting from this item. + */ + public function checkAccess($itemName, $params = array()) + { + Yii::trace('Checking permission: ' . $this->_name, __METHOD__); + if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { + if ($this->_name == $itemName) { + return true; + } + foreach ($this->_auth->getItemChildren($this->_name) as $item) { + if ($item->checkAccess($itemName, $params)) { + return true; + } + } + } + return false; + } + + /** + * @return IAuthManager the authorization manager + */ + public function getAuthManager() + { + return $this->_auth; + } + + /** + * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + */ + public function getType() + { + return $this->_type; + } + + /** + * @return string the item name + */ + public function getName() + { + return $this->_name; + } + + /** + * @param string $value the item name + */ + public function setName($value) + { + if ($this->_name !== $value) { + $oldName = $this->_name; + $this->_name = $value; + $this->_auth->saveAuthItem($this, $oldName); + } + } + + /** + * @return string the item description + */ + public function getDescription() + { + return $this->_description; + } + + /** + * @param string $value the item description + */ + public function setDescription($value) + { + if ($this->_description !== $value) { + $this->_description = $value; + $this->_auth->saveAuthItem($this); + } + } + + /** + * @return string the business rule associated with this item + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this item + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveAuthItem($this); + } + } + + /** + * @return mixed the additional data associated with this item + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value the additional data associated with this item + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveAuthItem($this); + } + } + + /** + * Adds a child item. + * @param string $name the name of the child item + * @return boolean whether the item is added successfully + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + * @see IAuthManager::addItemChild + */ + public function addChild($name) + { + return $this->_auth->addItemChild($this->_name, $name); + } + + /** + * Removes a child item. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $name the child item name + * @return boolean whether the removal is successful + * @see IAuthManager::removeItemChild + */ + public function removeChild($name) + { + return $this->_auth->removeItemChild($this->_name, $name); + } + + /** + * Returns a value indicating whether a child exists + * @param string $name the child item name + * @return boolean whether the child exists + * @see IAuthManager::hasItemChild + */ + public function hasChild($name) + { + return $this->_auth->hasItemChild($this->_name, $name); + } + + /** + * Returns the children of this item. + * @return array all child items of this item. + * @see IAuthManager::getItemChildren + */ + public function getChildren() + { + return $this->_auth->getItemChildren($this->_name); + } + + /** + * Assigns this item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return AuthAssignment the authorization assignment information. + * @throws \yii\base\Exception if the item has already been assigned to the user + * @see IAuthManager::assign + */ + public function assign($userId, $bizRule = null, $data = null) + { + return $this->_auth->assign($this->_name, $userId, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether removal is successful + * @see IAuthManager::revoke + */ + public function revoke($userId) + { + return $this->_auth->revoke($this->_name, $userId); + } + + /** + * Returns a value indicating whether this item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether the item has been assigned to the user. + * @see IAuthManager::isAssigned + */ + public function isAssigned($userId) + { + return $this->_auth->isAssigned($this->_name, $userId); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @return AuthAssignment the item assignment information. Null is returned if + * this item is not assigned to the user. + * @see IAuthManager::getAuthAssignment + */ + public function getAssignment($userId) + { + return $this->_auth->getAuthAssignment($this->_name, $userId); + } +} diff --git a/framework/web/AuthManager.php b/framework/web/AuthManager.php new file mode 100644 index 0000000..4888689 --- /dev/null +++ b/framework/web/AuthManager.php @@ -0,0 +1,143 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +abstract class AuthManager extends Component implements IAuthManager +{ + /** + * @var boolean Enable error reporting for bizRules. + */ + public $showErrors = false; + + /** + * @var array list of role names that are assigned to all users implicitly. + * These roles do not need to be explicitly assigned to any user. + * When calling [[checkAccess()]], these roles will be checked first. + * For performance reason, you should minimize the number of such roles. + * A typical usage of such roles is to define an 'authenticated' role and associate + * it with a biz rule which checks if the current user is authenticated. + * And then declare 'authenticated' in this property so that it can be applied to + * every authenticated user. + */ + public $defaultRoles = array(); + + /** + * Creates a role. + * This is a shortcut method to [[IAuthManager::createAuthItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return AuthItem the authorization item + */ + public function createRole($name, $description = '', $bizRule = null, $data = null) + { + return $this->createAuthItem($name, AuthItem::TYPE_ROLE, $description, $bizRule, $data); + } + + /** + * Creates a task. + * This is a shortcut method to [[IAuthManager::createAuthItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return AuthItem the authorization item + */ + public function createTask($name, $description = '', $bizRule = null, $data = null) + { + return $this->createAuthItem($name, AuthItem::TYPE_TASK, $description, $bizRule, $data); + } + + /** + * Creates an operation. + * This is a shortcut method to [[IAuthManager::createAuthItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return AuthItem the authorization item + */ + public function createOperation($name, $description = '', $bizRule = null, $data = null) + { + return $this->createAuthItem($name, AuthItem::TYPE_OPERATION, $description, $bizRule, $data); + } + + /** + * Returns roles. + * This is a shortcut method to [[IAuthManager::getAuthItems()]]. + * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user + * will be returned. Otherwise, all roles will be returned. + * @return array roles (name=>AuthItem) + */ + public function getRoles($userId = null) + { + return $this->getAuthItems(AuthItem::TYPE_ROLE, $userId); + } + + /** + * Returns tasks. + * This is a shortcut method to [[IAuthManager::getAuthItems()]]. + * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user + * will be returned. Otherwise, all tasks will be returned. + * @return array tasks (name=>AuthItem) + */ + public function getTasks($userId = null) + { + return $this->getAuthItems(AuthItem::TYPE_TASK, $userId); + } + + /** + * Returns operations. + * This is a shortcut method to [[IAuthManager::getAuthItems()]]. + * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user + * will be returned. Otherwise, all operations will be returned. + * @return array operations (name=>AuthItem) + */ + public function getOperations($userId = null) + { + return $this->getAuthItems(AuthItem::TYPE_OPERATION, $userId); + } + + /** + * Executes the specified business rule. + * @param string $bizRule the business rule to be executed. + * @param array $params parameters passed to [[IAuthManager::checkAccess()]]. + * @param mixed $data additional data associated with the authorization item or assignment. + * @return boolean whether the business rule returns true. + * If the business rule is empty, it will still return true. + */ + public function executeBizRule($bizRule, $params, $data) + { + return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); + } + + /** + * Checks the item types to make sure a child can be added to a parent. + * @param integer $parentType parent item type + * @param integer $childType child item type + * @throws Exception if the item cannot be added as a child due to its incompatible type. + */ + protected function checkItemChildType($parentType, $childType) + { + static $types = array('operation', 'task', 'role'); + if ($parentType < $childType) { + throw new Exception("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); + } + } +} diff --git a/framework/web/IAuthManager.php b/framework/web/IAuthManager.php new file mode 100644 index 0000000..1d2fbb9 --- /dev/null +++ b/framework/web/IAuthManager.php @@ -0,0 +1,173 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +interface IAuthManager +{ + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()); + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return AuthItem the authorization item + */ + public function createAuthItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeAuthItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return array the authorization items of the specific type. + */ + public function getAuthItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return AuthItem the authorization item. Null if the item cannot be found. + */ + public function getAuthItem($name); + /** + * Saves an authorization item to persistent storage. + * @param AuthItem $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveAuthItem($item, $oldName = null); + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return array all child items of the parent + */ + public function getItemChildren($itemName); + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return AuthAssignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return AuthAssignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAuthAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return array the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAuthAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param AuthAssignment $assignment the assignment that has been changed. + */ + public function saveAuthAssignment($assignment); + /** + * Removes all authorization data. + */ + public function clearAll(); + /** + * Removes all authorization assignments. + */ + public function clearAuthAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save(); + /** + * Executes a business rule. + * A business rule is a piece of PHP code that will be executed when [[checkAccess()]] is called. + * @param string $bizRule the business rule to be executed. + * @param array $params additional parameters to be passed to the business rule when being executed. + * @param mixed $data additional data that is associated with the corresponding authorization item or assignment + * @return boolean whether the execution returns a true value. + * If the business rule is empty, it will also return true. + */ + public function executeBizRule($bizRule, $params, $data); +} diff --git a/framework/web/User.php b/framework/web/User.php index 88fc12d..7d8e300 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -40,11 +40,11 @@ class User extends Component */ public $enableAutoLogin = false; /** - * @var string|array the URL for login when [[loginRequired()]] is called. + * @var string|array the URL for login when [[loginRequired()]] is called. * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. - * The first element of the array should be the route to the login action, and the rest of + * The first element of the array should be the route to the login action, and the rest of * the name-value pairs are GET parameters used to construct the login URL. For example, - * + * * ~~~ * array('site/login', 'ref' => 1) * ~~~ @@ -86,6 +86,8 @@ class User extends Component */ public $returnUrlVar = '__returnUrl'; + private $_access = array(); + /** * Initializes the application component. @@ -449,19 +451,33 @@ class User extends Component } /** - * Checks whether the user has access to the specified operation. - * @param $operator - * @param array $params - * @return bool - * @todo + * Performs access check for this user. + * @param string $operation the name of the operation that need access check. + * @param array $params name-value pairs that would be passed to business rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of [[id]] when [[DbAuthManager]] or + * [[PhpAuthManager]] is used. + * @param boolean $allowCaching whether to allow caching the result of access check. + * When this parameter is true (default), if the access check of an operation was performed + * before, its result will be directly returned when calling this method to check the same + * operation. If this parameter is false, this method will always call + * [[AuthManager::checkAccess()]] to obtain the up-to-date access result. Note that this + * caching is effective only within the same request and only works when `$params = array()`. + * @return boolean whether the operations can be performed by this user. */ - public function checkAccess($operation, $params = array()) + public function checkAccess($operation, $params = array(), $allowCaching = true) { $auth = Yii::$app->getAuthManager(); - if ($auth !== null) { - return $auth->checkAccess($this->getId(), $operation, $params); - } else { + if ($auth === null) { return false; } + if ($allowCaching && empty($params) && isset($this->_access[$operation])) { + return $this->_access[$operation]; + } + $access = $auth->checkAccess($this->getId(), $operation, $params); + if ($allowCaching && empty($params)) { + $this->_access[$operation] = $access; + } + return $access; } } From cd3387067155f34df056629c233fa895dfb1ba83 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Thu, 9 May 2013 18:53:17 +0400 Subject: [PATCH 02/19] Moved to framework/rbac + method names refactoring --- framework/rbac/Assignment.php | 97 ++++++++ framework/rbac/Item.php | 261 +++++++++++++++++++++ framework/rbac/Manager.php | 143 ++++++++++++ framework/rbac/PhpManager.php | 492 +++++++++++++++++++++++++++++++++++++++ framework/web/AuthAssignment.php | 97 -------- framework/web/AuthItem.php | 261 --------------------- framework/web/AuthManager.php | 143 ------------ framework/web/IAuthManager.php | 173 -------------- 8 files changed, 993 insertions(+), 674 deletions(-) create mode 100644 framework/rbac/Assignment.php create mode 100644 framework/rbac/Item.php create mode 100644 framework/rbac/Manager.php create mode 100644 framework/rbac/PhpManager.php delete mode 100644 framework/web/AuthAssignment.php delete mode 100644 framework/web/AuthItem.php delete mode 100644 framework/web/AuthManager.php delete mode 100644 framework/web/IAuthManager.php diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php new file mode 100644 index 0000000..c2a9f56 --- /dev/null +++ b/framework/rbac/Assignment.php @@ -0,0 +1,97 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class Assignment extends Object +{ + private $_auth; + private $_itemName; + private $_userId; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param IManager $auth the authorization manager + * @param string $itemName authorization item name + * @param mixed $userId user ID (see [[User::id]]) + * @param string $bizRule the business rule associated with this assignment + * @param mixed $data additional data for this assignment + */ + public function __construct($auth, $itemName, $userId, $bizRule = null, $data = null) + { + $this->_auth = $auth; + $this->_itemName = $itemName; + $this->_userId = $userId; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * @return mixed user ID (see [[User::id]]) + */ + public function getUserId() + { + return $this->_userId; + } + + /** + * @return string the authorization item name + */ + public function getItemName() + { + return $this->_itemName; + } + + /** + * @return string the business rule associated with this assignment + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this assignment + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveAssignment($this); + } + } + + /** + * @return mixed additional data for this assignment + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value additional data for this assignment + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveAssignment($this); + } + } +} diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php new file mode 100644 index 0000000..ba78e60 --- /dev/null +++ b/framework/rbac/Item.php @@ -0,0 +1,261 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class Item extends Object +{ + const TYPE_OPERATION = 0; + const TYPE_TASK = 1; + const TYPE_ROLE = 2; + + private $_auth; + private $_type; + private $_name; + private $_description; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param IManager $auth authorization manager + * @param string $name authorization item name + * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). + * @param string $description the description + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data for this item + */ + public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) + { + $this->_type = (int)$type; + $this->_auth = $auth; + $this->_name = $name; + $this->_description = $description; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * Checks to see if the specified item is within the hierarchy starting from this item. + * This method is expected to be internally used by the actual implementations + * of the [[IManager::checkAccess()]]. + * @param string $itemName the name of the item to be checked + * @param array $params the parameters to be passed to business rule evaluation + * @return boolean whether the specified item is within the hierarchy starting from this item. + */ + public function checkAccess($itemName, $params = array()) + { + Yii::trace('Checking permission: ' . $this->_name, __METHOD__); + if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { + if ($this->_name == $itemName) { + return true; + } + foreach ($this->_auth->getItemChildren($this->_name) as $item) { + if ($item->checkAccess($itemName, $params)) { + return true; + } + } + } + return false; + } + + /** + * @return IManager the authorization manager + */ + public function getManager() + { + return $this->_auth; + } + + /** + * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + */ + public function getType() + { + return $this->_type; + } + + /** + * @return string the item name + */ + public function getName() + { + return $this->_name; + } + + /** + * @param string $value the item name + */ + public function setName($value) + { + if ($this->_name !== $value) { + $oldName = $this->_name; + $this->_name = $value; + $this->_auth->saveItem($this, $oldName); + } + } + + /** + * @return string the item description + */ + public function getDescription() + { + return $this->_description; + } + + /** + * @param string $value the item description + */ + public function setDescription($value) + { + if ($this->_description !== $value) { + $this->_description = $value; + $this->_auth->saveItem($this); + } + } + + /** + * @return string the business rule associated with this item + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this item + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveItem($this); + } + } + + /** + * @return mixed the additional data associated with this item + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value the additional data associated with this item + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveItem($this); + } + } + + /** + * Adds a child item. + * @param string $name the name of the child item + * @return boolean whether the item is added successfully + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + * @see IManager::addItemChild + */ + public function addChild($name) + { + return $this->_auth->addItemChild($this->_name, $name); + } + + /** + * Removes a child item. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $name the child item name + * @return boolean whether the removal is successful + * @see IManager::removeItemChild + */ + public function removeChild($name) + { + return $this->_auth->removeItemChild($this->_name, $name); + } + + /** + * Returns a value indicating whether a child exists + * @param string $name the child item name + * @return boolean whether the child exists + * @see IManager::hasItemChild + */ + public function hasChild($name) + { + return $this->_auth->hasItemChild($this->_name, $name); + } + + /** + * Returns the children of this item. + * @return array all child items of this item. + * @see IManager::getItemChildren + */ + public function getChildren() + { + return $this->_auth->getItemChildren($this->_name); + } + + /** + * Assigns this item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item has already been assigned to the user + * @see IManager::assign + */ + public function assign($userId, $bizRule = null, $data = null) + { + return $this->_auth->assign($userId, $this->_name, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether removal is successful + * @see IManager::revoke + */ + public function revoke($userId) + { + return $this->_auth->revoke($userId, $this->_name); + } + + /** + * Returns a value indicating whether this item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether the item has been assigned to the user. + * @see IManager::isAssigned + */ + public function isAssigned($userId) + { + return $this->_auth->isAssigned($userId, $this->_name); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment the item assignment information. Null is returned if + * this item is not assigned to the user. + * @see IManager::getAssignment + */ + public function getAssignment($userId) + { + return $this->_auth->getAssignment($userId, $this->_name); + } +} diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php new file mode 100644 index 0000000..2f77e10 --- /dev/null +++ b/framework/rbac/Manager.php @@ -0,0 +1,143 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +abstract class Manager extends Component implements IManager +{ + /** + * @var boolean Enable error reporting for bizRules. + */ + public $showErrors = false; + + /** + * @var array list of role names that are assigned to all users implicitly. + * These roles do not need to be explicitly assigned to any user. + * When calling [[checkAccess()]], these roles will be checked first. + * For performance reason, you should minimize the number of such roles. + * A typical usage of such roles is to define an 'authenticated' role and associate + * it with a biz rule which checks if the current user is authenticated. + * And then declare 'authenticated' in this property so that it can be applied to + * every authenticated user. + */ + public $defaultRoles = array(); + + /** + * Creates a role. + * This is a shortcut method to [[IManager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createRole($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); + } + + /** + * Creates a task. + * This is a shortcut method to [[IManager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createTask($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); + } + + /** + * Creates an operation. + * This is a shortcut method to [[IManager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createOperation($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); + } + + /** + * Returns roles. + * This is a shortcut method to [[IManager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user + * will be returned. Otherwise, all roles will be returned. + * @return array roles (name=>AuthItem) + */ + public function getRoles($userId = null) + { + return $this->getItems($userId, Item::TYPE_ROLE); + } + + /** + * Returns tasks. + * This is a shortcut method to [[IManager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user + * will be returned. Otherwise, all tasks will be returned. + * @return array tasks (name=>AuthItem) + */ + public function getTasks($userId = null) + { + return $this->getItems($userId, Item::TYPE_TASK); + } + + /** + * Returns operations. + * This is a shortcut method to [[IManager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user + * will be returned. Otherwise, all operations will be returned. + * @return array operations (name=>AuthItem) + */ + public function getOperations($userId = null) + { + return $this->getItems($userId, Item::TYPE_OPERATION); + } + + /** + * Executes the specified business rule. + * @param string $bizRule the business rule to be executed. + * @param array $params parameters passed to [[IManager::checkAccess()]]. + * @param mixed $data additional data associated with the authorization item or assignment. + * @return boolean whether the business rule returns true. + * If the business rule is empty, it will still return true. + */ + public function executeBizRule($bizRule, $params, $data) + { + return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); + } + + /** + * Checks the item types to make sure a child can be added to a parent. + * @param integer $parentType parent item type + * @param integer $childType child item type + * @throws Exception if the item cannot be added as a child due to its incompatible type. + */ + protected function checkItemChildType($parentType, $childType) + { + static $types = array('operation', 'task', 'role'); + if ($parentType < $childType) { + throw new Exception("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); + } + } +} diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php new file mode 100644 index 0000000..08f5c83 --- /dev/null +++ b/framework/rbac/PhpManager.php @@ -0,0 +1,492 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class PhpManager extends Manager +{ + /** + * @var string the path of the PHP script that contains the authorization data. + * If not set, it will be using 'protected/data/auth.php' as the data file. + * Make sure this file is writable by the Web server process if the authorization + * needs to be changed. + * @see loadFromFile + * @see saveToFile + */ + public $authFile; + + private $_items = array(); // itemName => item + private $_children = array(); // itemName, childName => child + private $_assignments = array(); // userId, itemName => assignment + + /** + * Initializes the application component. + * This method overrides parent implementation by loading the authorization data + * from PHP script. + */ + public function init() + { + parent::init(); + if ($this->authFile === null) { + $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; + } + $this->load(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This can be either an integer or a string representing + * @param string $itemName the name of the operation that need access check + * the unique identifier of a user. See [[User::id]]. + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * Since version 1.1.11 a param with name 'userId' is added to this array, which holds the value of $userId. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + if (!isset($this->_items[$itemName])) { + return false; + } + $item = $this->_items[$itemName]; + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($this->_assignments[$userId][$itemName])) { + $assignment = $this->_assignments[$userId][$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + foreach ($this->_children as $parentName => $children) { + if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist or if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if (!isset($this->_items[$childName], $this->_items[$itemName])) { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + $child = $this->_items[$childName]; + $item = $this->_items[$itemName]; + $this->checkItemChildType($item->getType(), $child->getType()); + if ($this->detectLoop($itemName, $childName)) { + throw new Exception("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + if (isset($this->_children[$itemName][$childName])) { + throw new Exception("The item '$itemName' already has a child '$childName'."); + } + $this->_children[$itemName][$childName] = $this->_items[$childName]; + return true; + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + if (isset($this->_children[$itemName][$childName])) { + unset($this->_children[$itemName][$childName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + return isset($this->_children[$itemName][$childName]); + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return array all child items of the parent + */ + public function getItemChildren($names) + { + if (is_string($names)) { + return isset($this->_children[$names]) ? $this->_children[$names] : array(); + } + + $children = array(); + foreach ($names as $name) { + if (isset($this->_children[$name])) { + $children = array_merge($children, $this->_children[$name]); + } + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws Exception if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if (!isset($this->_items[$itemName])) { + throw new Exception("Unknown authorization item '$itemName'."); + } elseif (isset($this->_assignments[$userId][$itemName])) { + throw new Exception("Authorization item '$itemName' has already been assigned to user '$userId'."); + } else { + return $this->_assignments[$userId][$itemName] = new Assignment($this, $itemName, $userId, $bizRule, $data); + } + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + if (isset($this->_assignments[$userId][$itemName])) { + unset($this->_assignments[$userId][$itemName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return array the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : array(); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return array the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + if ($type === null && $userId === null) { + return $this->_items; + } + $items = array(); + if ($userId === null) { + foreach ($this->_items as $name => $item) { + if ($item->getType() == $type) { + $items[$name] = $item; + } + } + } elseif (isset($this->_assignments[$userId])) { + foreach ($this->_assignments[$userId] as $assignment) { + $name = $assignment->getItemName(); + if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->getType() == $type)) { + $items[$name] = $this->_items[$name]; + } + } + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + if (isset($this->_items[$name])) { + throw new Exception('Unable to add an item whose name is the same as an existing item.'); + } + return $this->_items[$name] = new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if (isset($this->_items[$name])) { + foreach ($this->_children as &$children) { + unset($children[$name]); + } + foreach ($this->_assignments as &$assignments) { + unset($assignments[$name]); + } + unset($this->_items[$name]); + return true; + } else { + return false; + } + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + return isset($this->_items[$name]) ? $this->_items[$name] : null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + * @throws Exception if an item with the same name already taken + */ + public function saveItem($item, $oldName = null) + { + if ($oldName !== null && ($newName = $item->getName()) !== $oldName) // name changed + { + if (isset($this->_items[$newName])) { + throw new Exception("Unable to change the item name. The name '$newName' is already used by another item."); + } + if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { + unset($this->_items[$oldName]); + $this->_items[$newName] = $item; + if (isset($this->_children[$oldName])) { + $this->_children[$newName] = $this->_children[$oldName]; + unset($this->_children[$oldName]); + } + foreach ($this->_children as &$children) { + if (isset($children[$oldName])) { + $children[$newName] = $children[$oldName]; + unset($children[$oldName]); + } + } + foreach ($this->_assignments as &$assignments) { + if (isset($assignments[$oldName])) { + $assignments[$newName] = $assignments[$oldName]; + unset($assignments[$oldName]); + } + } + } + } + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + } + + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save() + { + $items = array(); + foreach ($this->_items as $name => $item) { + $items[$name] = array( + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizRule' => $item->getBizRule(), + 'data' => $item->getData(), + ); + if (isset($this->_children[$name])) { + foreach ($this->_children[$name] as $child) { + $items[$name]['children'][] = $child->getName(); + } + } + } + + foreach ($this->_assignments as $userId => $assignments) { + foreach ($assignments as $name => $assignment) { + if (isset($items[$name])) { + $items[$name]['assignments'][$userId] = array( + 'bizRule' => $assignment->getBizRule(), + 'data' => $assignment->getData(), + ); + } + } + } + + $this->saveToFile($items, $this->authFile); + } + + /** + * Loads authorization data. + */ + public function load() + { + $this->clearAll(); + + $items = $this->loadFromFile($this->authFile); + + foreach ($items as $name => $item) { + $this->_items[$name] = new Item($this, $name, $item['type'], $item['description'], $item['bizRule'], $item['data']); + } + + foreach ($items as $name => $item) { + if (isset($item['children'])) { + foreach ($item['children'] as $childName) { + if (isset($this->_items[$childName])) { + $this->_children[$name][$childName] = $this->_items[$childName]; + } + } + } + if (isset($item['assignments'])) { + foreach ($item['assignments'] as $userId => $assignment) { + $this->_assignments[$userId][$name] = new Assignment($this, $name, $userId, $assignment['bizRule'], $assignment['data']); + } + } + } + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->_children = array(); + $this->_items = array(); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->_assignments = array(); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + if (!isset($this->_children[$childName], $this->_items[$itemName])) { + return false; + } + foreach ($this->_children[$childName] as $child) { + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * Loads the authorization data from a PHP script file. + * @param string $file the file path. + * @return array the authorization data + * @see saveToFile + */ + protected function loadFromFile($file) + { + if (is_file($file)) { + return require($file); + } else { + return array(); + } + } + + /** + * Saves the authorization data to a PHP script file. + * @param array $data the authorization data + * @param string $file the file path. + * @see loadFromFile + */ + protected function saveToFile($data, $file) + { + file_put_contents($file, " - * @author Alexander Kochetov - * @since 2.0 - */ -class AuthAssignment extends Object -{ - private $_auth; - private $_itemName; - private $_userId; - private $_bizRule; - private $_data; - - /** - * Constructor. - * @param IAuthManager $auth the authorization manager - * @param string $itemName authorization item name - * @param mixed $userId user ID (see [[User::id]]) - * @param string $bizRule the business rule associated with this assignment - * @param mixed $data additional data for this assignment - */ - public function __construct($auth, $itemName, $userId, $bizRule = null, $data = null) - { - $this->_auth = $auth; - $this->_itemName = $itemName; - $this->_userId = $userId; - $this->_bizRule = $bizRule; - $this->_data = $data; - } - - /** - * @return mixed user ID (see [[User::id]]) - */ - public function getUserId() - { - return $this->_userId; - } - - /** - * @return string the authorization item name - */ - public function getItemName() - { - return $this->_itemName; - } - - /** - * @return string the business rule associated with this assignment - */ - public function getBizRule() - { - return $this->_bizRule; - } - - /** - * @param string $value the business rule associated with this assignment - */ - public function setBizRule($value) - { - if ($this->_bizRule !== $value) { - $this->_bizRule = $value; - $this->_auth->saveAuthAssignment($this); - } - } - - /** - * @return mixed additional data for this assignment - */ - public function getData() - { - return $this->_data; - } - - /** - * @param mixed $value additional data for this assignment - */ - public function setData($value) - { - if ($this->_data !== $value) { - $this->_data = $value; - $this->_auth->saveAuthAssignment($this); - } - } -} diff --git a/framework/web/AuthItem.php b/framework/web/AuthItem.php deleted file mode 100644 index 0d3553d..0000000 --- a/framework/web/AuthItem.php +++ /dev/null @@ -1,261 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -class AuthItem extends Object -{ - const TYPE_OPERATION = 0; - const TYPE_TASK = 1; - const TYPE_ROLE = 2; - - private $_auth; - private $_type; - private $_name; - private $_description; - private $_bizRule; - private $_data; - - /** - * Constructor. - * @param IAuthManager $auth authorization manager - * @param string $name authorization item name - * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). - * @param string $description the description - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data for this item - */ - public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) - { - $this->_type = (int)$type; - $this->_auth = $auth; - $this->_name = $name; - $this->_description = $description; - $this->_bizRule = $bizRule; - $this->_data = $data; - } - - /** - * Checks to see if the specified item is within the hierarchy starting from this item. - * This method is expected to be internally used by the actual implementations - * of the [[IAuthManager::checkAccess()]]. - * @param string $itemName the name of the item to be checked - * @param array $params the parameters to be passed to business rule evaluation - * @return boolean whether the specified item is within the hierarchy starting from this item. - */ - public function checkAccess($itemName, $params = array()) - { - Yii::trace('Checking permission: ' . $this->_name, __METHOD__); - if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { - if ($this->_name == $itemName) { - return true; - } - foreach ($this->_auth->getItemChildren($this->_name) as $item) { - if ($item->checkAccess($itemName, $params)) { - return true; - } - } - } - return false; - } - - /** - * @return IAuthManager the authorization manager - */ - public function getAuthManager() - { - return $this->_auth; - } - - /** - * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). - */ - public function getType() - { - return $this->_type; - } - - /** - * @return string the item name - */ - public function getName() - { - return $this->_name; - } - - /** - * @param string $value the item name - */ - public function setName($value) - { - if ($this->_name !== $value) { - $oldName = $this->_name; - $this->_name = $value; - $this->_auth->saveAuthItem($this, $oldName); - } - } - - /** - * @return string the item description - */ - public function getDescription() - { - return $this->_description; - } - - /** - * @param string $value the item description - */ - public function setDescription($value) - { - if ($this->_description !== $value) { - $this->_description = $value; - $this->_auth->saveAuthItem($this); - } - } - - /** - * @return string the business rule associated with this item - */ - public function getBizRule() - { - return $this->_bizRule; - } - - /** - * @param string $value the business rule associated with this item - */ - public function setBizRule($value) - { - if ($this->_bizRule !== $value) { - $this->_bizRule = $value; - $this->_auth->saveAuthItem($this); - } - } - - /** - * @return mixed the additional data associated with this item - */ - public function getData() - { - return $this->_data; - } - - /** - * @param mixed $value the additional data associated with this item - */ - public function setData($value) - { - if ($this->_data !== $value) { - $this->_data = $value; - $this->_auth->saveAuthItem($this); - } - } - - /** - * Adds a child item. - * @param string $name the name of the child item - * @return boolean whether the item is added successfully - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - * @see IAuthManager::addItemChild - */ - public function addChild($name) - { - return $this->_auth->addItemChild($this->_name, $name); - } - - /** - * Removes a child item. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $name the child item name - * @return boolean whether the removal is successful - * @see IAuthManager::removeItemChild - */ - public function removeChild($name) - { - return $this->_auth->removeItemChild($this->_name, $name); - } - - /** - * Returns a value indicating whether a child exists - * @param string $name the child item name - * @return boolean whether the child exists - * @see IAuthManager::hasItemChild - */ - public function hasChild($name) - { - return $this->_auth->hasItemChild($this->_name, $name); - } - - /** - * Returns the children of this item. - * @return array all child items of this item. - * @see IAuthManager::getItemChildren - */ - public function getChildren() - { - return $this->_auth->getItemChildren($this->_name); - } - - /** - * Assigns this item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return AuthAssignment the authorization assignment information. - * @throws \yii\base\Exception if the item has already been assigned to the user - * @see IAuthManager::assign - */ - public function assign($userId, $bizRule = null, $data = null) - { - return $this->_auth->assign($this->_name, $userId, $bizRule, $data); - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return boolean whether removal is successful - * @see IAuthManager::revoke - */ - public function revoke($userId) - { - return $this->_auth->revoke($this->_name, $userId); - } - - /** - * Returns a value indicating whether this item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return boolean whether the item has been assigned to the user. - * @see IAuthManager::isAssigned - */ - public function isAssigned($userId) - { - return $this->_auth->isAssigned($this->_name, $userId); - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @return AuthAssignment the item assignment information. Null is returned if - * this item is not assigned to the user. - * @see IAuthManager::getAuthAssignment - */ - public function getAssignment($userId) - { - return $this->_auth->getAuthAssignment($this->_name, $userId); - } -} diff --git a/framework/web/AuthManager.php b/framework/web/AuthManager.php deleted file mode 100644 index 4888689..0000000 --- a/framework/web/AuthManager.php +++ /dev/null @@ -1,143 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -abstract class AuthManager extends Component implements IAuthManager -{ - /** - * @var boolean Enable error reporting for bizRules. - */ - public $showErrors = false; - - /** - * @var array list of role names that are assigned to all users implicitly. - * These roles do not need to be explicitly assigned to any user. - * When calling [[checkAccess()]], these roles will be checked first. - * For performance reason, you should minimize the number of such roles. - * A typical usage of such roles is to define an 'authenticated' role and associate - * it with a biz rule which checks if the current user is authenticated. - * And then declare 'authenticated' in this property so that it can be applied to - * every authenticated user. - */ - public $defaultRoles = array(); - - /** - * Creates a role. - * This is a shortcut method to [[IAuthManager::createAuthItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return AuthItem the authorization item - */ - public function createRole($name, $description = '', $bizRule = null, $data = null) - { - return $this->createAuthItem($name, AuthItem::TYPE_ROLE, $description, $bizRule, $data); - } - - /** - * Creates a task. - * This is a shortcut method to [[IAuthManager::createAuthItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return AuthItem the authorization item - */ - public function createTask($name, $description = '', $bizRule = null, $data = null) - { - return $this->createAuthItem($name, AuthItem::TYPE_TASK, $description, $bizRule, $data); - } - - /** - * Creates an operation. - * This is a shortcut method to [[IAuthManager::createAuthItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return AuthItem the authorization item - */ - public function createOperation($name, $description = '', $bizRule = null, $data = null) - { - return $this->createAuthItem($name, AuthItem::TYPE_OPERATION, $description, $bizRule, $data); - } - - /** - * Returns roles. - * This is a shortcut method to [[IAuthManager::getAuthItems()]]. - * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user - * will be returned. Otherwise, all roles will be returned. - * @return array roles (name=>AuthItem) - */ - public function getRoles($userId = null) - { - return $this->getAuthItems(AuthItem::TYPE_ROLE, $userId); - } - - /** - * Returns tasks. - * This is a shortcut method to [[IAuthManager::getAuthItems()]]. - * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user - * will be returned. Otherwise, all tasks will be returned. - * @return array tasks (name=>AuthItem) - */ - public function getTasks($userId = null) - { - return $this->getAuthItems(AuthItem::TYPE_TASK, $userId); - } - - /** - * Returns operations. - * This is a shortcut method to [[IAuthManager::getAuthItems()]]. - * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user - * will be returned. Otherwise, all operations will be returned. - * @return array operations (name=>AuthItem) - */ - public function getOperations($userId = null) - { - return $this->getAuthItems(AuthItem::TYPE_OPERATION, $userId); - } - - /** - * Executes the specified business rule. - * @param string $bizRule the business rule to be executed. - * @param array $params parameters passed to [[IAuthManager::checkAccess()]]. - * @param mixed $data additional data associated with the authorization item or assignment. - * @return boolean whether the business rule returns true. - * If the business rule is empty, it will still return true. - */ - public function executeBizRule($bizRule, $params, $data) - { - return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); - } - - /** - * Checks the item types to make sure a child can be added to a parent. - * @param integer $parentType parent item type - * @param integer $childType child item type - * @throws Exception if the item cannot be added as a child due to its incompatible type. - */ - protected function checkItemChildType($parentType, $childType) - { - static $types = array('operation', 'task', 'role'); - if ($parentType < $childType) { - throw new Exception("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); - } - } -} diff --git a/framework/web/IAuthManager.php b/framework/web/IAuthManager.php deleted file mode 100644 index 1d2fbb9..0000000 --- a/framework/web/IAuthManager.php +++ /dev/null @@ -1,173 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -interface IAuthManager -{ - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This should be either an integer or a string representing - * the unique identifier of a user. See [[User::id]]. - * @param string $itemName the name of the operation that we are checking access to - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. - * @return boolean whether the operations can be performed by the user. - */ - public function checkAccess($userId, $itemName, $params = array()); - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @throws \yii\base\Exception if an item with the same name already exists - * @return AuthItem the authorization item - */ - public function createAuthItem($name, $type, $description = '', $bizRule = null, $data = null); - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - public function removeAuthItem($name); - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return array the authorization items of the specific type. - */ - public function getAuthItems($userId = null, $type = null); - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return AuthItem the authorization item. Null if the item cannot be found. - */ - public function getAuthItem($name); - /** - * Saves an authorization item to persistent storage. - * @param AuthItem $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - */ - public function saveAuthItem($item, $oldName = null); - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - */ - public function addItemChild($itemName, $childName); - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - public function removeItemChild($itemName, $childName); - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - public function hasItemChild($itemName, $childName); - /** - * Returns the children of the specified item. - * @param mixed $itemName the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return array all child items of the parent - */ - public function getItemChildren($itemName); - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return AuthAssignment the authorization assignment information. - * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user - */ - public function assign($userId, $itemName, $bizRule = null, $data = null); - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - public function revoke($userId, $itemName); - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - public function isAssigned($userId, $itemName); - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return AuthAssignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - public function getAuthAssignment($userId, $itemName); - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return array the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - public function getAuthAssignments($userId); - /** - * Saves the changes to an authorization assignment. - * @param AuthAssignment $assignment the assignment that has been changed. - */ - public function saveAuthAssignment($assignment); - /** - * Removes all authorization data. - */ - public function clearAll(); - /** - * Removes all authorization assignments. - */ - public function clearAuthAssignments(); - /** - * Saves authorization data into persistent storage. - * If any change is made to the authorization data, please make - * sure you call this method to save the changed data into persistent storage. - */ - public function save(); - /** - * Executes a business rule. - * A business rule is a piece of PHP code that will be executed when [[checkAccess()]] is called. - * @param string $bizRule the business rule to be executed. - * @param array $params additional parameters to be passed to the business rule when being executed. - * @param mixed $data additional data that is associated with the corresponding authorization item or assignment - * @return boolean whether the execution returns a true value. - * If the business rule is empty, it will also return true. - */ - public function executeBizRule($bizRule, $params, $data); -} From 17906e712c301e252343bfb74cf6a648d955096b Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Thu, 9 May 2013 19:04:50 +0400 Subject: [PATCH 03/19] Application::getAuthManager() comment fix --- framework/base/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/base/Application.php b/framework/base/Application.php index 6bd31f1..2e068ef 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -306,7 +306,7 @@ class Application extends Module /** * Returns the auth manager for this application. - * @return \yii\web\AuthManager the auth manager for this application. + * @return \yii\rbac\Manager the auth manager for this application. */ public function getAuthManager() { From 850ff06560268420a7c14cd1104420fc9bf74477 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Thu, 9 May 2013 22:17:12 +0400 Subject: [PATCH 04/19] DbManager added + other classes comments fixes --- framework/rbac/Assignment.php | 8 +- framework/rbac/DbManager.php | 574 ++++++++++++++++++++++++++++++++++++++++++ framework/rbac/IManager.php | 173 +++++++++++++ framework/rbac/Item.php | 2 +- framework/rbac/Manager.php | 6 +- framework/rbac/PhpManager.php | 24 +- 6 files changed, 772 insertions(+), 15 deletions(-) create mode 100644 framework/rbac/DbManager.php create mode 100644 framework/rbac/IManager.php diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php index c2a9f56..576017e 100644 --- a/framework/rbac/Assignment.php +++ b/framework/rbac/Assignment.php @@ -19,24 +19,24 @@ use yii\base\Object; class Assignment extends Object { private $_auth; - private $_itemName; private $_userId; + private $_itemName; private $_bizRule; private $_data; /** * Constructor. * @param IManager $auth the authorization manager - * @param string $itemName authorization item name * @param mixed $userId user ID (see [[User::id]]) + * @param string $itemName authorization item name * @param string $bizRule the business rule associated with this assignment * @param mixed $data additional data for this assignment */ - public function __construct($auth, $itemName, $userId, $bizRule = null, $data = null) + public function __construct($auth, $userId, $itemName, $bizRule = null, $data = null) { $this->_auth = $auth; - $this->_itemName = $itemName; $this->_userId = $userId; + $this->_itemName = $itemName; $this->_bizRule = $bizRule; $this->_data = $data; } diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php new file mode 100644 index 0000000..5f64694 --- /dev/null +++ b/framework/rbac/DbManager.php @@ -0,0 +1,574 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class DbManager extends Manager +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbManager object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. + */ + public $itemTable = 'tbl_auth_item'; + /** + * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. + */ + public $itemChildTable = 'tbl_auth_item_child'; + /** + * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. + */ + public $assignmentTable = 'tbl_auth_assignment'; + + private $_usingSqlite; + + /** + * Initializes the application component. + * This method overrides the parent implementation by establishing the database connection. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); + } + $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); + parent::init(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should can be either an integer and a string representing + * @param string $itemName the name of the operation that need access check + * the unique identifier of a user. See [[User::id]]. + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + $assignments = $this->getAssignments($userId); + return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); + } + + /** + * Performs access check for the specified user. + * This method is internally called by [[checkAccess()]]. + * @param mixed $userId the user ID. This should can be either an integer and a string representing + * the unique identifier of a user. See [[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 biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' 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($userId, $itemName, $params, $assignments) + { + if (($item = $this->getItem($itemName)) === null) { + return false; + } + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($assignments[$itemName])) { + $assignment = $assignments[$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + $query = new Query; + $parents = $query->select(array('parent')) + ->from($this->itemChildTable) + ->where('child=:name', array(':name' => $itemName)) + ->createCommand($this->db) + ->queryColumn(); + foreach ($parents as $parent) { + if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist or if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if ($itemName === $childName) { + throw new Exception("Cannot add '$itemName' as a child of itself."); + } + $query = new Query; + $rows = $query->from($this->itemTable) + ->where('name=:name1 OR name=:name2', array( + ':name1' => $itemName, + ':name2' => $childName + )) + ->createCommand($this->db) + ->queryAll(); + if (count($rows) == 2) { + if ($rows[0]['name'] === $itemName) { + $parentType = $rows[0]['type']; + $childType = $rows[1]['type']; + } else { + $childType = $rows[0]['type']; + $parentType = $rows[1]['type']; + } + $this->checkItemChildType($parentType, $childType); + if ($this->detectLoop($itemName, $childName)) { + throw new Exception("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + $this->db->createCommand() + ->insert($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName, + )); + return true; + } else { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + return $this->db->createCommand() + ->delete($this->itemChildTable, 'parent=:parent AND child=:child', array( + ':parent' => $itemName, + ':child' => $childName + )) > 0; + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + $query = new Query; + return $query->select(array('parent')) + ->from($this->itemChildTable) + ->where('parent=:parent AND child=:child', array( + ':parent' => $itemName, + ':child' => $childName)) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + if (is_string($names)) { + $condition = 'parent=' . $this->db->quoteValue($names); + } elseif (is_array($names) && !empty($names)) { + foreach ($names as &$name) { + $name = $this->db->quoteValue($name); + } + $condition = 'parent IN (' . implode(', ', $names) . ')'; + } + $query = new Query; + $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) + ->from(array( + $this->itemTable, + $this->itemChildTable + )) + ->where($condition . ' AND name=child') + ->createCommand($this->db) + ->queryAll(); + $children = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $children[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws Exception if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if ($this->usingSqlite() && $this->getItem($itemName) === null) { + throw new Exception("The item '$itemName' does not exist."); + } + $this->db->createCommand() + ->insert($this->assignmentTable, array( + 'userid' => $userId, + 'itemname' => $itemName, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Assignment($this, $userId, $itemName, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, 'itemname=:itemname AND userid=:userid', array( + ':userid' => $userId, + ':itemname' => $itemName + )) > 0; + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($itemName, $userId) + { + $query = new Query; + return $query->select(array('itemname')) + ->from($this->assignmentTable) + ->where('itemname=:itemname AND userid=:userid', array( + ':userid' => $userId, + ':itemname' => $itemName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + $query = new Query; + $row = $query->from($this->assignmentTable) + ->where('itemname=:itemname AND userid=:userid', array( + ':userid' => $userId, + ':itemname' => $itemName + )) + ->createCommand($this->db) + ->queryRow(); + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); + } else { + return null; + } + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + $query = new Query; + $rows = $query->from($this->assignmentTable) + ->where('userid=:userid', array(':userid' => $userId)) + ->createCommand($this->db) + ->queryAll(); + $assignments = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $assignments[$row['itemname']] = new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); + } + return $assignments; + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'bizrule' => $assignment->getBizRule(), + 'data' => serialize($assignment->getData()), + ), 'itemname=:itemname AND userid=:userid', array( + 'userid' => $assignment->getUserId(), + 'itemname' => $assignment->getItemName() + )); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + $query = new Query; + if ($userId === null && $type === null) { + $command = $query->from($this->itemTable) + ->createCommand($this->db); + } elseif ($userId === null) { + $command = $query->from($this->itemTable) + ->where('type=:type', array(':type' => $type)) + ->createCommand($this->db); + } elseif ($type === null) { + $command = $query->select(array('name', 'type', 'description', 't1.bizrule', 't1.data')) + ->from(array( + $this->itemTable . ' t1', + $this->assignmentTable . ' t2' + )) + ->where('name=itemname AND userid=:userid', array(':userid' => $userId)) + ->createCommand($this->db); + } else { + $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') + ->from(array( + $this->itemTable . ' t1', + $this->assignmentTable . ' t2' + )) + ->where('name=itemname AND type=:type AND userid=:userid', array( + ':userid' => $userId, + ':type' => $type + )) + ->createCommand($this->db); + } + $items = array(); + foreach ($command->queryAll() as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $items[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + $this->db->createCommand() + ->insert($this->itemTable, array( + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if ($this->usingSqlite()) { + $this->db->createCommand() + ->delete($this->itemChildTable, 'parent=:name1 OR child=:name2', array( + ':name1' => $name, + ':name2' => $name + )); + $this->db->createCommand() + ->delete($this->assignmentTable, 'itemname=:name', array( + ':name' => $name, + )); + } + + return $this->db->createCommand() + ->delete($this->itemTable, 'name=:name', array( + ':name' => $name + )) > 0; + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + $query = new Query; + $row = $query->from($this->itemTable) + ->where('name=:name', array(':name' => $name)) + ->createCommand($this->db) + ->queryRow(); + + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } else + return null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveItem($item, $oldName = null) + { + if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'parent' => $item->getName(), + ), 'parent=:whereName', array( + ':whereName' => $oldName, + )); + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'child' => $item->getName(), + ), 'child=:whereName', array( + ':whereName' => $oldName, + )); + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'itemname' => $item->getName(), + ), 'itemname=:whereName', array( + ':whereName' => $oldName, + )); + } + + $this->db->createCommand() + ->update($this->itemTable, array( + 'name' => $item->getName(), + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizrule' => $item->getBizRule(), + 'data' => serialize($item->getData()), + ), 'name=:whereName', array( + ':whereName' => $oldName === null ? $item->getName() : $oldName, + )); + } + + /** + * Saves the authorization data to persistent storage. + */ + public function save() + { + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->db->createCommand()->delete($this->itemChildTable); + $this->db->createCommand()->delete($this->itemTable); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->db->createCommand()->delete($this->assignmentTable); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + foreach ($this->getItemChildren($childName) as $child) { + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * @return boolean whether the database is a SQLite database + */ + protected function usingSqlite() + { + return $this->_usingSqlite; + } +} diff --git a/framework/rbac/IManager.php b/framework/rbac/IManager.php new file mode 100644 index 0000000..6eeb661 --- /dev/null +++ b/framework/rbac/IManager.php @@ -0,0 +1,173 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +interface IManager +{ + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()); + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return Item the authorization item + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name); + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveItem($item, $oldName = null); + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($itemName); + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Item[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment); + /** + * Removes all authorization data. + */ + public function clearAll(); + /** + * Removes all authorization assignments. + */ + public function clearAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save(); + /** + * Executes a business rule. + * A business rule is a piece of PHP code that will be executed when [[checkAccess()]] is called. + * @param string $bizRule the business rule to be executed. + * @param array $params additional parameters to be passed to the business rule when being executed. + * @param mixed $data additional data that is associated with the corresponding authorization item or assignment + * @return boolean whether the execution returns a true value. + * If the business rule is empty, it will also return true. + */ + public function executeBizRule($bizRule, $params, $data); +} diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php index ba78e60..d09196f 100644 --- a/framework/rbac/Item.php +++ b/framework/rbac/Item.php @@ -202,7 +202,7 @@ class Item extends Object /** * Returns the children of this item. - * @return array all child items of this item. + * @return Item[] all child items of this item. * @see IManager::getItemChildren */ public function getChildren() diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php index 2f77e10..4846546 100644 --- a/framework/rbac/Manager.php +++ b/framework/rbac/Manager.php @@ -83,7 +83,7 @@ abstract class Manager extends Component implements IManager * This is a shortcut method to [[IManager::getItems()]]. * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user * will be returned. Otherwise, all roles will be returned. - * @return array roles (name=>AuthItem) + * @return Item[] roles (name=>AuthItem) */ public function getRoles($userId = null) { @@ -95,7 +95,7 @@ abstract class Manager extends Component implements IManager * This is a shortcut method to [[IManager::getItems()]]. * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user * will be returned. Otherwise, all tasks will be returned. - * @return array tasks (name=>AuthItem) + * @return Item[] tasks (name=>AuthItem) */ public function getTasks($userId = null) { @@ -107,7 +107,7 @@ abstract class Manager extends Component implements IManager * This is a shortcut method to [[IManager::getItems()]]. * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user * will be returned. Otherwise, all operations will be returned. - * @return array operations (name=>AuthItem) + * @return Item[] operations (name=>AuthItem) */ public function getOperations($userId = null) { diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index 08f5c83..3f471c6 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -52,8 +52,8 @@ class PhpManager extends Manager * @param string $itemName the name of the operation that need access check * the unique identifier of a user. See [[User::id]]. * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. - * Since version 1.1.11 a param with name 'userId' is added to this array, which holds the value of $userId. + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of `$userId`. * @return boolean whether the operations can be performed by the user. */ public function checkAccess($userId, $itemName, $params = array()) @@ -61,6 +61,7 @@ class PhpManager extends Manager if (!isset($this->_items[$itemName])) { return false; } + /** @var $item Item */ $item = $this->_items[$itemName]; Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); if (!isset($params['userId'])) { @@ -71,6 +72,7 @@ class PhpManager extends Manager return true; } if (isset($this->_assignments[$userId][$itemName])) { + /** @var $assignment Assignment */ $assignment = $this->_assignments[$userId][$itemName]; if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { return true; @@ -97,7 +99,9 @@ class PhpManager extends Manager if (!isset($this->_items[$childName], $this->_items[$itemName])) { throw new Exception("Either '$itemName' or '$childName' does not exist."); } + /** @var $child Item */ $child = $this->_items[$childName]; + /** @var $item Item */ $item = $this->_items[$itemName]; $this->checkItemChildType($item->getType(), $child->getType()); if ($this->detectLoop($itemName, $childName)) { @@ -142,7 +146,7 @@ class PhpManager extends Manager * Returns the children of the specified item. * @param mixed $names the parent item name. This can be either a string or an array. * The latter represents a list of item names. - * @return array all child items of the parent + * @return Item[] all child items of the parent */ public function getItemChildren($names) { @@ -176,7 +180,7 @@ class PhpManager extends Manager } elseif (isset($this->_assignments[$userId][$itemName])) { throw new Exception("Authorization item '$itemName' has already been assigned to user '$userId'."); } else { - return $this->_assignments[$userId][$itemName] = new Assignment($this, $itemName, $userId, $bizRule, $data); + return $this->_assignments[$userId][$itemName] = new Assignment($this, $userId, $itemName, $bizRule, $data); } } @@ -222,7 +226,7 @@ class PhpManager extends Manager /** * Returns the item assignments for the specified user. * @param mixed $userId the user ID (see [[User::id]]) - * @return array the item assignment information for the user. An empty array will be + * @return Assignment[] the item assignment information for the user. An empty array will be * returned if there is no item assigned to the user. */ public function getAssignments($userId) @@ -236,22 +240,24 @@ class PhpManager extends Manager * they are not assigned to a user. * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, * meaning returning all items regardless of their type. - * @return array the authorization items of the specific type. + * @return Item[] the authorization items of the specific type. */ public function getItems($userId = null, $type = null) { - if ($type === null && $userId === null) { + if ($userId === null && $type === null) { return $this->_items; } $items = array(); if ($userId === null) { foreach ($this->_items as $name => $item) { + /** @var $item Item */ if ($item->getType() == $type) { $items[$name] = $item; } } } elseif (isset($this->_assignments[$userId])) { foreach ($this->_assignments[$userId] as $assignment) { + /** @var $assignment Assignment */ $name = $assignment->getItemName(); if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->getType() == $type)) { $items[$name] = $this->_items[$name]; @@ -368,6 +374,7 @@ class PhpManager extends Manager { $items = array(); foreach ($this->_items as $name => $item) { + /** @var $item Item */ $items[$name] = array( 'type' => $item->getType(), 'description' => $item->getDescription(), @@ -376,6 +383,7 @@ class PhpManager extends Manager ); if (isset($this->_children[$name])) { foreach ($this->_children[$name] as $child) { + /** @var $child Item */ $items[$name]['children'][] = $child->getName(); } } @@ -383,6 +391,7 @@ class PhpManager extends Manager foreach ($this->_assignments as $userId => $assignments) { foreach ($assignments as $name => $assignment) { + /** @var $assignment Assignment */ if (isset($items[$name])) { $items[$name]['assignments'][$userId] = array( 'bizRule' => $assignment->getBizRule(), @@ -457,6 +466,7 @@ class PhpManager extends Manager return false; } foreach ($this->_children[$childName] as $child) { + /** @var $child Item */ if ($this->detectLoop($itemName, $child->getName())) { return true; } From c9fe510ebbe14266519b8feb1e5b687075372dad Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 09:52:21 +0400 Subject: [PATCH 05/19] Comment fixes + class descriptions added --- framework/rbac/Assignment.php | 9 +++++++++ framework/rbac/DbManager.php | 12 ++++++++++-- framework/rbac/IManager.php | 2 ++ framework/rbac/Item.php | 14 ++++++++++++++ framework/rbac/Manager.php | 24 ++++++++++++++++++++++++ framework/rbac/PhpManager.php | 13 ++++++++++++- 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php index 576017e..e5d6157 100644 --- a/framework/rbac/Assignment.php +++ b/framework/rbac/Assignment.php @@ -11,6 +11,15 @@ use Yii; use yii\base\Object; /** + * Assignment represents an assignment of a role to a user. + * It includes additional assignment information such as [[bizRule]] and [[data]]. + * Do not create a Assignment instance using the 'new' operator. + * Instead, call [[IManager::assign]]. + * + * @property mixed $userId User ID (see [[User::id]]). + * @property string $itemName The authorization item name. + * @property string $bizRule The business rule associated with this assignment. + * @property mixed $data Additional data for this assignment. * * @author Qiang Xue * @author Alexander Kochetov diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index 5f64694..d3c584c 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -14,6 +14,14 @@ use yii\base\Exception; use yii\base\InvalidConfigException; /** + * DbManager represents an authorization manager that stores authorization information in database. + * + * The database connection is specified by [[db]]. And the database schema + * should be as described in "framework/rbac/*.sql". You may change the names of + * the three tables used to store the authorization data by setting [[itemTable]], + * [[itemChildTable]] and [[assignmentTable]]. + * + * @property array $authItems The authorization items of the specific type. * * @author Qiang Xue * @author Alexander Kochetov @@ -60,9 +68,9 @@ class DbManager extends Manager /** * Performs access check for the specified user. - * @param mixed $userId the user ID. This should can be either an integer and a string representing - * @param string $itemName the name of the operation that need access check + * @param mixed $userId the user ID. This should can be either an integer or a string representing * the unique identifier of a user. See [[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 biz rules associated * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, * which holds the value of `$userId`. diff --git a/framework/rbac/IManager.php b/framework/rbac/IManager.php index 6eeb661..0e9eea0 100644 --- a/framework/rbac/IManager.php +++ b/framework/rbac/IManager.php @@ -10,6 +10,8 @@ namespace yii\rbac; /** * IManager interface is implemented by an auth manager application component. * + * An auth manager is mainly responsible for providing role-based access control (RBAC) service. + * * @author Qiang Xue * @author Alexander Kochetov * @since 2.0 diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php index d09196f..1297e41 100644 --- a/framework/rbac/Item.php +++ b/framework/rbac/Item.php @@ -11,6 +11,20 @@ use Yii; use yii\base\Object; /** + * Item represents an authorization item. + * An authorization item can be an operation, a task or a role. + * They form an authorization hierarchy. Items on higher levels of the hierarchy + * inherit the permissions represented by items on lower levels. + * A user may be assigned one or several authorization items (called [[Assignment]] assignments). + * He can perform an operation only when it is among his assigned items. + * + * @property IManager $authManager The authorization manager. + * @property integer $type The authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + * @property string $name The item name. + * @property string $description The item description. + * @property string $bizRule The business rule associated with this item. + * @property mixed $data The additional data associated with this item. + * @property array $children All child items of this item. * * @author Qiang Xue * @author Alexander Kochetov diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php index 4846546..6d0b74d 100644 --- a/framework/rbac/Manager.php +++ b/framework/rbac/Manager.php @@ -12,6 +12,30 @@ use yii\base\Component; use yii\base\Exception; /** + * Manager is the base class for authorization manager classes. + * + * Manager extends [[Component]] and implements some methods + * that are common among authorization manager classes. + * + * Manager together with its concrete child classes implement the Role-Based + * Access Control (RBAC). + * + * The main idea is that permissions are organized as a hierarchy of + * [[Item]] authorization items. Items on higer level inherit the permissions + * represented by items on lower level. And roles are simply top-level authorization items + * that may be assigned to individual users. A user is said to have a permission + * to do something if the corresponding authorization item is inherited by one of his roles. + * + * Using authorization manager consists of two aspects. First, the authorization hierarchy + * and assignments have to be established. Manager and its child classes + * provides APIs to accomplish this task. Developers may need to develop some GUI + * so that it is more intuitive to end-users. Second, developers call [[IManager::checkAccess()]] + * at appropriate places in the application code to check if the current user + * has the needed permission for an operation. + * + * @property array $roles Roles (name=>Item). + * @property array $tasks Tasks (name=>Item). + * @property array $operations Operations (name=>Item). * * @author Qiang Xue * @author Alexander Kochetov diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index 3f471c6..baacb36 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -11,6 +11,17 @@ use Yii; use yii\base\Exception; /** + * PhpManager represents an authorization manager that stores authorization + * information in terms of a PHP script file. + * + * The authorization data will be saved to and loaded from a file + * specified by [[authFile]], which defaults to 'protected/data/rbac.php'. + * + * PhpManager is mainly suitable for authorization data that is not too big + * (for example, the authorization data for a personal blog system). + * Use [[DbManager]] for more complex authorization data. + * + * @property array $authItems The authorization items of the specific type. * * @author Qiang Xue * @author Alexander Kochetov @@ -20,7 +31,7 @@ class PhpManager extends Manager { /** * @var string the path of the PHP script that contains the authorization data. - * If not set, it will be using 'protected/data/auth.php' as the data file. + * If not set, it will be using 'protected/data/rbac.php' as the data file. * Make sure this file is writable by the Web server process if the authorization * needs to be changed. * @see loadFromFile From 132391065ca3a148ffa777985ac72886bf9e6ab6 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 09:59:18 +0400 Subject: [PATCH 06/19] Comment fixes --- framework/rbac/DbManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index d3c584c..81a0edd 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -85,7 +85,7 @@ class DbManager extends Manager /** * Performs access check for the specified user. * This method is internally called by [[checkAccess()]]. - * @param mixed $userId the user ID. This should can be either an integer and a string representing + * @param mixed $userId the user ID. This should can be either an integer or a string representing * the unique identifier of a user. See [[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 biz rules associated From 56757cdd94dca5fe357f02373f7f9b8ace130120 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 10:02:36 +0400 Subject: [PATCH 07/19] PhpManager::saveToFile() LOCK_EX added --- framework/rbac/PhpManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index baacb36..60eb193 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -508,6 +508,6 @@ class PhpManager extends Manager */ protected function saveToFile($data, $file) { - file_put_contents($file, " Date: Fri, 10 May 2013 10:15:19 +0400 Subject: [PATCH 08/19] Exception types corrected --- framework/rbac/DbManager.php | 6 ++++-- framework/rbac/Manager.php | 6 +++--- framework/rbac/PhpManager.php | 19 +++++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index 81a0edd..286c0b5 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -12,6 +12,7 @@ use yii\db\Connection; use yii\db\Query; use yii\base\Exception; use yii\base\InvalidConfigException; +use yii\base\InvalidCallException; /** * DbManager represents an authorization manager that stores authorization information in database. @@ -133,7 +134,8 @@ class DbManager extends Manager * @param string $itemName the parent item name * @param string $childName the child item name * @return boolean whether the item is added successfully - * @throws Exception if either parent or child doesn't exist or if a loop has been detected. + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if a loop has been detected. */ public function addItemChild($itemName, $childName) { @@ -158,7 +160,7 @@ class DbManager extends Manager } $this->checkItemChildType($parentType, $childType); if ($this->detectLoop($itemName, $childName)) { - throw new Exception("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); } $this->db->createCommand() ->insert($this->itemChildTable, array( diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php index 6d0b74d..ce36e60 100644 --- a/framework/rbac/Manager.php +++ b/framework/rbac/Manager.php @@ -9,7 +9,7 @@ namespace yii\rbac; use Yii; use yii\base\Component; -use yii\base\Exception; +use yii\base\InvalidParamException; /** * Manager is the base class for authorization manager classes. @@ -155,13 +155,13 @@ abstract class Manager extends Component implements IManager * Checks the item types to make sure a child can be added to a parent. * @param integer $parentType parent item type * @param integer $childType child item type - * @throws Exception if the item cannot be added as a child due to its incompatible type. + * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. */ protected function checkItemChildType($parentType, $childType) { static $types = array('operation', 'task', 'role'); if ($parentType < $childType) { - throw new Exception("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); + throw new InvalidParamException("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); } } } diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php index 60eb193..8a4dbec 100644 --- a/framework/rbac/PhpManager.php +++ b/framework/rbac/PhpManager.php @@ -9,6 +9,8 @@ namespace yii\rbac; use Yii; use yii\base\Exception; +use yii\base\InvalidCallException; +use yii\base\InvalidParamException; /** * PhpManager represents an authorization manager that stores authorization @@ -103,7 +105,8 @@ class PhpManager extends Manager * @param string $itemName the parent item name * @param string $childName the child item name * @return boolean whether the item is added successfully - * @throws Exception if either parent or child doesn't exist or if a loop has been detected. + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. */ public function addItemChild($itemName, $childName) { @@ -116,10 +119,10 @@ class PhpManager extends Manager $item = $this->_items[$itemName]; $this->checkItemChildType($item->getType(), $child->getType()); if ($this->detectLoop($itemName, $childName)) { - throw new Exception("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); } if (isset($this->_children[$itemName][$childName])) { - throw new Exception("The item '$itemName' already has a child '$childName'."); + throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); } $this->_children[$itemName][$childName] = $this->_items[$childName]; return true; @@ -182,14 +185,14 @@ class PhpManager extends Manager * for this particular authorization item. * @param mixed $data additional data associated with this assignment * @return Assignment the authorization assignment information. - * @throws Exception if the item does not exist or if the item has already been assigned to the user + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user */ public function assign($userId, $itemName, $bizRule = null, $data = null) { if (!isset($this->_items[$itemName])) { - throw new Exception("Unknown authorization item '$itemName'."); + throw new InvalidParamException("Unknown authorization item '$itemName'."); } elseif (isset($this->_assignments[$userId][$itemName])) { - throw new Exception("Authorization item '$itemName' has already been assigned to user '$userId'."); + throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); } else { return $this->_assignments[$userId][$itemName] = new Assignment($this, $userId, $itemName, $bizRule, $data); } @@ -336,14 +339,14 @@ class PhpManager extends Manager * Saves an authorization item to persistent storage. * @param Item $item the item to be saved. * @param string $oldName the old item name. If null, it means the item name is not changed. - * @throws Exception if an item with the same name already taken + * @throws InvalidParamException if an item with the same name already taken */ public function saveItem($item, $oldName = null) { if ($oldName !== null && ($newName = $item->getName()) !== $oldName) // name changed { if (isset($this->_items[$newName])) { - throw new Exception("Unable to change the item name. The name '$newName' is already used by another item."); + throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); } if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { unset($this->_items[$oldName]); From a03d1164adbe2a0a5a81958918e0bc2fe731ab2f Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 10:47:08 +0400 Subject: [PATCH 09/19] New condition hash format used where its possible --- framework/rbac/DbManager.php | 93 +++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index 286c0b5..ea6e8ac 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -117,7 +117,7 @@ class DbManager extends Manager $query = new Query; $parents = $query->select(array('parent')) ->from($this->itemChildTable) - ->where('child=:name', array(':name' => $itemName)) + ->where(array('child' => $itemName)) ->createCommand($this->db) ->queryColumn(); foreach ($parents as $parent) { @@ -144,7 +144,7 @@ class DbManager extends Manager } $query = new Query; $rows = $query->from($this->itemTable) - ->where('name=:name1 OR name=:name2', array( + ->where(array('or', 'name=:name1', 'name=:name2'), array( ':name1' => $itemName, ':name2' => $childName )) @@ -183,9 +183,9 @@ class DbManager extends Manager public function removeItemChild($itemName, $childName) { return $this->db->createCommand() - ->delete($this->itemChildTable, 'parent=:parent AND child=:child', array( - ':parent' => $itemName, - ':child' => $childName + ->delete($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName )) > 0; } @@ -200,9 +200,10 @@ class DbManager extends Manager $query = new Query; return $query->select(array('parent')) ->from($this->itemChildTable) - ->where('parent=:parent AND child=:child', array( - ':parent' => $itemName, - ':child' => $childName)) + ->where(array( + 'parent' => $itemName, + 'child' => $childName + )) ->createCommand($this->db) ->queryScalar() !== false; } @@ -215,21 +216,14 @@ class DbManager extends Manager */ public function getItemChildren($names) { - if (is_string($names)) { - $condition = 'parent=' . $this->db->quoteValue($names); - } elseif (is_array($names) && !empty($names)) { - foreach ($names as &$name) { - $name = $this->db->quoteValue($name); - } - $condition = 'parent IN (' . implode(', ', $names) . ')'; - } $query = new Query; $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) ->from(array( $this->itemTable, $this->itemChildTable )) - ->where($condition . ' AND name=child') + ->where(array('parent'=>$names)) + ->andWhere('name=child') ->createCommand($this->db) ->queryAll(); $children = array(); @@ -276,9 +270,9 @@ class DbManager extends Manager public function revoke($userId, $itemName) { return $this->db->createCommand() - ->delete($this->assignmentTable, 'itemname=:itemname AND userid=:userid', array( - ':userid' => $userId, - ':itemname' => $itemName + ->delete($this->assignmentTable, array( + 'userid' => $userId, + 'itemname' => $itemName )) > 0; } @@ -293,9 +287,9 @@ class DbManager extends Manager $query = new Query; return $query->select(array('itemname')) ->from($this->assignmentTable) - ->where('itemname=:itemname AND userid=:userid', array( - ':userid' => $userId, - ':itemname' => $itemName + ->where(array( + 'userid' => $userId, + 'itemname' => $itemName )) ->createCommand($this->db) ->queryScalar() !== false; @@ -312,9 +306,9 @@ class DbManager extends Manager { $query = new Query; $row = $query->from($this->assignmentTable) - ->where('itemname=:itemname AND userid=:userid', array( - ':userid' => $userId, - ':itemname' => $itemName + ->where(array( + 'userid' => $userId, + 'itemname' => $itemName )) ->createCommand($this->db) ->queryRow(); @@ -338,7 +332,7 @@ class DbManager extends Manager { $query = new Query; $rows = $query->from($this->assignmentTable) - ->where('userid=:userid', array(':userid' => $userId)) + ->where(array('userid' => $userId)) ->createCommand($this->db) ->queryAll(); $assignments = array(); @@ -361,7 +355,7 @@ class DbManager extends Manager ->update($this->assignmentTable, array( 'bizrule' => $assignment->getBizRule(), 'data' => serialize($assignment->getData()), - ), 'itemname=:itemname AND userid=:userid', array( + ), array( 'userid' => $assignment->getUserId(), 'itemname' => $assignment->getItemName() )); @@ -383,7 +377,7 @@ class DbManager extends Manager ->createCommand($this->db); } elseif ($userId === null) { $command = $query->from($this->itemTable) - ->where('type=:type', array(':type' => $type)) + ->where(array('type' => $type)) ->createCommand($this->db); } elseif ($type === null) { $command = $query->select(array('name', 'type', 'description', 't1.bizrule', 't1.data')) @@ -391,7 +385,8 @@ class DbManager extends Manager $this->itemTable . ' t1', $this->assignmentTable . ' t2' )) - ->where('name=itemname AND userid=:userid', array(':userid' => $userId)) + ->where(array('userid' => $userId)) + ->andWhere('name=itemname') ->createCommand($this->db); } else { $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') @@ -399,10 +394,11 @@ class DbManager extends Manager $this->itemTable . ' t1', $this->assignmentTable . ' t2' )) - ->where('name=itemname AND type=:type AND userid=:userid', array( - ':userid' => $userId, - ':type' => $type + ->where(array( + 'userid' => $userId, + 'type' => $type )) + ->andWhere('name=itemname') ->createCommand($this->db); } $items = array(); @@ -452,20 +448,13 @@ class DbManager extends Manager { if ($this->usingSqlite()) { $this->db->createCommand() - ->delete($this->itemChildTable, 'parent=:name1 OR child=:name2', array( + ->delete($this->itemChildTable, array('or', 'parent=:name1', 'child=:name2'), array( ':name1' => $name, ':name2' => $name )); - $this->db->createCommand() - ->delete($this->assignmentTable, 'itemname=:name', array( - ':name' => $name, - )); + $this->db->createCommand()->delete($this->assignmentTable, array('itemname' => $name)); } - - return $this->db->createCommand() - ->delete($this->itemTable, 'name=:name', array( - ':name' => $name - )) > 0; + return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; } /** @@ -477,7 +466,7 @@ class DbManager extends Manager { $query = new Query; $row = $query->from($this->itemTable) - ->where('name=:name', array(':name' => $name)) + ->where(array('name' => $name)) ->createCommand($this->db) ->queryRow(); @@ -501,20 +490,20 @@ class DbManager extends Manager $this->db->createCommand() ->update($this->itemChildTable, array( 'parent' => $item->getName(), - ), 'parent=:whereName', array( - ':whereName' => $oldName, + ), array( + 'parent' => $oldName, )); $this->db->createCommand() ->update($this->itemChildTable, array( 'child' => $item->getName(), - ), 'child=:whereName', array( - ':whereName' => $oldName, + ), array( + 'child' => $oldName, )); $this->db->createCommand() ->update($this->assignmentTable, array( 'itemname' => $item->getName(), - ), 'itemname=:whereName', array( - ':whereName' => $oldName, + ), array( + 'itemname' => $oldName, )); } @@ -525,8 +514,8 @@ class DbManager extends Manager 'description' => $item->getDescription(), 'bizrule' => $item->getBizRule(), 'data' => serialize($item->getData()), - ), 'name=:whereName', array( - ':whereName' => $oldName === null ? $item->getName() : $oldName, + ), array( + 'name' => $oldName === null ? $item->getName() : $oldName, )); } From 3fe23f83c75bc106e17885297de4be6cae3d61d2 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 13:50:17 +0400 Subject: [PATCH 10/19] Remove IManager interface --- framework/rbac/Assignment.php | 4 +- framework/rbac/IManager.php | 175 ------------------------------------------ framework/rbac/Item.php | 24 +++--- framework/rbac/Manager.php | 163 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 168 insertions(+), 198 deletions(-) delete mode 100644 framework/rbac/IManager.php diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php index e5d6157..5b6a607 100644 --- a/framework/rbac/Assignment.php +++ b/framework/rbac/Assignment.php @@ -14,7 +14,7 @@ use yii\base\Object; * Assignment represents an assignment of a role to a user. * It includes additional assignment information such as [[bizRule]] and [[data]]. * Do not create a Assignment instance using the 'new' operator. - * Instead, call [[IManager::assign]]. + * Instead, call [[Manager::assign()]]. * * @property mixed $userId User ID (see [[User::id]]). * @property string $itemName The authorization item name. @@ -35,7 +35,7 @@ class Assignment extends Object /** * Constructor. - * @param IManager $auth the authorization manager + * @param Manager $auth the authorization manager * @param mixed $userId user ID (see [[User::id]]) * @param string $itemName authorization item name * @param string $bizRule the business rule associated with this assignment diff --git a/framework/rbac/IManager.php b/framework/rbac/IManager.php deleted file mode 100644 index 0e9eea0..0000000 --- a/framework/rbac/IManager.php +++ /dev/null @@ -1,175 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -interface IManager -{ - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This should be either an integer or a string representing - * the unique identifier of a user. See [[User::id]]. - * @param string $itemName the name of the operation that we are checking access to - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. - * @return boolean whether the operations can be performed by the user. - */ - public function checkAccess($userId, $itemName, $params = array()); - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @throws \yii\base\Exception if an item with the same name already exists - * @return Item the authorization item - */ - public function createItem($name, $type, $description = '', $bizRule = null, $data = null); - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - public function removeItem($name); - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - public function getItems($userId = null, $type = null); - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - public function getItem($name); - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - */ - public function saveItem($item, $oldName = null); - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - */ - public function addItemChild($itemName, $childName); - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - public function removeItemChild($itemName, $childName); - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - public function hasItemChild($itemName, $childName); - /** - * Returns the children of the specified item. - * @param mixed $itemName the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - public function getItemChildren($itemName); - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user - */ - public function assign($userId, $itemName, $bizRule = null, $data = null); - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - public function revoke($userId, $itemName); - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - public function isAssigned($userId, $itemName); - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - public function getAssignment($userId, $itemName); - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return Item[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - public function getAssignments($userId); - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - public function saveAssignment($assignment); - /** - * Removes all authorization data. - */ - public function clearAll(); - /** - * Removes all authorization assignments. - */ - public function clearAssignments(); - /** - * Saves authorization data into persistent storage. - * If any change is made to the authorization data, please make - * sure you call this method to save the changed data into persistent storage. - */ - public function save(); - /** - * Executes a business rule. - * A business rule is a piece of PHP code that will be executed when [[checkAccess()]] is called. - * @param string $bizRule the business rule to be executed. - * @param array $params additional parameters to be passed to the business rule when being executed. - * @param mixed $data additional data that is associated with the corresponding authorization item or assignment - * @return boolean whether the execution returns a true value. - * If the business rule is empty, it will also return true. - */ - public function executeBizRule($bizRule, $params, $data); -} diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php index 1297e41..ef56a53 100644 --- a/framework/rbac/Item.php +++ b/framework/rbac/Item.php @@ -18,7 +18,7 @@ use yii\base\Object; * A user may be assigned one or several authorization items (called [[Assignment]] assignments). * He can perform an operation only when it is among his assigned items. * - * @property IManager $authManager The authorization manager. + * @property Manager $authManager The authorization manager. * @property integer $type The authorization item type. This could be 0 (operation), 1 (task) or 2 (role). * @property string $name The item name. * @property string $description The item description. @@ -45,7 +45,7 @@ class Item extends Object /** * Constructor. - * @param IManager $auth authorization manager + * @param Manager $auth authorization manager * @param string $name authorization item name * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). * @param string $description the description @@ -65,7 +65,7 @@ class Item extends Object /** * Checks to see if the specified item is within the hierarchy starting from this item. * This method is expected to be internally used by the actual implementations - * of the [[IManager::checkAccess()]]. + * of the [[Manager::checkAccess()]]. * @param string $itemName the name of the item to be checked * @param array $params the parameters to be passed to business rule evaluation * @return boolean whether the specified item is within the hierarchy starting from this item. @@ -87,7 +87,7 @@ class Item extends Object } /** - * @return IManager the authorization manager + * @return Manager the authorization manager */ public function getManager() { @@ -184,7 +184,7 @@ class Item extends Object * @param string $name the name of the child item * @return boolean whether the item is added successfully * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - * @see IManager::addItemChild + * @see Manager::addItemChild */ public function addChild($name) { @@ -196,7 +196,7 @@ class Item extends Object * Note, the child item is not deleted. Only the parent-child relationship is removed. * @param string $name the child item name * @return boolean whether the removal is successful - * @see IManager::removeItemChild + * @see Manager::removeItemChild */ public function removeChild($name) { @@ -207,7 +207,7 @@ class Item extends Object * Returns a value indicating whether a child exists * @param string $name the child item name * @return boolean whether the child exists - * @see IManager::hasItemChild + * @see Manager::hasItemChild */ public function hasChild($name) { @@ -217,7 +217,7 @@ class Item extends Object /** * Returns the children of this item. * @return Item[] all child items of this item. - * @see IManager::getItemChildren + * @see Manager::getItemChildren */ public function getChildren() { @@ -232,7 +232,7 @@ class Item extends Object * @param mixed $data additional data associated with this assignment * @return Assignment the authorization assignment information. * @throws \yii\base\Exception if the item has already been assigned to the user - * @see IManager::assign + * @see Manager::assign */ public function assign($userId, $bizRule = null, $data = null) { @@ -243,7 +243,7 @@ class Item extends Object * Revokes an authorization assignment from a user. * @param mixed $userId the user ID (see [[User::id]]) * @return boolean whether removal is successful - * @see IManager::revoke + * @see Manager::revoke */ public function revoke($userId) { @@ -254,7 +254,7 @@ class Item extends Object * Returns a value indicating whether this item has been assigned to the user. * @param mixed $userId the user ID (see [[User::id]]) * @return boolean whether the item has been assigned to the user. - * @see IManager::isAssigned + * @see Manager::isAssigned */ public function isAssigned($userId) { @@ -266,7 +266,7 @@ class Item extends Object * @param mixed $userId the user ID (see [[User::id]]) * @return Assignment the item assignment information. Null is returned if * this item is not assigned to the user. - * @see IManager::getAssignment + * @see Manager::getAssignment */ public function getAssignment($userId) { diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php index ce36e60..5c3c4f6 100644 --- a/framework/rbac/Manager.php +++ b/framework/rbac/Manager.php @@ -29,7 +29,7 @@ use yii\base\InvalidParamException; * Using authorization manager consists of two aspects. First, the authorization hierarchy * and assignments have to be established. Manager and its child classes * provides APIs to accomplish this task. Developers may need to develop some GUI - * so that it is more intuitive to end-users. Second, developers call [[IManager::checkAccess()]] + * so that it is more intuitive to end-users. Second, developers call [[Manager::checkAccess()]] * at appropriate places in the application code to check if the current user * has the needed permission for an operation. * @@ -41,7 +41,7 @@ use yii\base\InvalidParamException; * @author Alexander Kochetov * @since 2.0 */ -abstract class Manager extends Component implements IManager +abstract class Manager extends Component { /** * @var boolean Enable error reporting for bizRules. @@ -62,7 +62,7 @@ abstract class Manager extends Component implements IManager /** * Creates a role. - * This is a shortcut method to [[IManager::createItem()]]. + * This is a shortcut method to [[Manager::createItem()]]. * @param string $name the item name * @param string $description the item description. * @param string $bizRule the business rule associated with this item @@ -76,7 +76,7 @@ abstract class Manager extends Component implements IManager /** * Creates a task. - * This is a shortcut method to [[IManager::createItem()]]. + * This is a shortcut method to [[Manager::createItem()]]. * @param string $name the item name * @param string $description the item description. * @param string $bizRule the business rule associated with this item @@ -90,7 +90,7 @@ abstract class Manager extends Component implements IManager /** * Creates an operation. - * This is a shortcut method to [[IManager::createItem()]]. + * This is a shortcut method to [[Manager::createItem()]]. * @param string $name the item name * @param string $description the item description. * @param string $bizRule the business rule associated with this item @@ -104,7 +104,7 @@ abstract class Manager extends Component implements IManager /** * Returns roles. - * This is a shortcut method to [[IManager::getItems()]]. + * This is a shortcut method to [[Manager::getItems()]]. * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user * will be returned. Otherwise, all roles will be returned. * @return Item[] roles (name=>AuthItem) @@ -116,7 +116,7 @@ abstract class Manager extends Component implements IManager /** * Returns tasks. - * This is a shortcut method to [[IManager::getItems()]]. + * This is a shortcut method to [[Manager::getItems()]]. * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user * will be returned. Otherwise, all tasks will be returned. * @return Item[] tasks (name=>AuthItem) @@ -128,7 +128,7 @@ abstract class Manager extends Component implements IManager /** * Returns operations. - * This is a shortcut method to [[IManager::getItems()]]. + * This is a shortcut method to [[Manager::getItems()]]. * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user * will be returned. Otherwise, all operations will be returned. * @return Item[] operations (name=>AuthItem) @@ -141,7 +141,7 @@ abstract class Manager extends Component implements IManager /** * Executes the specified business rule. * @param string $bizRule the business rule to be executed. - * @param array $params parameters passed to [[IManager::checkAccess()]]. + * @param array $params parameters passed to [[Manager::checkAccess()]]. * @param mixed $data additional data associated with the authorization item or assignment. * @return boolean whether the business rule returns true. * If the business rule is empty, it will still return true. @@ -164,4 +164,149 @@ abstract class Manager extends Component implements IManager throw new InvalidParamException("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); } } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + abstract public function checkAccess($userId, $itemName, $params = array()); + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return Item the authorization item + */ + abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + abstract public function removeItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + abstract public function getItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + abstract public function getItem($name); + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + abstract public function saveItem($item, $oldName = null); + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + abstract public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + abstract public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + abstract public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + abstract public function getItemChildren($itemName); + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + abstract public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + abstract public function revoke($userId, $itemName); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + abstract public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + abstract public function getAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Item[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + abstract public function getAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + abstract public function saveAssignment($assignment); + /** + * Removes all authorization data. + */ + abstract public function clearAll(); + /** + * Removes all authorization assignments. + */ + abstract public function clearAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + abstract public function save(); } From 0d90541084a0e9e0135c98a05946326126d0a053 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 14:59:11 +0400 Subject: [PATCH 11/19] Moved from framework to yii --- framework/rbac/Assignment.php | 106 -------- framework/rbac/DbManager.php | 573 ------------------------------------------ framework/rbac/Item.php | 275 -------------------- framework/rbac/Manager.php | 312 ----------------------- framework/rbac/PhpManager.php | 516 ------------------------------------- yii/rbac/Assignment.php | 106 ++++++++ yii/rbac/DbManager.php | 573 ++++++++++++++++++++++++++++++++++++++++++ yii/rbac/Item.php | 275 ++++++++++++++++++++ yii/rbac/Manager.php | 312 +++++++++++++++++++++++ yii/rbac/PhpManager.php | 516 +++++++++++++++++++++++++++++++++++++ 10 files changed, 1782 insertions(+), 1782 deletions(-) delete mode 100644 framework/rbac/Assignment.php delete mode 100644 framework/rbac/DbManager.php delete mode 100644 framework/rbac/Item.php delete mode 100644 framework/rbac/Manager.php delete mode 100644 framework/rbac/PhpManager.php create mode 100644 yii/rbac/Assignment.php create mode 100644 yii/rbac/DbManager.php create mode 100644 yii/rbac/Item.php create mode 100644 yii/rbac/Manager.php create mode 100644 yii/rbac/PhpManager.php diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php deleted file mode 100644 index 5b6a607..0000000 --- a/framework/rbac/Assignment.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -class Assignment extends Object -{ - private $_auth; - private $_userId; - private $_itemName; - private $_bizRule; - private $_data; - - /** - * Constructor. - * @param Manager $auth the authorization manager - * @param mixed $userId user ID (see [[User::id]]) - * @param string $itemName authorization item name - * @param string $bizRule the business rule associated with this assignment - * @param mixed $data additional data for this assignment - */ - public function __construct($auth, $userId, $itemName, $bizRule = null, $data = null) - { - $this->_auth = $auth; - $this->_userId = $userId; - $this->_itemName = $itemName; - $this->_bizRule = $bizRule; - $this->_data = $data; - } - - /** - * @return mixed user ID (see [[User::id]]) - */ - public function getUserId() - { - return $this->_userId; - } - - /** - * @return string the authorization item name - */ - public function getItemName() - { - return $this->_itemName; - } - - /** - * @return string the business rule associated with this assignment - */ - public function getBizRule() - { - return $this->_bizRule; - } - - /** - * @param string $value the business rule associated with this assignment - */ - public function setBizRule($value) - { - if ($this->_bizRule !== $value) { - $this->_bizRule = $value; - $this->_auth->saveAssignment($this); - } - } - - /** - * @return mixed additional data for this assignment - */ - public function getData() - { - return $this->_data; - } - - /** - * @param mixed $value additional data for this assignment - */ - public function setData($value) - { - if ($this->_data !== $value) { - $this->_data = $value; - $this->_auth->saveAssignment($this); - } - } -} diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php deleted file mode 100644 index ea6e8ac..0000000 --- a/framework/rbac/DbManager.php +++ /dev/null @@ -1,573 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -class DbManager extends Manager -{ - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbManager object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; - /** - * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. - */ - public $itemTable = 'tbl_auth_item'; - /** - * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. - */ - public $itemChildTable = 'tbl_auth_item_child'; - /** - * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. - */ - public $assignmentTable = 'tbl_auth_assignment'; - - private $_usingSqlite; - - /** - * Initializes the application component. - * This method overrides the parent implementation by establishing the database connection. - */ - public function init() - { - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); - } - $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); - parent::init(); - } - - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This should can be either an integer or a string representing - * the unique identifier of a user. See [[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 biz rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, - * which holds the value of `$userId`. - * @return boolean whether the operations can be performed by the user. - */ - public function checkAccess($userId, $itemName, $params = array()) - { - $assignments = $this->getAssignments($userId); - return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); - } - - /** - * Performs access check for the specified user. - * This method is internally called by [[checkAccess()]]. - * @param mixed $userId the user ID. This should can be either an integer or a string representing - * the unique identifier of a user. See [[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 biz rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' 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($userId, $itemName, $params, $assignments) - { - if (($item = $this->getItem($itemName)) === null) { - return false; - } - Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); - if (!isset($params['userId'])) { - $params['userId'] = $userId; - } - if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { - if (in_array($itemName, $this->defaultRoles)) { - return true; - } - if (isset($assignments[$itemName])) { - $assignment = $assignments[$itemName]; - if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { - return true; - } - } - $query = new Query; - $parents = $query->select(array('parent')) - ->from($this->itemChildTable) - ->where(array('child' => $itemName)) - ->createCommand($this->db) - ->queryColumn(); - foreach ($parents as $parent) { - if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { - return true; - } - } - } - return false; - } - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the item is added successfully - * @throws Exception if either parent or child doesn't exist. - * @throws InvalidCallException if a loop has been detected. - */ - public function addItemChild($itemName, $childName) - { - if ($itemName === $childName) { - throw new Exception("Cannot add '$itemName' as a child of itself."); - } - $query = new Query; - $rows = $query->from($this->itemTable) - ->where(array('or', 'name=:name1', 'name=:name2'), array( - ':name1' => $itemName, - ':name2' => $childName - )) - ->createCommand($this->db) - ->queryAll(); - if (count($rows) == 2) { - if ($rows[0]['name'] === $itemName) { - $parentType = $rows[0]['type']; - $childType = $rows[1]['type']; - } else { - $childType = $rows[0]['type']; - $parentType = $rows[1]['type']; - } - $this->checkItemChildType($parentType, $childType); - if ($this->detectLoop($itemName, $childName)) { - throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); - } - $this->db->createCommand() - ->insert($this->itemChildTable, array( - 'parent' => $itemName, - 'child' => $childName, - )); - return true; - } else { - throw new Exception("Either '$itemName' or '$childName' does not exist."); - } - } - - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - public function removeItemChild($itemName, $childName) - { - return $this->db->createCommand() - ->delete($this->itemChildTable, array( - 'parent' => $itemName, - 'child' => $childName - )) > 0; - } - - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - public function hasItemChild($itemName, $childName) - { - $query = new Query; - return $query->select(array('parent')) - ->from($this->itemChildTable) - ->where(array( - 'parent' => $itemName, - 'child' => $childName - )) - ->createCommand($this->db) - ->queryScalar() !== false; - } - - /** - * Returns the children of the specified item. - * @param mixed $names the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - public function getItemChildren($names) - { - $query = new Query; - $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) - ->from(array( - $this->itemTable, - $this->itemChildTable - )) - ->where(array('parent'=>$names)) - ->andWhere('name=child') - ->createCommand($this->db) - ->queryAll(); - $children = array(); - foreach ($rows as $row) { - if (($data = @unserialize($row['data'])) === false) { - $data = null; - } - $children[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); - } - return $children; - } - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws Exception if the item does not exist or if the item has already been assigned to the user - */ - public function assign($userId, $itemName, $bizRule = null, $data = null) - { - if ($this->usingSqlite() && $this->getItem($itemName) === null) { - throw new Exception("The item '$itemName' does not exist."); - } - $this->db->createCommand() - ->insert($this->assignmentTable, array( - 'userid' => $userId, - 'itemname' => $itemName, - 'bizrule' => $bizRule, - 'data' => serialize($data) - )); - return new Assignment($this, $userId, $itemName, $bizRule, $data); - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - public function revoke($userId, $itemName) - { - return $this->db->createCommand() - ->delete($this->assignmentTable, array( - 'userid' => $userId, - 'itemname' => $itemName - )) > 0; - } - - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - public function isAssigned($itemName, $userId) - { - $query = new Query; - return $query->select(array('itemname')) - ->from($this->assignmentTable) - ->where(array( - 'userid' => $userId, - 'itemname' => $itemName - )) - ->createCommand($this->db) - ->queryScalar() !== false; - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - public function getAssignment($userId, $itemName) - { - $query = new Query; - $row = $query->from($this->assignmentTable) - ->where(array( - 'userid' => $userId, - 'itemname' => $itemName - )) - ->createCommand($this->db) - ->queryRow(); - if ($row !== false) { - if (($data = @unserialize($row['data'])) === false) { - $data = null; - } - return new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); - } else { - return null; - } - } - - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return Assignment[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - public function getAssignments($userId) - { - $query = new Query; - $rows = $query->from($this->assignmentTable) - ->where(array('userid' => $userId)) - ->createCommand($this->db) - ->queryAll(); - $assignments = array(); - foreach ($rows as $row) { - if (($data = @unserialize($row['data'])) === false) { - $data = null; - } - $assignments[$row['itemname']] = new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); - } - return $assignments; - } - - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - public function saveAssignment($assignment) - { - $this->db->createCommand() - ->update($this->assignmentTable, array( - 'bizrule' => $assignment->getBizRule(), - 'data' => serialize($assignment->getData()), - ), array( - 'userid' => $assignment->getUserId(), - 'itemname' => $assignment->getItemName() - )); - } - - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - public function getItems($userId = null, $type = null) - { - $query = new Query; - if ($userId === null && $type === null) { - $command = $query->from($this->itemTable) - ->createCommand($this->db); - } elseif ($userId === null) { - $command = $query->from($this->itemTable) - ->where(array('type' => $type)) - ->createCommand($this->db); - } elseif ($type === null) { - $command = $query->select(array('name', 'type', 'description', 't1.bizrule', 't1.data')) - ->from(array( - $this->itemTable . ' t1', - $this->assignmentTable . ' t2' - )) - ->where(array('userid' => $userId)) - ->andWhere('name=itemname') - ->createCommand($this->db); - } else { - $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') - ->from(array( - $this->itemTable . ' t1', - $this->assignmentTable . ' t2' - )) - ->where(array( - 'userid' => $userId, - 'type' => $type - )) - ->andWhere('name=itemname') - ->createCommand($this->db); - } - $items = array(); - foreach ($command->queryAll() as $row) { - if (($data = @unserialize($row['data'])) === false) { - $data = null; - } - $items[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); - } - return $items; - } - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @return Item the authorization item - * @throws Exception if an item with the same name already exists - */ - public function createItem($name, $type, $description = '', $bizRule = null, $data = null) - { - $this->db->createCommand() - ->insert($this->itemTable, array( - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'bizrule' => $bizRule, - 'data' => serialize($data) - )); - return new Item($this, $name, $type, $description, $bizRule, $data); - } - - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - public function removeItem($name) - { - if ($this->usingSqlite()) { - $this->db->createCommand() - ->delete($this->itemChildTable, array('or', 'parent=:name1', 'child=:name2'), array( - ':name1' => $name, - ':name2' => $name - )); - $this->db->createCommand()->delete($this->assignmentTable, array('itemname' => $name)); - } - return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; - } - - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - public function getItem($name) - { - $query = new Query; - $row = $query->from($this->itemTable) - ->where(array('name' => $name)) - ->createCommand($this->db) - ->queryRow(); - - if ($row !== false) { - if (($data = @unserialize($row['data'])) === false) { - $data = null; - } - return new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); - } else - return null; - } - - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - */ - public function saveItem($item, $oldName = null) - { - if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { - $this->db->createCommand() - ->update($this->itemChildTable, array( - 'parent' => $item->getName(), - ), array( - 'parent' => $oldName, - )); - $this->db->createCommand() - ->update($this->itemChildTable, array( - 'child' => $item->getName(), - ), array( - 'child' => $oldName, - )); - $this->db->createCommand() - ->update($this->assignmentTable, array( - 'itemname' => $item->getName(), - ), array( - 'itemname' => $oldName, - )); - } - - $this->db->createCommand() - ->update($this->itemTable, array( - 'name' => $item->getName(), - 'type' => $item->getType(), - 'description' => $item->getDescription(), - 'bizrule' => $item->getBizRule(), - 'data' => serialize($item->getData()), - ), array( - 'name' => $oldName === null ? $item->getName() : $oldName, - )); - } - - /** - * Saves the authorization data to persistent storage. - */ - public function save() - { - } - - /** - * Removes all authorization data. - */ - public function clearAll() - { - $this->clearAssignments(); - $this->db->createCommand()->delete($this->itemChildTable); - $this->db->createCommand()->delete($this->itemTable); - } - - /** - * Removes all authorization assignments. - */ - public function clearAssignments() - { - $this->db->createCommand()->delete($this->assignmentTable); - } - - /** - * Checks whether there is a loop in the authorization item hierarchy. - * @param string $itemName parent item name - * @param string $childName the name of the child item that is to be added to the hierarchy - * @return boolean whether a loop exists - */ - protected function detectLoop($itemName, $childName) - { - if ($childName === $itemName) { - return true; - } - foreach ($this->getItemChildren($childName) as $child) { - if ($this->detectLoop($itemName, $child->getName())) { - return true; - } - } - return false; - } - - /** - * @return boolean whether the database is a SQLite database - */ - protected function usingSqlite() - { - return $this->_usingSqlite; - } -} diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php deleted file mode 100644 index ef56a53..0000000 --- a/framework/rbac/Item.php +++ /dev/null @@ -1,275 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -class Item extends Object -{ - const TYPE_OPERATION = 0; - const TYPE_TASK = 1; - const TYPE_ROLE = 2; - - private $_auth; - private $_type; - private $_name; - private $_description; - private $_bizRule; - private $_data; - - /** - * Constructor. - * @param Manager $auth authorization manager - * @param string $name authorization item name - * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). - * @param string $description the description - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data for this item - */ - public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) - { - $this->_type = (int)$type; - $this->_auth = $auth; - $this->_name = $name; - $this->_description = $description; - $this->_bizRule = $bizRule; - $this->_data = $data; - } - - /** - * Checks to see if the specified item is within the hierarchy starting from this item. - * This method is expected to be internally used by the actual implementations - * of the [[Manager::checkAccess()]]. - * @param string $itemName the name of the item to be checked - * @param array $params the parameters to be passed to business rule evaluation - * @return boolean whether the specified item is within the hierarchy starting from this item. - */ - public function checkAccess($itemName, $params = array()) - { - Yii::trace('Checking permission: ' . $this->_name, __METHOD__); - if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { - if ($this->_name == $itemName) { - return true; - } - foreach ($this->_auth->getItemChildren($this->_name) as $item) { - if ($item->checkAccess($itemName, $params)) { - return true; - } - } - } - return false; - } - - /** - * @return Manager the authorization manager - */ - public function getManager() - { - return $this->_auth; - } - - /** - * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). - */ - public function getType() - { - return $this->_type; - } - - /** - * @return string the item name - */ - public function getName() - { - return $this->_name; - } - - /** - * @param string $value the item name - */ - public function setName($value) - { - if ($this->_name !== $value) { - $oldName = $this->_name; - $this->_name = $value; - $this->_auth->saveItem($this, $oldName); - } - } - - /** - * @return string the item description - */ - public function getDescription() - { - return $this->_description; - } - - /** - * @param string $value the item description - */ - public function setDescription($value) - { - if ($this->_description !== $value) { - $this->_description = $value; - $this->_auth->saveItem($this); - } - } - - /** - * @return string the business rule associated with this item - */ - public function getBizRule() - { - return $this->_bizRule; - } - - /** - * @param string $value the business rule associated with this item - */ - public function setBizRule($value) - { - if ($this->_bizRule !== $value) { - $this->_bizRule = $value; - $this->_auth->saveItem($this); - } - } - - /** - * @return mixed the additional data associated with this item - */ - public function getData() - { - return $this->_data; - } - - /** - * @param mixed $value the additional data associated with this item - */ - public function setData($value) - { - if ($this->_data !== $value) { - $this->_data = $value; - $this->_auth->saveItem($this); - } - } - - /** - * Adds a child item. - * @param string $name the name of the child item - * @return boolean whether the item is added successfully - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - * @see Manager::addItemChild - */ - public function addChild($name) - { - return $this->_auth->addItemChild($this->_name, $name); - } - - /** - * Removes a child item. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $name the child item name - * @return boolean whether the removal is successful - * @see Manager::removeItemChild - */ - public function removeChild($name) - { - return $this->_auth->removeItemChild($this->_name, $name); - } - - /** - * Returns a value indicating whether a child exists - * @param string $name the child item name - * @return boolean whether the child exists - * @see Manager::hasItemChild - */ - public function hasChild($name) - { - return $this->_auth->hasItemChild($this->_name, $name); - } - - /** - * Returns the children of this item. - * @return Item[] all child items of this item. - * @see Manager::getItemChildren - */ - public function getChildren() - { - return $this->_auth->getItemChildren($this->_name); - } - - /** - * Assigns this item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws \yii\base\Exception if the item has already been assigned to the user - * @see Manager::assign - */ - public function assign($userId, $bizRule = null, $data = null) - { - return $this->_auth->assign($userId, $this->_name, $bizRule, $data); - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return boolean whether removal is successful - * @see Manager::revoke - */ - public function revoke($userId) - { - return $this->_auth->revoke($userId, $this->_name); - } - - /** - * Returns a value indicating whether this item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return boolean whether the item has been assigned to the user. - * @see Manager::isAssigned - */ - public function isAssigned($userId) - { - return $this->_auth->isAssigned($userId, $this->_name); - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @return Assignment the item assignment information. Null is returned if - * this item is not assigned to the user. - * @see Manager::getAssignment - */ - public function getAssignment($userId) - { - return $this->_auth->getAssignment($userId, $this->_name); - } -} diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php deleted file mode 100644 index 5c3c4f6..0000000 --- a/framework/rbac/Manager.php +++ /dev/null @@ -1,312 +0,0 @@ -Item). - * @property array $tasks Tasks (name=>Item). - * @property array $operations Operations (name=>Item). - * - * @author Qiang Xue - * @author Alexander Kochetov - * @since 2.0 - */ -abstract class Manager extends Component -{ - /** - * @var boolean Enable error reporting for bizRules. - */ - public $showErrors = false; - - /** - * @var array list of role names that are assigned to all users implicitly. - * These roles do not need to be explicitly assigned to any user. - * When calling [[checkAccess()]], these roles will be checked first. - * For performance reason, you should minimize the number of such roles. - * A typical usage of such roles is to define an 'authenticated' role and associate - * it with a biz rule which checks if the current user is authenticated. - * And then declare 'authenticated' in this property so that it can be applied to - * every authenticated user. - */ - public $defaultRoles = array(); - - /** - * Creates a role. - * This is a shortcut method to [[Manager::createItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return Item the authorization item - */ - public function createRole($name, $description = '', $bizRule = null, $data = null) - { - return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); - } - - /** - * Creates a task. - * This is a shortcut method to [[Manager::createItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return Item the authorization item - */ - public function createTask($name, $description = '', $bizRule = null, $data = null) - { - return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); - } - - /** - * Creates an operation. - * This is a shortcut method to [[Manager::createItem()]]. - * @param string $name the item name - * @param string $description the item description. - * @param string $bizRule the business rule associated with this item - * @param mixed $data additional data to be passed when evaluating the business rule - * @return Item the authorization item - */ - public function createOperation($name, $description = '', $bizRule = null, $data = null) - { - return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); - } - - /** - * Returns roles. - * This is a shortcut method to [[Manager::getItems()]]. - * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user - * will be returned. Otherwise, all roles will be returned. - * @return Item[] roles (name=>AuthItem) - */ - public function getRoles($userId = null) - { - return $this->getItems($userId, Item::TYPE_ROLE); - } - - /** - * Returns tasks. - * This is a shortcut method to [[Manager::getItems()]]. - * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user - * will be returned. Otherwise, all tasks will be returned. - * @return Item[] tasks (name=>AuthItem) - */ - public function getTasks($userId = null) - { - return $this->getItems($userId, Item::TYPE_TASK); - } - - /** - * Returns operations. - * This is a shortcut method to [[Manager::getItems()]]. - * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user - * will be returned. Otherwise, all operations will be returned. - * @return Item[] operations (name=>AuthItem) - */ - public function getOperations($userId = null) - { - return $this->getItems($userId, Item::TYPE_OPERATION); - } - - /** - * Executes the specified business rule. - * @param string $bizRule the business rule to be executed. - * @param array $params parameters passed to [[Manager::checkAccess()]]. - * @param mixed $data additional data associated with the authorization item or assignment. - * @return boolean whether the business rule returns true. - * If the business rule is empty, it will still return true. - */ - public function executeBizRule($bizRule, $params, $data) - { - return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); - } - - /** - * Checks the item types to make sure a child can be added to a parent. - * @param integer $parentType parent item type - * @param integer $childType child item type - * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. - */ - protected function checkItemChildType($parentType, $childType) - { - static $types = array('operation', 'task', 'role'); - if ($parentType < $childType) { - throw new InvalidParamException("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); - } - } - - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This should be either an integer or a string representing - * the unique identifier of a user. See [[User::id]]. - * @param string $itemName the name of the operation that we are checking access to - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. - * @return boolean whether the operations can be performed by the user. - */ - abstract public function checkAccess($userId, $itemName, $params = array()); - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @throws \yii\base\Exception if an item with the same name already exists - * @return Item the authorization item - */ - abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - abstract public function removeItem($name); - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - abstract public function getItems($userId = null, $type = null); - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - abstract public function getItem($name); - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - */ - abstract public function saveItem($item, $oldName = null); - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. - */ - abstract public function addItemChild($itemName, $childName); - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - abstract public function removeItemChild($itemName, $childName); - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - abstract public function hasItemChild($itemName, $childName); - /** - * Returns the children of the specified item. - * @param mixed $itemName the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - abstract public function getItemChildren($itemName); - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user - */ - abstract public function assign($userId, $itemName, $bizRule = null, $data = null); - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - abstract public function revoke($userId, $itemName); - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - abstract public function isAssigned($userId, $itemName); - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - abstract public function getAssignment($userId, $itemName); - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return Item[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - abstract public function getAssignments($userId); - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - abstract public function saveAssignment($assignment); - /** - * Removes all authorization data. - */ - abstract public function clearAll(); - /** - * Removes all authorization assignments. - */ - abstract public function clearAssignments(); - /** - * Saves authorization data into persistent storage. - * If any change is made to the authorization data, please make - * sure you call this method to save the changed data into persistent storage. - */ - abstract public function save(); -} diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php deleted file mode 100644 index 8a4dbec..0000000 --- a/framework/rbac/PhpManager.php +++ /dev/null @@ -1,516 +0,0 @@ - - * @author Alexander Kochetov - * @since 2.0 - */ -class PhpManager extends Manager -{ - /** - * @var string the path of the PHP script that contains the authorization data. - * If not set, it will be using 'protected/data/rbac.php' as the data file. - * Make sure this file is writable by the Web server process if the authorization - * needs to be changed. - * @see loadFromFile - * @see saveToFile - */ - public $authFile; - - private $_items = array(); // itemName => item - private $_children = array(); // itemName, childName => child - private $_assignments = array(); // userId, itemName => assignment - - /** - * Initializes the application component. - * This method overrides parent implementation by loading the authorization data - * from PHP script. - */ - public function init() - { - parent::init(); - if ($this->authFile === null) { - $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; - } - $this->load(); - } - - /** - * Performs access check for the specified user. - * @param mixed $userId the user ID. This can be either an integer or a string representing - * @param string $itemName the name of the operation that need access check - * the unique identifier of a user. See [[User::id]]. - * @param array $params name-value pairs that would be passed to biz rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to - * this array, which holds the value of `$userId`. - * @return boolean whether the operations can be performed by the user. - */ - public function checkAccess($userId, $itemName, $params = array()) - { - if (!isset($this->_items[$itemName])) { - return false; - } - /** @var $item Item */ - $item = $this->_items[$itemName]; - Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); - if (!isset($params['userId'])) { - $params['userId'] = $userId; - } - if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { - if (in_array($itemName, $this->defaultRoles)) { - return true; - } - if (isset($this->_assignments[$userId][$itemName])) { - /** @var $assignment Assignment */ - $assignment = $this->_assignments[$userId][$itemName]; - if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { - return true; - } - } - foreach ($this->_children as $parentName => $children) { - if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { - return true; - } - } - } - return false; - } - - /** - * Adds an item as a child of another item. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the item is added successfully - * @throws Exception if either parent or child doesn't exist. - * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. - */ - public function addItemChild($itemName, $childName) - { - if (!isset($this->_items[$childName], $this->_items[$itemName])) { - throw new Exception("Either '$itemName' or '$childName' does not exist."); - } - /** @var $child Item */ - $child = $this->_items[$childName]; - /** @var $item Item */ - $item = $this->_items[$itemName]; - $this->checkItemChildType($item->getType(), $child->getType()); - if ($this->detectLoop($itemName, $childName)) { - throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); - } - if (isset($this->_children[$itemName][$childName])) { - throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); - } - $this->_children[$itemName][$childName] = $this->_items[$childName]; - return true; - } - - /** - * Removes a child from its parent. - * Note, the child item is not deleted. Only the parent-child relationship is removed. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the removal is successful - */ - public function removeItemChild($itemName, $childName) - { - if (isset($this->_children[$itemName][$childName])) { - unset($this->_children[$itemName][$childName]); - return true; - } else { - return false; - } - } - - /** - * Returns a value indicating whether a child exists within a parent. - * @param string $itemName the parent item name - * @param string $childName the child item name - * @return boolean whether the child exists - */ - public function hasItemChild($itemName, $childName) - { - return isset($this->_children[$itemName][$childName]); - } - - /** - * Returns the children of the specified item. - * @param mixed $names the parent item name. This can be either a string or an array. - * The latter represents a list of item names. - * @return Item[] all child items of the parent - */ - public function getItemChildren($names) - { - if (is_string($names)) { - return isset($this->_children[$names]) ? $this->_children[$names] : array(); - } - - $children = array(); - foreach ($names as $name) { - if (isset($this->_children[$name])) { - $children = array_merge($children, $this->_children[$name]); - } - } - return $children; - } - - /** - * Assigns an authorization item to a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called - * for this particular authorization item. - * @param mixed $data additional data associated with this assignment - * @return Assignment the authorization assignment information. - * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user - */ - public function assign($userId, $itemName, $bizRule = null, $data = null) - { - if (!isset($this->_items[$itemName])) { - throw new InvalidParamException("Unknown authorization item '$itemName'."); - } elseif (isset($this->_assignments[$userId][$itemName])) { - throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); - } else { - return $this->_assignments[$userId][$itemName] = new Assignment($this, $userId, $itemName, $bizRule, $data); - } - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - public function revoke($userId, $itemName) - { - if (isset($this->_assignments[$userId][$itemName])) { - unset($this->_assignments[$userId][$itemName]); - return true; - } else { - return false; - } - } - - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. - */ - public function isAssigned($userId, $itemName) - { - return isset($this->_assignments[$userId][$itemName]); - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[User::id]]) - * @param string $itemName the item name - * @return Assignment the item assignment information. Null is returned if - * the item is not assigned to the user. - */ - public function getAssignment($userId, $itemName) - { - return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; - } - - /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[User::id]]) - * @return Assignment[] the item assignment information for the user. An empty array will be - * returned if there is no item assigned to the user. - */ - public function getAssignments($userId) - { - return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : array(); - } - - /** - * Returns the authorization items of the specific type and user. - * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if - * they are not assigned to a user. - * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, - * meaning returning all items regardless of their type. - * @return Item[] the authorization items of the specific type. - */ - public function getItems($userId = null, $type = null) - { - if ($userId === null && $type === null) { - return $this->_items; - } - $items = array(); - if ($userId === null) { - foreach ($this->_items as $name => $item) { - /** @var $item Item */ - if ($item->getType() == $type) { - $items[$name] = $item; - } - } - } elseif (isset($this->_assignments[$userId])) { - foreach ($this->_assignments[$userId] as $assignment) { - /** @var $assignment Assignment */ - $name = $assignment->getItemName(); - if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->getType() == $type)) { - $items[$name] = $this->_items[$name]; - } - } - } - return $items; - } - - /** - * Creates an authorization item. - * An authorization item represents an action permission (e.g. creating a post). - * It has three types: operation, task and role. - * Authorization items form a hierarchy. Higher level items inheirt permissions representing - * by lower level items. - * @param string $name the item name. This must be a unique identifier. - * @param integer $type the item type (0: operation, 1: task, 2: role). - * @param string $description description of the item - * @param string $bizRule business rule associated with the item. This is a piece of - * PHP code that will be executed when [[checkAccess()]] is called for the item. - * @param mixed $data additional data associated with the item. - * @return Item the authorization item - * @throws Exception if an item with the same name already exists - */ - public function createItem($name, $type, $description = '', $bizRule = null, $data = null) - { - if (isset($this->_items[$name])) { - throw new Exception('Unable to add an item whose name is the same as an existing item.'); - } - return $this->_items[$name] = new Item($this, $name, $type, $description, $bizRule, $data); - } - - /** - * Removes the specified authorization item. - * @param string $name the name of the item to be removed - * @return boolean whether the item exists in the storage and has been removed - */ - public function removeItem($name) - { - if (isset($this->_items[$name])) { - foreach ($this->_children as &$children) { - unset($children[$name]); - } - foreach ($this->_assignments as &$assignments) { - unset($assignments[$name]); - } - unset($this->_items[$name]); - return true; - } else { - return false; - } - } - - /** - * Returns the authorization item with the specified name. - * @param string $name the name of the item - * @return Item the authorization item. Null if the item cannot be found. - */ - public function getItem($name) - { - return isset($this->_items[$name]) ? $this->_items[$name] : null; - } - - /** - * Saves an authorization item to persistent storage. - * @param Item $item the item to be saved. - * @param string $oldName the old item name. If null, it means the item name is not changed. - * @throws InvalidParamException if an item with the same name already taken - */ - public function saveItem($item, $oldName = null) - { - if ($oldName !== null && ($newName = $item->getName()) !== $oldName) // name changed - { - if (isset($this->_items[$newName])) { - throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); - } - if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { - unset($this->_items[$oldName]); - $this->_items[$newName] = $item; - if (isset($this->_children[$oldName])) { - $this->_children[$newName] = $this->_children[$oldName]; - unset($this->_children[$oldName]); - } - foreach ($this->_children as &$children) { - if (isset($children[$oldName])) { - $children[$newName] = $children[$oldName]; - unset($children[$oldName]); - } - } - foreach ($this->_assignments as &$assignments) { - if (isset($assignments[$oldName])) { - $assignments[$newName] = $assignments[$oldName]; - unset($assignments[$oldName]); - } - } - } - } - } - - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - public function saveAssignment($assignment) - { - } - - /** - * Saves authorization data into persistent storage. - * If any change is made to the authorization data, please make - * sure you call this method to save the changed data into persistent storage. - */ - public function save() - { - $items = array(); - foreach ($this->_items as $name => $item) { - /** @var $item Item */ - $items[$name] = array( - 'type' => $item->getType(), - 'description' => $item->getDescription(), - 'bizRule' => $item->getBizRule(), - 'data' => $item->getData(), - ); - if (isset($this->_children[$name])) { - foreach ($this->_children[$name] as $child) { - /** @var $child Item */ - $items[$name]['children'][] = $child->getName(); - } - } - } - - foreach ($this->_assignments as $userId => $assignments) { - foreach ($assignments as $name => $assignment) { - /** @var $assignment Assignment */ - if (isset($items[$name])) { - $items[$name]['assignments'][$userId] = array( - 'bizRule' => $assignment->getBizRule(), - 'data' => $assignment->getData(), - ); - } - } - } - - $this->saveToFile($items, $this->authFile); - } - - /** - * Loads authorization data. - */ - public function load() - { - $this->clearAll(); - - $items = $this->loadFromFile($this->authFile); - - foreach ($items as $name => $item) { - $this->_items[$name] = new Item($this, $name, $item['type'], $item['description'], $item['bizRule'], $item['data']); - } - - foreach ($items as $name => $item) { - if (isset($item['children'])) { - foreach ($item['children'] as $childName) { - if (isset($this->_items[$childName])) { - $this->_children[$name][$childName] = $this->_items[$childName]; - } - } - } - if (isset($item['assignments'])) { - foreach ($item['assignments'] as $userId => $assignment) { - $this->_assignments[$userId][$name] = new Assignment($this, $name, $userId, $assignment['bizRule'], $assignment['data']); - } - } - } - } - - /** - * Removes all authorization data. - */ - public function clearAll() - { - $this->clearAssignments(); - $this->_children = array(); - $this->_items = array(); - } - - /** - * Removes all authorization assignments. - */ - public function clearAssignments() - { - $this->_assignments = array(); - } - - /** - * Checks whether there is a loop in the authorization item hierarchy. - * @param string $itemName parent item name - * @param string $childName the name of the child item that is to be added to the hierarchy - * @return boolean whether a loop exists - */ - protected function detectLoop($itemName, $childName) - { - if ($childName === $itemName) { - return true; - } - if (!isset($this->_children[$childName], $this->_items[$itemName])) { - return false; - } - foreach ($this->_children[$childName] as $child) { - /** @var $child Item */ - if ($this->detectLoop($itemName, $child->getName())) { - return true; - } - } - return false; - } - - /** - * Loads the authorization data from a PHP script file. - * @param string $file the file path. - * @return array the authorization data - * @see saveToFile - */ - protected function loadFromFile($file) - { - if (is_file($file)) { - return require($file); - } else { - return array(); - } - } - - /** - * Saves the authorization data to a PHP script file. - * @param array $data the authorization data - * @param string $file the file path. - * @see loadFromFile - */ - protected function saveToFile($data, $file) - { - file_put_contents($file, " + * @author Alexander Kochetov + * @since 2.0 + */ +class Assignment extends Object +{ + private $_auth; + private $_userId; + private $_itemName; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param Manager $auth the authorization manager + * @param mixed $userId user ID (see [[User::id]]) + * @param string $itemName authorization item name + * @param string $bizRule the business rule associated with this assignment + * @param mixed $data additional data for this assignment + */ + public function __construct($auth, $userId, $itemName, $bizRule = null, $data = null) + { + $this->_auth = $auth; + $this->_userId = $userId; + $this->_itemName = $itemName; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * @return mixed user ID (see [[User::id]]) + */ + public function getUserId() + { + return $this->_userId; + } + + /** + * @return string the authorization item name + */ + public function getItemName() + { + return $this->_itemName; + } + + /** + * @return string the business rule associated with this assignment + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this assignment + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveAssignment($this); + } + } + + /** + * @return mixed additional data for this assignment + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value additional data for this assignment + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveAssignment($this); + } + } +} diff --git a/yii/rbac/DbManager.php b/yii/rbac/DbManager.php new file mode 100644 index 0000000..ea6e8ac --- /dev/null +++ b/yii/rbac/DbManager.php @@ -0,0 +1,573 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class DbManager extends Manager +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbManager object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. + */ + public $itemTable = 'tbl_auth_item'; + /** + * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. + */ + public $itemChildTable = 'tbl_auth_item_child'; + /** + * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. + */ + public $assignmentTable = 'tbl_auth_assignment'; + + private $_usingSqlite; + + /** + * Initializes the application component. + * This method overrides the parent implementation by establishing the database connection. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); + } + $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); + parent::init(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[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 biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + $assignments = $this->getAssignments($userId); + return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); + } + + /** + * Performs access check for the specified user. + * This method is internally called by [[checkAccess()]]. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[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 biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' 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($userId, $itemName, $params, $assignments) + { + if (($item = $this->getItem($itemName)) === null) { + return false; + } + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($assignments[$itemName])) { + $assignment = $assignments[$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + $query = new Query; + $parents = $query->select(array('parent')) + ->from($this->itemChildTable) + ->where(array('child' => $itemName)) + ->createCommand($this->db) + ->queryColumn(); + foreach ($parents as $parent) { + if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if ($itemName === $childName) { + throw new Exception("Cannot add '$itemName' as a child of itself."); + } + $query = new Query; + $rows = $query->from($this->itemTable) + ->where(array('or', 'name=:name1', 'name=:name2'), array( + ':name1' => $itemName, + ':name2' => $childName + )) + ->createCommand($this->db) + ->queryAll(); + if (count($rows) == 2) { + if ($rows[0]['name'] === $itemName) { + $parentType = $rows[0]['type']; + $childType = $rows[1]['type']; + } else { + $childType = $rows[0]['type']; + $parentType = $rows[1]['type']; + } + $this->checkItemChildType($parentType, $childType); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + $this->db->createCommand() + ->insert($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName, + )); + return true; + } else { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + return $this->db->createCommand() + ->delete($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName + )) > 0; + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + $query = new Query; + return $query->select(array('parent')) + ->from($this->itemChildTable) + ->where(array( + 'parent' => $itemName, + 'child' => $childName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + $query = new Query; + $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) + ->from(array( + $this->itemTable, + $this->itemChildTable + )) + ->where(array('parent'=>$names)) + ->andWhere('name=child') + ->createCommand($this->db) + ->queryAll(); + $children = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $children[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws Exception if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if ($this->usingSqlite() && $this->getItem($itemName) === null) { + throw new Exception("The item '$itemName' does not exist."); + } + $this->db->createCommand() + ->insert($this->assignmentTable, array( + 'userid' => $userId, + 'itemname' => $itemName, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Assignment($this, $userId, $itemName, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, array( + 'userid' => $userId, + 'itemname' => $itemName + )) > 0; + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($itemName, $userId) + { + $query = new Query; + return $query->select(array('itemname')) + ->from($this->assignmentTable) + ->where(array( + 'userid' => $userId, + 'itemname' => $itemName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + $query = new Query; + $row = $query->from($this->assignmentTable) + ->where(array( + 'userid' => $userId, + 'itemname' => $itemName + )) + ->createCommand($this->db) + ->queryRow(); + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); + } else { + return null; + } + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + $query = new Query; + $rows = $query->from($this->assignmentTable) + ->where(array('userid' => $userId)) + ->createCommand($this->db) + ->queryAll(); + $assignments = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $assignments[$row['itemname']] = new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); + } + return $assignments; + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'bizrule' => $assignment->getBizRule(), + 'data' => serialize($assignment->getData()), + ), array( + 'userid' => $assignment->getUserId(), + 'itemname' => $assignment->getItemName() + )); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + $query = new Query; + if ($userId === null && $type === null) { + $command = $query->from($this->itemTable) + ->createCommand($this->db); + } elseif ($userId === null) { + $command = $query->from($this->itemTable) + ->where(array('type' => $type)) + ->createCommand($this->db); + } elseif ($type === null) { + $command = $query->select(array('name', 'type', 'description', 't1.bizrule', 't1.data')) + ->from(array( + $this->itemTable . ' t1', + $this->assignmentTable . ' t2' + )) + ->where(array('userid' => $userId)) + ->andWhere('name=itemname') + ->createCommand($this->db); + } else { + $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') + ->from(array( + $this->itemTable . ' t1', + $this->assignmentTable . ' t2' + )) + ->where(array( + 'userid' => $userId, + 'type' => $type + )) + ->andWhere('name=itemname') + ->createCommand($this->db); + } + $items = array(); + foreach ($command->queryAll() as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $items[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + $this->db->createCommand() + ->insert($this->itemTable, array( + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if ($this->usingSqlite()) { + $this->db->createCommand() + ->delete($this->itemChildTable, array('or', 'parent=:name1', 'child=:name2'), array( + ':name1' => $name, + ':name2' => $name + )); + $this->db->createCommand()->delete($this->assignmentTable, array('itemname' => $name)); + } + return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + $query = new Query; + $row = $query->from($this->itemTable) + ->where(array('name' => $name)) + ->createCommand($this->db) + ->queryRow(); + + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } else + return null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveItem($item, $oldName = null) + { + if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'parent' => $item->getName(), + ), array( + 'parent' => $oldName, + )); + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'child' => $item->getName(), + ), array( + 'child' => $oldName, + )); + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'itemname' => $item->getName(), + ), array( + 'itemname' => $oldName, + )); + } + + $this->db->createCommand() + ->update($this->itemTable, array( + 'name' => $item->getName(), + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizrule' => $item->getBizRule(), + 'data' => serialize($item->getData()), + ), array( + 'name' => $oldName === null ? $item->getName() : $oldName, + )); + } + + /** + * Saves the authorization data to persistent storage. + */ + public function save() + { + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->db->createCommand()->delete($this->itemChildTable); + $this->db->createCommand()->delete($this->itemTable); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->db->createCommand()->delete($this->assignmentTable); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + foreach ($this->getItemChildren($childName) as $child) { + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * @return boolean whether the database is a SQLite database + */ + protected function usingSqlite() + { + return $this->_usingSqlite; + } +} diff --git a/yii/rbac/Item.php b/yii/rbac/Item.php new file mode 100644 index 0000000..ef56a53 --- /dev/null +++ b/yii/rbac/Item.php @@ -0,0 +1,275 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class Item extends Object +{ + const TYPE_OPERATION = 0; + const TYPE_TASK = 1; + const TYPE_ROLE = 2; + + private $_auth; + private $_type; + private $_name; + private $_description; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param Manager $auth authorization manager + * @param string $name authorization item name + * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). + * @param string $description the description + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data for this item + */ + public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) + { + $this->_type = (int)$type; + $this->_auth = $auth; + $this->_name = $name; + $this->_description = $description; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * Checks to see if the specified item is within the hierarchy starting from this item. + * This method is expected to be internally used by the actual implementations + * of the [[Manager::checkAccess()]]. + * @param string $itemName the name of the item to be checked + * @param array $params the parameters to be passed to business rule evaluation + * @return boolean whether the specified item is within the hierarchy starting from this item. + */ + public function checkAccess($itemName, $params = array()) + { + Yii::trace('Checking permission: ' . $this->_name, __METHOD__); + if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { + if ($this->_name == $itemName) { + return true; + } + foreach ($this->_auth->getItemChildren($this->_name) as $item) { + if ($item->checkAccess($itemName, $params)) { + return true; + } + } + } + return false; + } + + /** + * @return Manager the authorization manager + */ + public function getManager() + { + return $this->_auth; + } + + /** + * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + */ + public function getType() + { + return $this->_type; + } + + /** + * @return string the item name + */ + public function getName() + { + return $this->_name; + } + + /** + * @param string $value the item name + */ + public function setName($value) + { + if ($this->_name !== $value) { + $oldName = $this->_name; + $this->_name = $value; + $this->_auth->saveItem($this, $oldName); + } + } + + /** + * @return string the item description + */ + public function getDescription() + { + return $this->_description; + } + + /** + * @param string $value the item description + */ + public function setDescription($value) + { + if ($this->_description !== $value) { + $this->_description = $value; + $this->_auth->saveItem($this); + } + } + + /** + * @return string the business rule associated with this item + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this item + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveItem($this); + } + } + + /** + * @return mixed the additional data associated with this item + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value the additional data associated with this item + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveItem($this); + } + } + + /** + * Adds a child item. + * @param string $name the name of the child item + * @return boolean whether the item is added successfully + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + * @see Manager::addItemChild + */ + public function addChild($name) + { + return $this->_auth->addItemChild($this->_name, $name); + } + + /** + * Removes a child item. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $name the child item name + * @return boolean whether the removal is successful + * @see Manager::removeItemChild + */ + public function removeChild($name) + { + return $this->_auth->removeItemChild($this->_name, $name); + } + + /** + * Returns a value indicating whether a child exists + * @param string $name the child item name + * @return boolean whether the child exists + * @see Manager::hasItemChild + */ + public function hasChild($name) + { + return $this->_auth->hasItemChild($this->_name, $name); + } + + /** + * Returns the children of this item. + * @return Item[] all child items of this item. + * @see Manager::getItemChildren + */ + public function getChildren() + { + return $this->_auth->getItemChildren($this->_name); + } + + /** + * Assigns this item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item has already been assigned to the user + * @see Manager::assign + */ + public function assign($userId, $bizRule = null, $data = null) + { + return $this->_auth->assign($userId, $this->_name, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether removal is successful + * @see Manager::revoke + */ + public function revoke($userId) + { + return $this->_auth->revoke($userId, $this->_name); + } + + /** + * Returns a value indicating whether this item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether the item has been assigned to the user. + * @see Manager::isAssigned + */ + public function isAssigned($userId) + { + return $this->_auth->isAssigned($userId, $this->_name); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment the item assignment information. Null is returned if + * this item is not assigned to the user. + * @see Manager::getAssignment + */ + public function getAssignment($userId) + { + return $this->_auth->getAssignment($userId, $this->_name); + } +} diff --git a/yii/rbac/Manager.php b/yii/rbac/Manager.php new file mode 100644 index 0000000..5c3c4f6 --- /dev/null +++ b/yii/rbac/Manager.php @@ -0,0 +1,312 @@ +Item). + * @property array $tasks Tasks (name=>Item). + * @property array $operations Operations (name=>Item). + * + * @author Qiang Xue + * @author Alexander Kochetov + * @since 2.0 + */ +abstract class Manager extends Component +{ + /** + * @var boolean Enable error reporting for bizRules. + */ + public $showErrors = false; + + /** + * @var array list of role names that are assigned to all users implicitly. + * These roles do not need to be explicitly assigned to any user. + * When calling [[checkAccess()]], these roles will be checked first. + * For performance reason, you should minimize the number of such roles. + * A typical usage of such roles is to define an 'authenticated' role and associate + * it with a biz rule which checks if the current user is authenticated. + * And then declare 'authenticated' in this property so that it can be applied to + * every authenticated user. + */ + public $defaultRoles = array(); + + /** + * Creates a role. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createRole($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); + } + + /** + * Creates a task. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createTask($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); + } + + /** + * Creates an operation. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createOperation($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); + } + + /** + * Returns roles. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user + * will be returned. Otherwise, all roles will be returned. + * @return Item[] roles (name=>AuthItem) + */ + public function getRoles($userId = null) + { + return $this->getItems($userId, Item::TYPE_ROLE); + } + + /** + * Returns tasks. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user + * will be returned. Otherwise, all tasks will be returned. + * @return Item[] tasks (name=>AuthItem) + */ + public function getTasks($userId = null) + { + return $this->getItems($userId, Item::TYPE_TASK); + } + + /** + * Returns operations. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user + * will be returned. Otherwise, all operations will be returned. + * @return Item[] operations (name=>AuthItem) + */ + public function getOperations($userId = null) + { + return $this->getItems($userId, Item::TYPE_OPERATION); + } + + /** + * Executes the specified business rule. + * @param string $bizRule the business rule to be executed. + * @param array $params parameters passed to [[Manager::checkAccess()]]. + * @param mixed $data additional data associated with the authorization item or assignment. + * @return boolean whether the business rule returns true. + * If the business rule is empty, it will still return true. + */ + public function executeBizRule($bizRule, $params, $data) + { + return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); + } + + /** + * Checks the item types to make sure a child can be added to a parent. + * @param integer $parentType parent item type + * @param integer $childType child item type + * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. + */ + protected function checkItemChildType($parentType, $childType) + { + static $types = array('operation', 'task', 'role'); + if ($parentType < $childType) { + throw new InvalidParamException("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); + } + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + abstract public function checkAccess($userId, $itemName, $params = array()); + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return Item the authorization item + */ + abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + abstract public function removeItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + abstract public function getItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + abstract public function getItem($name); + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + abstract public function saveItem($item, $oldName = null); + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + abstract public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + abstract public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + abstract public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + abstract public function getItemChildren($itemName); + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + abstract public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + abstract public function revoke($userId, $itemName); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + abstract public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + abstract public function getAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Item[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + abstract public function getAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + abstract public function saveAssignment($assignment); + /** + * Removes all authorization data. + */ + abstract public function clearAll(); + /** + * Removes all authorization assignments. + */ + abstract public function clearAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + abstract public function save(); +} diff --git a/yii/rbac/PhpManager.php b/yii/rbac/PhpManager.php new file mode 100644 index 0000000..8a4dbec --- /dev/null +++ b/yii/rbac/PhpManager.php @@ -0,0 +1,516 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class PhpManager extends Manager +{ + /** + * @var string the path of the PHP script that contains the authorization data. + * If not set, it will be using 'protected/data/rbac.php' as the data file. + * Make sure this file is writable by the Web server process if the authorization + * needs to be changed. + * @see loadFromFile + * @see saveToFile + */ + public $authFile; + + private $_items = array(); // itemName => item + private $_children = array(); // itemName, childName => child + private $_assignments = array(); // userId, itemName => assignment + + /** + * Initializes the application component. + * This method overrides parent implementation by loading the authorization data + * from PHP script. + */ + public function init() + { + parent::init(); + if ($this->authFile === null) { + $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; + } + $this->load(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This can be either an integer or a string representing + * @param string $itemName the name of the operation that need access check + * the unique identifier of a user. See [[User::id]]. + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + if (!isset($this->_items[$itemName])) { + return false; + } + /** @var $item Item */ + $item = $this->_items[$itemName]; + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($this->_assignments[$userId][$itemName])) { + /** @var $assignment Assignment */ + $assignment = $this->_assignments[$userId][$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + foreach ($this->_children as $parentName => $children) { + if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if (!isset($this->_items[$childName], $this->_items[$itemName])) { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + /** @var $child Item */ + $child = $this->_items[$childName]; + /** @var $item Item */ + $item = $this->_items[$itemName]; + $this->checkItemChildType($item->getType(), $child->getType()); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + if (isset($this->_children[$itemName][$childName])) { + throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); + } + $this->_children[$itemName][$childName] = $this->_items[$childName]; + return true; + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + if (isset($this->_children[$itemName][$childName])) { + unset($this->_children[$itemName][$childName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + return isset($this->_children[$itemName][$childName]); + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + if (is_string($names)) { + return isset($this->_children[$names]) ? $this->_children[$names] : array(); + } + + $children = array(); + foreach ($names as $name) { + if (isset($this->_children[$name])) { + $children = array_merge($children, $this->_children[$name]); + } + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if (!isset($this->_items[$itemName])) { + throw new InvalidParamException("Unknown authorization item '$itemName'."); + } elseif (isset($this->_assignments[$userId][$itemName])) { + throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); + } else { + return $this->_assignments[$userId][$itemName] = new Assignment($this, $userId, $itemName, $bizRule, $data); + } + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + if (isset($this->_assignments[$userId][$itemName])) { + unset($this->_assignments[$userId][$itemName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : array(); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + if ($userId === null && $type === null) { + return $this->_items; + } + $items = array(); + if ($userId === null) { + foreach ($this->_items as $name => $item) { + /** @var $item Item */ + if ($item->getType() == $type) { + $items[$name] = $item; + } + } + } elseif (isset($this->_assignments[$userId])) { + foreach ($this->_assignments[$userId] as $assignment) { + /** @var $assignment Assignment */ + $name = $assignment->getItemName(); + if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->getType() == $type)) { + $items[$name] = $this->_items[$name]; + } + } + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + if (isset($this->_items[$name])) { + throw new Exception('Unable to add an item whose name is the same as an existing item.'); + } + return $this->_items[$name] = new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if (isset($this->_items[$name])) { + foreach ($this->_children as &$children) { + unset($children[$name]); + } + foreach ($this->_assignments as &$assignments) { + unset($assignments[$name]); + } + unset($this->_items[$name]); + return true; + } else { + return false; + } + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + return isset($this->_items[$name]) ? $this->_items[$name] : null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + * @throws InvalidParamException if an item with the same name already taken + */ + public function saveItem($item, $oldName = null) + { + if ($oldName !== null && ($newName = $item->getName()) !== $oldName) // name changed + { + if (isset($this->_items[$newName])) { + throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); + } + if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { + unset($this->_items[$oldName]); + $this->_items[$newName] = $item; + if (isset($this->_children[$oldName])) { + $this->_children[$newName] = $this->_children[$oldName]; + unset($this->_children[$oldName]); + } + foreach ($this->_children as &$children) { + if (isset($children[$oldName])) { + $children[$newName] = $children[$oldName]; + unset($children[$oldName]); + } + } + foreach ($this->_assignments as &$assignments) { + if (isset($assignments[$oldName])) { + $assignments[$newName] = $assignments[$oldName]; + unset($assignments[$oldName]); + } + } + } + } + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + } + + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save() + { + $items = array(); + foreach ($this->_items as $name => $item) { + /** @var $item Item */ + $items[$name] = array( + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizRule' => $item->getBizRule(), + 'data' => $item->getData(), + ); + if (isset($this->_children[$name])) { + foreach ($this->_children[$name] as $child) { + /** @var $child Item */ + $items[$name]['children'][] = $child->getName(); + } + } + } + + foreach ($this->_assignments as $userId => $assignments) { + foreach ($assignments as $name => $assignment) { + /** @var $assignment Assignment */ + if (isset($items[$name])) { + $items[$name]['assignments'][$userId] = array( + 'bizRule' => $assignment->getBizRule(), + 'data' => $assignment->getData(), + ); + } + } + } + + $this->saveToFile($items, $this->authFile); + } + + /** + * Loads authorization data. + */ + public function load() + { + $this->clearAll(); + + $items = $this->loadFromFile($this->authFile); + + foreach ($items as $name => $item) { + $this->_items[$name] = new Item($this, $name, $item['type'], $item['description'], $item['bizRule'], $item['data']); + } + + foreach ($items as $name => $item) { + if (isset($item['children'])) { + foreach ($item['children'] as $childName) { + if (isset($this->_items[$childName])) { + $this->_children[$name][$childName] = $this->_items[$childName]; + } + } + } + if (isset($item['assignments'])) { + foreach ($item['assignments'] as $userId => $assignment) { + $this->_assignments[$userId][$name] = new Assignment($this, $name, $userId, $assignment['bizRule'], $assignment['data']); + } + } + } + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->_children = array(); + $this->_items = array(); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->_assignments = array(); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + if (!isset($this->_children[$childName], $this->_items[$itemName])) { + return false; + } + foreach ($this->_children[$childName] as $child) { + /** @var $child Item */ + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * Loads the authorization data from a PHP script file. + * @param string $file the file path. + * @return array the authorization data + * @see saveToFile + */ + protected function loadFromFile($file) + { + if (is_file($file)) { + return require($file); + } else { + return array(); + } + } + + /** + * Saves the authorization data to a PHP script file. + * @param array $data the authorization data + * @param string $file the file path. + * @see loadFromFile + */ + protected function saveToFile($data, $file) + { + file_put_contents($file, " Date: Fri, 10 May 2013 15:46:59 +0400 Subject: [PATCH 12/19] fixes #209: do not produce error if not timezone set in case of running yiic from framework dir --- yii/yiic.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/yii/yiic.php b/yii/yiic.php index 7cd0c40..3be0436 100644 --- a/yii/yiic.php +++ b/yii/yiic.php @@ -7,6 +7,10 @@ * @license http://www.yiiframework.com/license/ */ +if(!ini_get('date.timezone')) { + date_default_timezone_set('UTC'); +} + defined('YII_DEBUG') or define('YII_DEBUG', true); // fcgi doesn't have STDIN defined by default From bea449f8a0487a8b75a9b6d2401f5664d51ced96 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 16:11:04 +0400 Subject: [PATCH 13/19] Unit tests --- tests/unit/framework/rbac/ManagerTestBase.php | 248 ++++++++++++++++++++++++++ tests/unit/framework/rbac/PhpManagerTest.php | 33 ++++ 2 files changed, 281 insertions(+) create mode 100644 tests/unit/framework/rbac/ManagerTestBase.php create mode 100644 tests/unit/framework/rbac/PhpManagerTest.php diff --git a/tests/unit/framework/rbac/ManagerTestBase.php b/tests/unit/framework/rbac/ManagerTestBase.php new file mode 100644 index 0000000..03d5354 --- /dev/null +++ b/tests/unit/framework/rbac/ManagerTestBase.php @@ -0,0 +1,248 @@ +auth->createItem($name, $type, $description, $bizRule, $data); + $this->assertTrue($item instanceof Item); + $this->assertEquals($item->type, $type); + $this->assertEquals($item->name, $name); + $this->assertEquals($item->description, $description); + $this->assertEquals($item->bizRule, $bizRule); + $this->assertEquals($item->data, $data); + + // test shortcut + $name2 = 'createUser'; + $item2 = $this->auth->createRole($name2, $description, $bizRule, $data); + $this->assertEquals($item2->type, Item::TYPE_ROLE); + + // test adding an item with the same name + $this->setExpectedException('Exception'); + $this->auth->createItem($name, $type, $description, $bizRule, $data); + } + + public function testGetItem() + { + $this->assertTrue($this->auth->getItem('readPost') instanceof Item); + $this->assertTrue($this->auth->getItem('reader') instanceof Item); + $this->assertNull($this->auth->getItem('unknown')); + } + + public function testRemoveAuthItem() + { + $this->assertTrue($this->auth->getItem('updatePost') instanceof Item); + $this->assertTrue($this->auth->removeItem('updatePost')); + $this->assertNull($this->auth->getItem('updatePost')); + $this->assertFalse($this->auth->removeItem('updatePost')); + } + + public function testChangeItemName() + { + $item = $this->auth->getItem('readPost'); + $this->assertTrue($item instanceof Item); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $item->name = 'readPost2'; + $this->assertNull($this->auth->getItem('readPost')); + $this->assertEquals($this->auth->getItem('readPost2'), $item); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost2')); + } + + public function testAddItemChild() + { + $this->auth->addItemChild('createPost', 'updatePost'); + + // test adding upper level item to lower one + $this->setExpectedException('Exception'); + $this->auth->addItemChild('readPost', 'reader'); + } + + public function testAddItemChild2() + { + // test adding inexistent items + $this->setExpectedException('Exception'); + $this->assertFalse($this->auth->addItemChild('createPost2', 'updatePost')); + } + + public function testRemoveItemChild() + { + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->removeItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->removeItemChild('reader', 'readPost')); + } + + public function testGetItemChildren() + { + $this->assertEquals(array(), $this->auth->getItemChildren('readPost')); + $children = $this->auth->getItemChildren('author'); + $this->assertEquals(3, count($children)); + $this->assertTrue(reset($children) instanceof Item); + } + + public function testAssign() + { + $auth = $this->auth->assign('new user', 'createPost', 'rule', 'data'); + $this->assertTrue($auth instanceof Assignment); + $this->assertEquals($auth->userId, 'new user'); + $this->assertEquals($auth->itemName, 'createPost'); + $this->assertEquals($auth->bizRule, 'rule'); + $this->assertEquals($auth->data, 'data'); + + $this->setExpectedException('Exception'); + $this->auth->assign('new user', 'createPost2', 'rule', 'data'); + } + + public function testRevoke() + { + $this->assertTrue($this->auth->isAssigned('author B', 'author')); + $auth = $this->auth->getAssignment('author B', 'author'); + $this->assertTrue($auth instanceof Assignment); + $this->assertTrue($this->auth->revoke('author B', 'author')); + $this->assertFalse($this->auth->isAssigned('author B', 'author')); + $this->assertFalse($this->auth->revoke('author B', 'author')); + } + + public function testGetAssignments() + { + $this->auth->assign('author B', 'deletePost'); + $auths = $this->auth->getAssignments('author B'); + $this->assertEquals(2, count($auths)); + $this->assertTrue(reset($auths) instanceof Assignment); + } + + public function testGetItems() + { + $this->assertEquals(count($this->auth->getRoles()), 4); + $this->assertEquals(count($this->auth->getOperations()), 4); + $this->assertEquals(count($this->auth->getTasks()), 1); + $this->assertEquals(count($this->auth->getItems()), 9); + + $this->assertEquals(count($this->auth->getItems('author B', null)), 1); + $this->assertEquals(count($this->auth->getItems('author C', null)), 0); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_ROLE)), 1); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_OPERATION)), 0); + } + + public function testClearAll() + { + $this->auth->clearAll(); + $this->assertEquals(count($this->auth->getRoles()), 0); + $this->assertEquals(count($this->auth->getOperations()), 0); + $this->assertEquals(count($this->auth->getTasks()), 0); + $this->assertEquals(count($this->auth->getItems()), 0); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testClearAssignments() + { + $this->auth->clearAssignments(); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testDetectLoop() + { + $this->setExpectedException('Exception'); + $this->auth->addItemChild('readPost', 'readPost'); + } + + public function testExecuteBizRule() + { + $this->assertTrue($this->auth->executeBizRule(null, array(), null)); + $this->assertTrue($this->auth->executeBizRule('return 1==true;', array(), null)); + $this->assertTrue($this->auth->executeBizRule('return $params[0]==$params[1];', array(1, '1'), null)); + $this->assertFalse($this->auth->executeBizRule('invalid', array(), null)); + } + + public function testCheckAccess() + { + $results = array( + 'reader A' => array( + 'createPost' => false, + 'readPost' => true, + 'updatePost' => false, + 'updateOwnPost' => false, + 'deletePost' => false, + ), + 'author B' => array( + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => true, + 'deletePost' => false, + ), + 'editor C' => array( + 'createPost' => false, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => false, + ), + 'admin D' => array( + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => true, + ), + ); + + $params = array('authorID' => 'author B'); + + foreach (array('reader A', 'author B', 'editor C', 'admin D') as $user) { + $params['userID'] = $user; + foreach (array('createPost', 'readPost', 'updatePost', 'updateOwnPost', 'deletePost') as $operation) { + $result = $this->auth->checkAccess($user, $operation, $params); + $this->assertEquals($results[$user][$operation], $result); + } + } + } + + protected function prepareData() + { + $this->auth->createOperation('createPost', 'create a post'); + $this->auth->createOperation('readPost', 'read a post'); + $this->auth->createOperation('updatePost', 'update a post'); + $this->auth->createOperation('deletePost', 'delete a post'); + + $task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'return $params["authorID"]==$params["userID"];'); + $task->addChild('updatePost'); + + $role = $this->auth->createRole('reader'); + $role->addChild('readPost'); + + $role = $this->auth->createRole('author'); + $role->addChild('reader'); + $role->addChild('createPost'); + $role->addChild('updateOwnPost'); + + $role = $this->auth->createRole('editor'); + $role->addChild('reader'); + $role->addChild('updatePost'); + + $role = $this->auth->createRole('admin'); + $role->addChild('editor'); + $role->addChild('author'); + $role->addChild('deletePost'); + + $this->auth->assign('reader A', 'reader'); + $this->auth->assign('author B', 'author'); + $this->auth->assign('editor C', 'editor'); + $this->auth->assign('admin D', 'admin'); + } +} diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php new file mode 100644 index 0000000..2a9fc3f --- /dev/null +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -0,0 +1,33 @@ +getRuntimePath() . '/rbac.php'; + @unlink($authFile); + $this->auth = new PhpManager; + $this->auth->authFile = $authFile; + $this->auth->init(); + $this->prepareData(); + } + + public function tearDown() + { + @unlink($this->auth->authFile); + } + + public function testSaveLoad() + { + $this->auth->save(); + $this->auth->clearAll(); + $this->auth->load(); + $this->testCheckAccess(); + } +} From 4b8bcd43e06e66cc1ee805843960bedb8a516460 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 16:18:01 +0400 Subject: [PATCH 14/19] Unit tests fix --- tests/unit/framework/rbac/PhpManagerTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php index 2a9fc3f..69fdd41 100644 --- a/tests/unit/framework/rbac/PhpManagerTest.php +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\rbac; +use Yii; use yii\rbac\PhpManager; require_once(__DIR__ . '/ManagerTestBase.php'); @@ -10,7 +11,7 @@ class PhpManagerTest extends ManagerTestBase { public function setUp() { - $authFile = \Yii::$app->getRuntimePath() . '/rbac.php'; + $authFile = Yii::$app->getRuntimePath() . '/rbac.php'; @unlink($authFile); $this->auth = new PhpManager; $this->auth->authFile = $authFile; From b9ea7e6fd8b1f6d88dbb786580e91999f351bc6d Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 16:32:28 +0400 Subject: [PATCH 15/19] DbManager code style fix + right Exception type fix --- yii/rbac/DbManager.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yii/rbac/DbManager.php b/yii/rbac/DbManager.php index ea6e8ac..386ce5a 100644 --- a/yii/rbac/DbManager.php +++ b/yii/rbac/DbManager.php @@ -10,9 +10,11 @@ namespace yii\rbac; use Yii; use yii\db\Connection; use yii\db\Query; +use yii\db\Expression; use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidCallException; +use yii\base\InvalidParamException; /** * DbManager represents an authorization manager that stores authorization information in database. @@ -222,8 +224,7 @@ class DbManager extends Manager $this->itemTable, $this->itemChildTable )) - ->where(array('parent'=>$names)) - ->andWhere('name=child') + ->where(array('parent' => $names, 'name' => new Expression('child'))) ->createCommand($this->db) ->queryAll(); $children = array(); @@ -244,12 +245,12 @@ class DbManager extends Manager * for this particular authorization item. * @param mixed $data additional data associated with this assignment * @return Assignment the authorization assignment information. - * @throws Exception if the item does not exist or if the item has already been assigned to the user + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user */ public function assign($userId, $itemName, $bizRule = null, $data = null) { if ($this->usingSqlite() && $this->getItem($itemName) === null) { - throw new Exception("The item '$itemName' does not exist."); + throw new InvalidParamException("The item '$itemName' does not exist."); } $this->db->createCommand() ->insert($this->assignmentTable, array( @@ -385,8 +386,7 @@ class DbManager extends Manager $this->itemTable . ' t1', $this->assignmentTable . ' t2' )) - ->where(array('userid' => $userId)) - ->andWhere('name=itemname') + ->where(array('userid' => $userId, 'name' => new Expression('itemname'))) ->createCommand($this->db); } else { $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') @@ -396,9 +396,9 @@ class DbManager extends Manager )) ->where(array( 'userid' => $userId, - 'type' => $type + 'type' => $type, + 'name' => new Expression('itemname'), )) - ->andWhere('name=itemname') ->createCommand($this->db); } $items = array(); From 9d3a3b3ebef665a68cd83cf291acf72b270141cd Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 17:28:28 +0400 Subject: [PATCH 16/19] DbManager some Db fields renaming --- yii/rbac/DbManager.php | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/yii/rbac/DbManager.php b/yii/rbac/DbManager.php index 386ce5a..f3658b2 100644 --- a/yii/rbac/DbManager.php +++ b/yii/rbac/DbManager.php @@ -254,8 +254,8 @@ class DbManager extends Manager } $this->db->createCommand() ->insert($this->assignmentTable, array( - 'userid' => $userId, - 'itemname' => $itemName, + 'user_id' => $userId, + 'item_name' => $itemName, 'bizrule' => $bizRule, 'data' => serialize($data) )); @@ -272,8 +272,8 @@ class DbManager extends Manager { return $this->db->createCommand() ->delete($this->assignmentTable, array( - 'userid' => $userId, - 'itemname' => $itemName + 'user_id' => $userId, + 'item_name' => $itemName )) > 0; } @@ -286,11 +286,11 @@ class DbManager extends Manager public function isAssigned($itemName, $userId) { $query = new Query; - return $query->select(array('itemname')) + return $query->select(array('item_name')) ->from($this->assignmentTable) ->where(array( - 'userid' => $userId, - 'itemname' => $itemName + 'user_id' => $userId, + 'item_name' => $itemName )) ->createCommand($this->db) ->queryScalar() !== false; @@ -308,8 +308,8 @@ class DbManager extends Manager $query = new Query; $row = $query->from($this->assignmentTable) ->where(array( - 'userid' => $userId, - 'itemname' => $itemName + 'user_id' => $userId, + 'item_name' => $itemName )) ->createCommand($this->db) ->queryRow(); @@ -317,7 +317,7 @@ class DbManager extends Manager if (($data = @unserialize($row['data'])) === false) { $data = null; } - return new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); + return new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); } else { return null; } @@ -333,7 +333,7 @@ class DbManager extends Manager { $query = new Query; $rows = $query->from($this->assignmentTable) - ->where(array('userid' => $userId)) + ->where(array('user_id' => $userId)) ->createCommand($this->db) ->queryAll(); $assignments = array(); @@ -341,7 +341,7 @@ class DbManager extends Manager if (($data = @unserialize($row['data'])) === false) { $data = null; } - $assignments[$row['itemname']] = new Assignment($this, $row['userid'], $row['itemname'], $row['bizrule'], $data); + $assignments[$row['item_name']] = new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); } return $assignments; } @@ -357,8 +357,8 @@ class DbManager extends Manager 'bizrule' => $assignment->getBizRule(), 'data' => serialize($assignment->getData()), ), array( - 'userid' => $assignment->getUserId(), - 'itemname' => $assignment->getItemName() + 'user_id' => $assignment->getUserId(), + 'item_name' => $assignment->getItemName() )); } @@ -386,7 +386,7 @@ class DbManager extends Manager $this->itemTable . ' t1', $this->assignmentTable . ' t2' )) - ->where(array('userid' => $userId, 'name' => new Expression('itemname'))) + ->where(array('user_id' => $userId, 'name' => new Expression('item_name'))) ->createCommand($this->db); } else { $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') @@ -395,9 +395,9 @@ class DbManager extends Manager $this->assignmentTable . ' t2' )) ->where(array( - 'userid' => $userId, + 'user_id' => $userId, 'type' => $type, - 'name' => new Expression('itemname'), + 'name' => new Expression('item_name'), )) ->createCommand($this->db); } @@ -452,7 +452,7 @@ class DbManager extends Manager ':name1' => $name, ':name2' => $name )); - $this->db->createCommand()->delete($this->assignmentTable, array('itemname' => $name)); + $this->db->createCommand()->delete($this->assignmentTable, array('item_name' => $name)); } return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; } @@ -501,9 +501,9 @@ class DbManager extends Manager )); $this->db->createCommand() ->update($this->assignmentTable, array( - 'itemname' => $item->getName(), + 'item_name' => $item->getName(), ), array( - 'itemname' => $oldName, + 'item_name' => $oldName, )); } From f554d3ec2ebfa847dc2be5fe59d9116ea44cc9f4 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 21:41:07 +0400 Subject: [PATCH 17/19] RBAC sql files --- yii/rbac/schema-mssql.sql | 43 +++++++++++++++++++++++++++++++++++++++++++ yii/rbac/schema-mysql.sql | 43 +++++++++++++++++++++++++++++++++++++++++++ yii/rbac/schema-oci.sql | 43 +++++++++++++++++++++++++++++++++++++++++++ yii/rbac/schema-pgsql.sql | 43 +++++++++++++++++++++++++++++++++++++++++++ yii/rbac/schema-sqlite.sql | 43 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 yii/rbac/schema-mssql.sql create mode 100644 yii/rbac/schema-mysql.sql create mode 100644 yii/rbac/schema-oci.sql create mode 100644 yii/rbac/schema-pgsql.sql create mode 100644 yii/rbac/schema-sqlite.sql diff --git a/yii/rbac/schema-mssql.sql b/yii/rbac/schema-mssql.sql new file mode 100644 index 0000000..f02b261 --- /dev/null +++ b/yii/rbac/schema-mssql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists [tbl_auth_assignment]; +drop table if exists [tbl_auth_item_child]; +drop table if exists [tbl_auth_item]; + +create table [tbl_auth_item] +( + [name] varchar(64) not null, + [type] integer not null, + [description] text, + [bizrule] text, + [data] text, + primary key ([name]) +); + +create table [tbl_auth_item_child] +( + [parent] varchar(64) not null, + [child] varchar(64) not null, + primary key ([parent],[child]), + foreign key ([parent]) references [tbl_auth_item] ([name]) on delete cascade on update cascade, + foreign key ([child]) references [tbl_auth_item] ([name]) on delete cascade on update cascade +); + +create table [tbl_auth_assignment] +( + [item_name] varchar(64) not null, + [user_id] varchar(64) not null, + [bizrule] text, + [data] text, + primary key ([item_name],[user_id]), + foreign key ([item_name]) references [tbl_auth_item] ([name]) on delete cascade on update cascade +); diff --git a/yii/rbac/schema-mysql.sql b/yii/rbac/schema-mysql.sql new file mode 100644 index 0000000..687a436 --- /dev/null +++ b/yii/rbac/schema-mysql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists `tbl_auth_assignment`; +drop table if exists `tbl_auth_item_child`; +drop table if exists `tbl_auth_item`; + +create table `tbl_auth_item` +( + `name` varchar(64) not null, + `type` integer not null, + `description` text, + `bizrule` text, + `data` text, + primary key (`name`) +) engine InnoDB; + +create table `tbl_auth_item_child` +( + `parent` varchar(64) not null, + `child` varchar(64) not null, + primary key (`parent`,`child`), + foreign key (`parent`) references `tbl_auth_item` (`name`) on delete cascade on update cascade, + foreign key (`child`) references `tbl_auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; + +create table `tbl_auth_assignment` +( + `item_name` varchar(64) not null, + `user_id` varchar(64) not null, + `bizrule` text, + `data` text, + primary key (`item_name`,`user_id`), + foreign key (`item_name`) references `tbl_auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; diff --git a/yii/rbac/schema-oci.sql b/yii/rbac/schema-oci.sql new file mode 100644 index 0000000..623fba2 --- /dev/null +++ b/yii/rbac/schema-oci.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists "tbl_auth_assignment"; +drop table if exists "tbl_auth_item_child"; +drop table if exists "tbl_auth_item"; + +create table "tbl_auth_item" +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table "tbl_auth_item_child" +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references "tbl_auth_item" ("name") on delete cascade on update cascade, + foreign key ("child") references "tbl_auth_item" ("name") on delete cascade on update cascade +); + +create table "tbl_auth_assignment" +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" text, + "data" text, + primary key ("item_name","user_id"), + foreign key ("item_name") references "tbl_auth_item" ("name") on delete cascade on update cascade +); diff --git a/yii/rbac/schema-pgsql.sql b/yii/rbac/schema-pgsql.sql new file mode 100644 index 0000000..623fba2 --- /dev/null +++ b/yii/rbac/schema-pgsql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists "tbl_auth_assignment"; +drop table if exists "tbl_auth_item_child"; +drop table if exists "tbl_auth_item"; + +create table "tbl_auth_item" +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table "tbl_auth_item_child" +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references "tbl_auth_item" ("name") on delete cascade on update cascade, + foreign key ("child") references "tbl_auth_item" ("name") on delete cascade on update cascade +); + +create table "tbl_auth_assignment" +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" text, + "data" text, + primary key ("item_name","user_id"), + foreign key ("item_name") references "tbl_auth_item" ("name") on delete cascade on update cascade +); diff --git a/yii/rbac/schema-sqlite.sql b/yii/rbac/schema-sqlite.sql new file mode 100644 index 0000000..32eb363 --- /dev/null +++ b/yii/rbac/schema-sqlite.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists 'tbl_auth_assignment'; +drop table if exists 'tbl_auth_item_child'; +drop table if exists 'tbl_auth_item'; + +create table 'tbl_auth_item' +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table 'tbl_auth_item_child' +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references 'tbl_auth_item' ("name") on delete cascade on update cascade, + foreign key ("child") references 'tbl_auth_item' ("name") on delete cascade on update cascade +); + +create table 'tbl_auth_assignment' +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" text, + "data" text, + primary key ("item_name","user_id"), + foreign key ("item_name") references 'tbl_auth_item' ("name") on delete cascade on update cascade +); From 176366a24d81245ce4ba9e255bc62646ef4248c7 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Fri, 10 May 2013 21:49:54 +0400 Subject: [PATCH 18/19] RBAC sql files identation fix --- yii/rbac/schema-mssql.sql | 4 ++-- yii/rbac/schema-mysql.sql | 4 ++-- yii/rbac/schema-oci.sql | 4 ++-- yii/rbac/schema-pgsql.sql | 4 ++-- yii/rbac/schema-sqlite.sql | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/yii/rbac/schema-mssql.sql b/yii/rbac/schema-mssql.sql index f02b261..a1170b1 100644 --- a/yii/rbac/schema-mssql.sql +++ b/yii/rbac/schema-mssql.sql @@ -34,8 +34,8 @@ create table [tbl_auth_item_child] create table [tbl_auth_assignment] ( - [item_name] varchar(64) not null, - [user_id] varchar(64) not null, + [item_name] varchar(64) not null, + [user_id] varchar(64) not null, [bizrule] text, [data] text, primary key ([item_name],[user_id]), diff --git a/yii/rbac/schema-mysql.sql b/yii/rbac/schema-mysql.sql index 687a436..aa8015b 100644 --- a/yii/rbac/schema-mysql.sql +++ b/yii/rbac/schema-mysql.sql @@ -34,8 +34,8 @@ create table `tbl_auth_item_child` create table `tbl_auth_assignment` ( - `item_name` varchar(64) not null, - `user_id` varchar(64) not null, + `item_name` varchar(64) not null, + `user_id` varchar(64) not null, `bizrule` text, `data` text, primary key (`item_name`,`user_id`), diff --git a/yii/rbac/schema-oci.sql b/yii/rbac/schema-oci.sql index 623fba2..16b7964 100644 --- a/yii/rbac/schema-oci.sql +++ b/yii/rbac/schema-oci.sql @@ -34,8 +34,8 @@ create table "tbl_auth_item_child" create table "tbl_auth_assignment" ( - "item_name" varchar(64) not null, - "user_id" varchar(64) not null, + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, "bizrule" text, "data" text, primary key ("item_name","user_id"), diff --git a/yii/rbac/schema-pgsql.sql b/yii/rbac/schema-pgsql.sql index 623fba2..16b7964 100644 --- a/yii/rbac/schema-pgsql.sql +++ b/yii/rbac/schema-pgsql.sql @@ -34,8 +34,8 @@ create table "tbl_auth_item_child" create table "tbl_auth_assignment" ( - "item_name" varchar(64) not null, - "user_id" varchar(64) not null, + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, "bizrule" text, "data" text, primary key ("item_name","user_id"), diff --git a/yii/rbac/schema-sqlite.sql b/yii/rbac/schema-sqlite.sql index 32eb363..668196e 100644 --- a/yii/rbac/schema-sqlite.sql +++ b/yii/rbac/schema-sqlite.sql @@ -34,8 +34,8 @@ create table 'tbl_auth_item_child' create table 'tbl_auth_assignment' ( - "item_name" varchar(64) not null, - "user_id" varchar(64) not null, + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, "bizrule" text, "data" text, primary key ("item_name","user_id"), From 1c1b2da5b1ff08a59cc30cd6465252d3d7c72f88 Mon Sep 17 00:00:00 2001 From: resurtm Date: Sat, 11 May 2013 00:17:50 +0600 Subject: [PATCH 19/19] Add exception classes hierarchy UML diagram. --- docs/internals/exception_hierarchy.png | Bin 0 -> 64306 bytes docs/internals/exception_hierarchy.vsd | Bin 0 -> 151040 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/internals/exception_hierarchy.png create mode 100644 docs/internals/exception_hierarchy.vsd diff --git a/docs/internals/exception_hierarchy.png b/docs/internals/exception_hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..2c43e03793b16102385a46f1d00d84ab67da9f2a GIT binary patch literal 64306 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
-Ddu)d)iG-k-_Sh~}AqlIq10092mPhzU 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 C1i6*~ literal 0 HcmV?d00001