diff --git a/docs/guide/authorization.md b/docs/guide/authorization.md index 1bb8d3513d..7be120d2f0 100644 --- a/docs/guide/authorization.md +++ b/docs/guide/authorization.md @@ -86,209 +86,3 @@ Role based access control (RBAC) Role based access control is very flexible approach to controlling access that is a perfect match for complex systems where permissions are customizable. - -### Using file-based config for RBAC - -In order to start using it some extra steps are required. First of all we need to configure `authManager` application -component in application config file (`web.php` or `main.php` depending on template you've used): - -```php -'authManager' => [ - 'class' => 'app\components\PhpManager', - 'defaultRoles' => ['guest'], -], -``` - -Often use role is stored in the same database table as other user data. In this case we may defined it by creating our -own component (`app/components/PhpManager.php`): - -```php -user->isGuest) { - // we suppose that user's role is stored in identity - $this->assign(Yii::$app->user->identity->id, Yii::$app->user->identity->role); - } - } -} -``` - -Now create custom rule class: - -```php -namespace app\rbac; - -use yii\rbac\Rule; -use Yii; - -class NotGuestRule extends Rule -{ - public $name = 'notGuestRule'; - - public function execute($params, $data) - { - return !Yii::$app->user->isGuest; - } -} -``` - -Then create permissions hierarchy in `@app/data/rbac.php`: - -```php - [ - $notGuest->name => serialize($notGuest), - ], - 'items' => [ - // HERE ARE YOUR MANAGEMENT TASKS - 'manageThing0' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'ruleName' => null, 'data' => null], - 'manageThing1' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'ruleName' => null, 'data' => null], - 'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'ruleName' => null, 'data' => null], - 'manageThing3' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'ruleName' => null, 'data' => null], - - // AND THE ROLES - 'guest' => [ - 'type' => Item::TYPE_ROLE, - 'description' => 'Guest', - 'ruleName' => null, - 'data' => null - ], - - 'user' => [ - 'type' => Item::TYPE_ROLE, - 'description' => 'User', - 'children' => [ - 'guest', - 'manageThing0', // User can edit thing0 - ], - 'ruleName' => $notGuest->name, - 'data' => null - ], - - 'moderator' => [ - 'type' => Item::TYPE_ROLE, - 'description' => 'Moderator', - 'children' => [ - 'user', // Can manage all that user can - 'manageThing1', // and also thing1 - ], - 'ruleName' => null, - 'data' => null - ], - - 'admin' => [ - 'type' => Item::TYPE_ROLE, - 'description' => 'Admin', - 'children' => [ - 'moderator', // can do all the stuff that moderator can - 'manageThing2', // and also manage thing2 - ], - 'ruleName' => null, - 'data' => null - ], - - 'godmode' => [ - 'type' => Item::TYPE_ROLE, - 'description' => 'Super admin', - 'children' => [ - 'admin', // can do all that admin can - 'manageThing3', // and also thing3 - ], - 'ruleName' => null, - 'data' => null - ], - ], -]; -``` - -Now you can specify roles from RBAC in controller's access control configuration: - -```php -public function behaviors() -{ - return [ - 'access' => [ - 'class' => 'yii\filters\AccessControl', - 'except' => ['something'], - 'rules' => [ - [ - 'allow' => true, - 'roles' => ['manageThing1'], - ], - ], - ], - ]; -} -``` - -Another way is to call [[yii\web\User::checkAccess()]] where appropriate. - -### Using DB-based storage for RBAC - -Storing RBAC hierarchy in database is less efficient performancewise but is much more flexible. It is easier to create -a good management UI for it so in case you need permissions structure that is managed by end user DB is your choice. - -In order to get started you need to configure database connection in `db` component. After it is done [get `schema-*.sql` -file for your database](https://github.com/yiisoft/yii2/tree/master/framework/rbac) and execute it. - -Next step is to configure `authManager` application component in application config file (`web.php` or `main.php` -depending on template you've used): - -```php -'authManager' => [ - 'class' => 'yii\rbac\DbManager', - 'defaultRoles' => ['guest'], -], -``` - -TBD - -### How it works - -TBD: write about how it works with pictures :) - -### Avoiding too much RBAC - -In order to keep auth hierarchy simple and efficient you should avoid creating and using too much nodes. Most of the time -simple checks could be used instead. For example such code that uses RBAC: - -```php -public function editArticle($id) -{ - $article = Article::findOne($id); - if (!$article) { - throw new NotFoundHttpException; - } - if (!\Yii::$app->user->checkAccess('edit_article', ['article' => $article])) { - throw new ForbiddenHttpException; - } - // ... -} -``` - -can be replaced with simpler code that doesn't use RBAC: - -```php -public function editArticle($id) -{ - $article = Article::findOne(['id' => $id, 'author_id' => \Yii::$app->user->id]); - if (!$article) { - throw new NotFoundHttpException; - } - // ... -} -``` diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 855337e2c3..24de70a45e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -282,6 +282,7 @@ Yii Framework 2 Change Log - Chg: `yii\log\Logger` is split into `yii\log\Logger` and `yii\log\Dispatcher`. (qiangxue) - Chg: Moved all filter classes to namespace `yii\filters` (qiangxue) - Chg: Removed `Application::preload` in favor of `Application::bootstrap` (qiangxue) +- Chg: Re-implemented RBAC by following more closely to the original NIST RBAC model. Dropped `yii\rbac\PhpManager`. (qiangxue) - New #66: [Auth client library](https://github.com/yiisoft/yii2-authclient) OpenId, OAuth1, OAuth2 clients (klimov-paul) - New #303: Added built-in support for REST API (qiangxue) - New #503: Added `yii\di\Container` and `yii\di\ServiceLocator` (qiangxue) diff --git a/framework/rbac/Assignment.php b/framework/rbac/Assignment.php index ab7c3eb92d..2ae5efc538 100644 --- a/framework/rbac/Assignment.php +++ b/framework/rbac/Assignment.php @@ -12,9 +12,8 @@ use yii\base\Object; /** * Assignment represents an assignment of a role to a user. - * It includes additional assignment information such as [[ruleName]] and [[data]]. - * Do not create a Assignment instance using the 'new' operator. - * Instead, call [[Manager::assign()]]. + * + * It includes additional assignment information including [[ruleName]] and [[data]]. * * @author Qiang Xue * @author Alexander Kochetov @@ -23,33 +22,15 @@ use yii\base\Object; class Assignment extends Object { /** - * @var Manager the auth manager of this item - */ - public $manager; - /** - * @var string name of the rule associated with this assignment - */ - public $ruleName; - /** - * @var mixed additional data for this assignment - */ - public $data; - /** - * @var mixed user ID (see [[\yii\web\User::id]]). Do not modify this property after it is populated. - * To modify the user ID of an assignment, you must remove the assignment and create a new one. + * @var string|integer user ID (see [[\yii\web\User::id]]) */ public $userId; /** - * @return string the authorization item name. Do not modify this property after it is populated. - * To modify the item name of an assignment, you must remove the assignment and create a new one. + * @return string the role name */ - public $itemName; - + public $roleName; /** - * Saves the changes to an authorization assignment. + * @var integer UNIX timestamp representing the assignment creation time */ - public function save() - { - $this->manager->saveAssignment($this); - } + public $createdAt; } diff --git a/framework/rbac/BaseManager.php b/framework/rbac/BaseManager.php new file mode 100644 index 0000000000..96356affa0 --- /dev/null +++ b/framework/rbac/BaseManager.php @@ -0,0 +1,210 @@ + + * @since 2.0 + */ +abstract class BaseManager extends Component implements ManagerInterface +{ + /** + * @var array a list of role names that are assigned to every user automatically without calling [[assign()]]. + */ + public $defaultRoles = []; + + /** + * Returns the named auth item. + * @param string $name the auth item name. + * @return Item the auth item corresponding to the specified name. Null is returned if no such item. + */ + abstract protected function getItem($name); + + /** + * Returns the items of the specified type. + * @param integer $type the auth item type (either [[Item::TYPE_ROLE]] or [[Item::TYPE_PERMISSION]] + * @return Item[] the auth items of the specified type. + */ + abstract protected function getItems($type); + + /** + * Adds an auth item to the RBAC system. + * @param Item $item + * @return boolean whether the auth item is successfully added to the system + * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique) + */ + abstract protected function addItem($item); + + /** + * Adds a rule to the RBAC system. + * @param Rule $rule + * @return boolean whether the rule is successfully added to the system + * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique) + */ + abstract protected function addRule($rule); + + /** + * Removes an auth item from the RBAC system. + * @param Item $item + * @return boolean whether the role or permission is successfully removed + * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique) + */ + abstract protected function removeItem($item); + + /** + * Removes a rule from the RBAC system. + * @param Rule $rule + * @return boolean whether the rule is successfully removed + * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique) + */ + abstract protected function removeRule($rule); + + /** + * Updates an auth item in the RBAC system. + * @param string $name the old name of the auth item + * @param Item $item + * @return boolean whether the auth item is successfully updated + * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique) + */ + abstract protected function updateItem($name, $item); + + /** + * Updates a rule to the RBAC system. + * @param string $name the old name of the rule + * @param Rule $rule + * @return boolean whether the rule is successfully updated + * @throws \Exception if data validation or saving fails (such as the name of the rule is not unique) + */ + abstract protected function updateRule($name, $rule); + + /** + * @inheritdoc + */ + public function createRole($name) + { + $role = new Role; + $role->name = $name; + return $role; + } + + /** + * @inheritdoc + */ + public function createPermission($name) + { + $permission = new Permission(); + $permission->name = $name; + return $permission; + } + + /** + * @inheritdoc + */ + public function add($object) + { + if ($object instanceof Item) { + return $this->addItem($object); + } elseif ($object instanceof Rule) { + return $this->addRule($object); + } else { + throw new InvalidParamException("Adding unsupported object type."); + } + } + + /** + * @inheritdoc + */ + public function remove($object) + { + if ($object instanceof Item) { + return $this->removeItem($object); + } elseif ($object instanceof Rule) { + return $this->removeRule($object); + } else { + throw new InvalidParamException("Removing unsupported object type."); + } + } + + /** + * @inheritdoc + */ + public function update($name, $object) + { + if ($object instanceof Item) { + return $this->updateItem($name, $object); + } elseif ($object instanceof Rule) { + return $this->updateRule($name, $object); + } else { + throw new InvalidParamException("Updating unsupported object type."); + } + } + + /** + * @inheritdoc + */ + public function getRole($name) + { + $item = $this->getItem($name); + return $item instanceof Item && $item->type == Item::TYPE_ROLE ? $item : null; + } + + /** + * @inheritdoc + */ + public function getPermission($name) + { + $item = $this->getItem($name); + return $item instanceof Item && $item->type == Item::TYPE_PERMISSION ? $item : null; + } + + /** + * @inheritdoc + */ + public function getRoles() + { + return $this->getItems(Item::TYPE_ROLE); + } + + /** + * @inheritdoc + */ + public function getPermissions() + { + return $this->getItems(Item::TYPE_PERMISSION); + } + + /** + * Executes the rule associated with the specified auth item. + * + * If the item does not specify a rule, this method will return true. Otherwise, it will + * return the value of [[Rule::execute()]]. + * + * @param Item $item the auth item that needs to execute its rule + * @param array $params parameters passed to [[ManagerInterface::checkAccess()]] and will be passed to the rule + * @return boolean the return value of [[Rule::execute()]]. If the auth item does not specify a rule, true will be returned. + * @throws InvalidConfigException if the auth item has an invalid rule. + */ + protected function executeRule($item, $params) + { + if ($item->ruleName === null) { + return true; + } + $rule = $this->getRule($item->ruleName); + if ($rule instanceof Rule) { + return $rule->execute($item, $params); + } else { + throw new InvalidConfigException("Rule not found: {$item->ruleName}"); + } + } +} diff --git a/framework/rbac/DbManager.php b/framework/rbac/DbManager.php index 883a8317e4..9ecfc770a3 100644 --- a/framework/rbac/DbManager.php +++ b/framework/rbac/DbManager.php @@ -11,7 +11,6 @@ use Yii; use yii\db\Connection; use yii\db\Query; use yii\db\Expression; -use yii\base\Exception; use yii\base\InvalidCallException; use yii\base\InvalidParamException; use yii\di\Instance; @@ -24,14 +23,11 @@ use yii\di\Instance; * the three tables used to store the authorization data by setting [[itemTable]], * [[itemChildTable]] and [[assignmentTable]]. * - * @property Item[] $items The authorization items of the specific type. This property is read-only. - * @property Rule[] $rules This property is read-only. - * * @author Qiang Xue * @author Alexander Kochetov * @since 2.0 */ -class DbManager extends Manager +class DbManager extends BaseManager { /** * @var Connection|string the DB connection object or the application component ID of the DB connection. @@ -60,7 +56,6 @@ class DbManager extends Manager */ public $ruleTable = '{{%auth_rule}}'; - private $_usingSqlite; /** * Initializes the application component. @@ -69,613 +64,584 @@ class DbManager extends Manager public function init() { parent::init(); + $this->db = Instance::ensure($this->db, Connection::className()); - $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); } /** - * 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 [[\yii\web\User::id]]. - * @param string $itemName the name of the operation that need access check - * @param array $params name-value pairs that would be passed to rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, - * which holds the value of `$userId`. - * @return boolean whether the operations can be performed by the user. + * @inheritdoc */ - public function checkAccess($userId, $itemName, $params = []) + public function checkAccess($userId, $permissionName, $params = []) { $assignments = $this->getAssignments($userId); - - return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); + if (!isset($params['user'])) { + $params['user'] = $userId; + } + return $this->checkAccessRecursive($userId, $permissionName, $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 + * @param string|integer $user the user ID. This should can be either an integer or a string representing * the unique identifier of a user. See [[\yii\web\User::id]]. * @param string $itemName the name of the operation that need access check * @param array $params name-value pairs that would be passed to rules associated - * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * with the tasks and roles assigned to the user. A param with name 'user' is added to this array, * which holds the value of `$userId`. * @param Assignment[] $assignments the assignments to the specified user * @return boolean whether the operations can be performed by the user. */ - protected function checkAccessRecursive($userId, $itemName, $params, $assignments) + protected function checkAccessRecursive($user, $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; + + Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__); + + if (!$this->executeRule($item, $params)) { + return false; + } + + if (isset($this->defaultRoles[$itemName]) || isset($assignments[$itemName])) { + return true; } - if ($this->executeRule($item->ruleName, $params, $item->data)) { - if (in_array($itemName, $this->defaultRoles)) { + + $query = new Query; + $parents = $query->select(['parent']) + ->from($this->itemChildTable) + ->where(['child' => $itemName]) + ->column($this->db); + foreach ($parents as $parent) { + if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) { return true; } - if (isset($assignments[$itemName])) { - $assignment = $assignments[$itemName]; - if ($this->executeRule($assignment->ruleName, $params, $assignment->data)) { - return true; - } - } - $query = new Query; - $parents = $query->select(['parent']) - ->from($this->itemChildTable) - ->where(['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. + * @inheritdoc */ - public function addItemChild($itemName, $childName) + protected function getItem($name) { - if ($itemName === $childName) { - throw new Exception("Cannot add '$itemName' as a child of itself."); + $row = (new Query)->from($this->itemTable) + ->where(['name' => $name]) + ->one($this->db); + + if ($row === false) { + return null; } - $query = new Query; - $rows = $query->from($this->itemTable) - ->where(['or', 'name=:name1', 'name=:name2'], [':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, ['parent' => $itemName, 'child' => $childName]) - ->execute(); - return true; - } else { - throw new Exception("Either '$itemName' or '$childName' does not exist."); + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; } + + return $this->populateItem($row); } /** - * 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 + * Returns a value indicating whether the database supports cascading update and delete. + * The default implementation will return false for SQLite database and true for all other databases. + * @return boolean whether the database supports cascading update and delete. */ - public function removeItemChild($itemName, $childName) + protected function supportsCascadeUpdate() { - return $this->db->createCommand() - ->delete($this->itemChildTable, ['parent' => $itemName, 'child' => $childName]) - ->execute() > 0; + return strncmp($this->db->getDriverName(), 'sqlite', 6) !== 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 + * @inheritdoc */ - public function hasItemChild($itemName, $childName) + protected function addItem($item) { - $query = new Query; + $time = time(); + if ($item->createdAt === null) { + $item->createdAt = $time; + } + if ($item->updatedAt === null) { + $item->updatedAt = $time; + } + $this->db->createCommand() + ->insert($this->itemTable, [ + 'name' => $item->name, + 'type' => $item->type, + 'description' => $item->description, + 'rule_name' => $item->ruleName, + 'data' => $item->data === null ? null : serialize($item->data), + 'created_at' => $item->createdAt, + 'updated_at' => $item->updatedAt, + ])->execute(); - return $query->select(['parent']) - ->from($this->itemChildTable) - ->where(['parent' => $itemName, 'child' => $childName]) - ->createCommand($this->db) - ->queryScalar() !== false; + return true; } /** - * 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 + * @inheritdoc */ - public function getItemChildren($names) + protected function removeItem($item) { - $query = new Query; - $rows = $query->select(['name', 'type', 'description', 'rule_name', 'data']) - ->from([$this->itemTable, $this->itemChildTable]) - ->where(['parent' => $names, 'name' => new Expression('child')]) - ->createCommand($this->db) - ->queryAll(); - $children = []; - foreach ($rows as $row) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - $children[$row['name']] = new Item([ - 'manager' => $this, - 'name' => $row['name'], - 'type' => $row['type'], - 'description' => $row['description'], - 'ruleName' => $row['rule_name'], - 'data' => $data, - ]); + if (!$this->supportsCascadeUpdate()) { + $this->db->createCommand() + ->delete($this->itemChildTable, ['or', 'parent=:name', 'child=:name'], [':name' => $item->name]) + ->execute(); + $this->db->createCommand() + ->delete($this->assignmentTable, ['item_name' => $item->name]) + ->execute(); } - return $children; + $this->db->createCommand() + ->delete($this->itemTable, ['name' => $item->name]) + ->execute(); + + return true; } /** - * Assigns an authorization item to a user. - * - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @param string $ruleName 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 + * @inheritdoc */ - public function assign($userId, $itemName, $ruleName = null, $data = null) + protected function updateItem($name, $item) { - if ($this->usingSqlite() && $this->getItem($itemName) === null) { - throw new InvalidParamException("The item '$itemName' does not exist."); + if (!$this->supportsCascadeUpdate() && $item->name !== $name) { + $this->db->createCommand() + ->update($this->itemChildTable, ['parent' => $item->name], ['parent' => $name]) + ->execute(); + $this->db->createCommand() + ->update($this->itemChildTable, ['child' => $item->name], ['child' => $name]) + ->execute(); + $this->db->createCommand() + ->update($this->assignmentTable, ['item_name' => $item->name], ['item_name' => $name]) + ->execute(); } + + $item->updatedAt = time(); + $this->db->createCommand() - ->insert($this->assignmentTable, [ - 'user_id' => $userId, - 'item_name' => $itemName, - 'rule_name' => $ruleName, - 'data' => $data === null ? null : serialize($data), - ]) - ->execute(); + ->update($this->itemTable, [ + 'name' => $item->name, + 'description' => $item->description, + 'rule_name' => $item->ruleName, + 'data' => $item->data === null ? null : serialize($item->data), + 'updated_at' => $item->updatedAt, + ], [ + 'name' => $name, + ])->execute(); - return new Assignment([ - 'manager' => $this, - 'userId' => $userId, - 'itemName' => $itemName, - 'ruleName' => $ruleName, - 'data' => $data, - ]); + return true; } /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful + * @inheritdoc */ - public function revoke($userId, $itemName) + protected function addRule($rule) { - return $this->db->createCommand() - ->delete($this->assignmentTable, ['user_id' => $userId, 'item_name' => $itemName]) - ->execute() > 0; + $time = time(); + if ($rule->createdAt === null) { + $rule->createdAt = $time; + } + if ($rule->updatedAt === null) { + $rule->updatedAt = $time; + } + $this->db->createCommand() + ->insert($this->ruleTable, [ + 'name' => $rule->name, + 'data' => serialize($rule), + 'created_at' => $rule->createdAt, + 'updated_at' => $rule->updatedAt, + ])->execute(); + + return true; } /** - * Revokes all authorization assignments from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful + * @inheritdoc */ - public function revokeAll($userId) + protected function updateRule($name, $rule) { - return $this->db->createCommand() - ->delete($this->assignmentTable, ['user_id' => $userId]) - ->execute() > 0; + if (!$this->supportsCascadeUpdate() && $rule->name !== $name) { + $this->db->createCommand() + ->update($this->itemTable, ['rule_name' => $rule->name], ['rule_name' => $name]) + ->execute(); + } + + $rule->updatedAt = time(); + + $this->db->createCommand() + ->update($this->ruleTable, [ + 'name' => $rule->name, + 'data' => serialize($rule), + 'updated_at' => $rule->updatedAt, + ], [ + 'name' => $name, + ])->execute(); + + return true; } /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether the item has been assigned to the user. + * @inheritdoc */ - public function isAssigned($userId, $itemName) + protected function removeRule($rule) { - $query = new Query; + if (!$this->supportsCascadeUpdate()) { + $this->db->createCommand() + ->delete($this->itemTable, ['rule_name' => $rule->name]) + ->execute(); + } - return $query->select(['item_name']) - ->from($this->assignmentTable) - ->where(['user_id' => $userId, 'item_name' => $itemName]) - ->createCommand($this->db) - ->queryScalar() !== false; + $this->db->createCommand() + ->delete($this->ruleTable, ['name' => $rule->name]) + ->execute(); + + return true; } /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[\yii\web\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. + * @inheritdoc */ - public function getAssignment($userId, $itemName) + protected function getItems($type) { - $query = new Query; - $row = $query->from($this->assignmentTable) - ->where(['user_id' => $userId, 'item_name' => $itemName]) - ->createCommand($this->db) - ->queryOne(); - if ($row !== false) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } + $query = (new Query) + ->from($this->itemTable) + ->where(['type' => $type]); - return new Assignment([ - 'manager' => $this, - 'userId' => $row['user_id'], - 'itemName' => $row['item_name'], - 'ruleName' => $row['rule_name'], - 'data' => $data, - ]); - } else { - return null; + $items = []; + foreach ($query->all($this->db) as $row) { + $items[$row['name']] = $this->populateItem($row); } + + return $items; } /** - * Returns the item assignments for the specified user. - * @param mixed $userId the user ID (see [[\yii\web\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. + * Populates an auth item with the data fetched from database + * @param array $row the data from the auth item table + * @return Item the populated auth item instance (either Role or Permission) */ - public function getAssignments($userId) + protected function populateItem($row) { - $query = new Query; - $rows = $query->from($this->assignmentTable) - ->where(['user_id' => $userId]) - ->createCommand($this->db) - ->queryAll(); - $assignments = []; - foreach ($rows as $row) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; - } - $assignments[$row['item_name']] = new Assignment([ - 'manager' => $this, - 'userId' => $row['user_id'], - 'itemName' => $row['item_name'], - 'ruleName' => $row['rule_name'], - 'data' => $data, - ]); + $class = $row['type'] == Item::TYPE_PERMISSION ? Permission::className() : Role::className(); + + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; } - return $assignments; + return new $class([ + 'name' => $row['name'], + 'type' => $row['type'], + 'description' => $row['description'], + 'ruleName' => $row['rule_name'], + 'data' => $data, + 'createdAt' => $row['created_at'], + 'updatedAt' => $row['updated_at'], + ]); } /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. + * @inheritdoc */ - public function saveAssignment(Assignment $assignment) + public function getRolesByUser($userId) { - $this->db->createCommand() - ->update($this->assignmentTable, [ - 'rule_name' => $assignment->ruleName, - 'data' => $assignment->data === null ? null : serialize($assignment->data), - ], [ - 'user_id' => $assignment->userId, - 'item_name' => $assignment->itemName, - ]) - ->execute(); + $query = (new Query)->select('b.*') + ->from(['a' => $this->assignmentTable, 'b' => $this->itemTable]) + ->where('a.item_name=b.name') + ->andWhere(['a.user_id' => $userId]); + + $roles = []; + foreach ($query->all($this->db) as $row) { + $roles[$row['name']] = $this->populateItem($row); + } + return $roles; } /** - * 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. + * @inheritdoc */ - public function getItems($userId = null, $type = null) + public function getPermissionsByRole($roleName) { - $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]) - ->createCommand($this->db); - } elseif ($type === null) { - $command = $query->select(['name', 'type', 'description', 't1.rule_name', 't1.data']) - ->from([$this->itemTable . ' t1', $this->assignmentTable . ' t2']) - ->where(['user_id' => $userId, 'name' => new Expression('item_name')]) - ->createCommand($this->db); - } else { - $command = $query->select(['name', 'type', 'description', 't1.rule_name', 't1.data']) - ->from([$this->itemTable . ' t1', $this->assignmentTable . ' t2']) - ->where(['user_id' => $userId, 'type' => $type, 'name' => new Expression('item_name')]) - ->createCommand($this->db); + $childrenList = $this->getChildrenList(); + $result = []; + $this->getChildrenRecursive($roleName, $childrenList, $result); + if (empty($result)) { + return []; } - $items = []; - foreach ($command->queryAll() as $row) { - if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { - $data = null; + $query = (new Query)->from($this->itemTable)->where([ + 'type' => Item::TYPE_PERMISSION, + 'name' => array_keys($result), + ]); + $permissions = []; + foreach ($query->all($this->db) as $row) { + $permissions[$row['name']] = $this->populateItem($row); + } + return $permissions; + } + + /** + * @inheritdoc + */ + public function getPermissionsByUser($userId) + { + $query = (new Query)->select('item_name') + ->from($this->assignmentTable) + ->where(['user_id' => $userId]); + + $childrenList = $this->getChildrenList(); + $result = []; + foreach ($query->column($this->db) as $roleName) { + $this->getChildrenRecursive($roleName, $childrenList, $result); + } + + if (empty($result)) { + return []; + } + + $query = (new Query)->from($this->itemTable)->where([ + 'type' => Item::TYPE_PERMISSION, + 'name' => array_keys($result), + ]); + $permissions = []; + foreach ($query->all($this->db) as $row) { + $permissions[$row['name']] = $this->populateItem($row); + } + return $permissions; + } + + /** + * Returns the children for every parent. + * @return array the children list. Each array key is a parent item name, + * and the corresponding array value is a list of child item names. + */ + protected function getChildrenList() + { + $query = (new Query)->from($this->itemChildTable); + $parents = []; + foreach ($query->all($this->db) as $row) { + $parents[$row['parent']][] = $row['child']; + } + return $parents; + } + + /** + * Recursively finds all children and grand children of the specified item. + * @param string $name the name of the item whose children are to be looked for. + * @param array $childrenList the child list built via [[getChildrenList()]] + * @param array $result the children and grand children (in array keys) + */ + protected function getChildrenRecursive($name, $childrenList, &$result) + { + if (isset($childrenList[$name])) { + foreach ($childrenList[$name] as $child) { + $result[$child] = true; + $this->getChildrenRecursive($child, $childrenList, $result); } - $items[$row['name']] = new Item([ - 'manager' => $this, - 'name' => $row['name'], - 'type' => $row['type'], - 'description' => $row['description'], - 'ruleName' => $row['rule_name'], - 'data' => $data, - ]); } + } - return $items; + /** + * @inheritdoc + */ + public function getRule($name) + { + $row = (new Query)->select(['data']) + ->from($this->ruleTable) + ->where(['name' => $name]) + ->one($this->db); + return $row === false ? null : unserialize($row['data']); } /** - * 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 $rule 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 = '', $rule = null, $data = null) + * @inheritdoc + */ + public function getRules() { - $this->db->createCommand() - ->insert($this->itemTable, [ - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'rule_name' => $rule, - 'data' => $data === null ? null : serialize($data), - ]) - ->execute(); + $query = (new Query)->from($this->ruleTable); - return new Item([ - 'manager' => $this, - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'ruleName' => $rule, - 'data' => $data, - ]); + $rules = []; + foreach ($query->all($this->db) as $row) { + $rules[$row['name']] = unserialize($row['data']); + } + + return $rules; } /** - * 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 + * @inheritdoc */ - public function removeItem($name) + public function getAssignment($roleName, $userId) { - if ($this->usingSqlite()) { - $this->db->createCommand() - ->delete($this->itemChildTable, ['or', 'parent=:name', 'child=:name'], [':name' => $name]) - ->execute(); - $this->db->createCommand() - ->delete($this->assignmentTable, ['item_name' => $name]) - ->execute(); + $row = (new Query)->from($this->assignmentTable) + ->where(['user_id' => $userId, 'item_name' => $roleName]) + ->one($this->db); + + if ($row === false) { + return null; } - return $this->db->createCommand() - ->delete($this->itemTable, ['name' => $name]) - ->execute() > 0; + if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { + $data = null; + } + + return new Assignment([ + 'userId' => $row['user_id'], + 'roleName' => $row['item_name'], + 'createdAt' => $row['created_at'], + ]); } /** - * 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. + * @inheritdoc */ - public function getItem($name) + public function getAssignments($userId) { - $query = new Query; - $row = $query->from($this->itemTable) - ->where(['name' => $name]) - ->createCommand($this->db) - ->queryOne(); + $query = (new Query) + ->from($this->assignmentTable) + ->where(['user_id' => $userId]); - if ($row !== false) { + $assignments = []; + foreach ($query->all($this->db) as $row) { if (!isset($row['data']) || ($data = @unserialize($row['data'])) === false) { $data = null; } - - return new Item([ - 'manager' => $this, - 'name' => $row['name'], - 'type' => $row['type'], - 'description' => $row['description'], - 'ruleName' => $row['rule_name'], - 'data' => $data, + $assignments[$row['item_name']] = new Assignment([ + 'userId' => $row['user_id'], + 'roleName' => $row['item_name'], + 'createdAt' => $row['created_at'], ]); - } else { - return null; } + + return $assignments; } /** - * 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. + * @inheritdoc */ - public function saveItem(Item $item, $oldName = null) + public function addChild($parent, $child) { - if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { - $this->db->createCommand() - ->update($this->itemChildTable, ['parent' => $item->getName()], ['parent' => $oldName]) - ->execute(); - $this->db->createCommand() - ->update($this->itemChildTable, ['child' => $item->getName()], ['child' => $oldName]) - ->execute(); - $this->db->createCommand() - ->update($this->assignmentTable, ['item_name' => $item->getName()], ['item_name' => $oldName]) - ->execute(); + if ($parent->name === $child->name) { + throw new InvalidParamException("Cannot add '{$parent->name}' as a child of itself."); + } + + if ($parent instanceof Permission && $child instanceof Role) { + throw new InvalidParamException("Cannot add a role as a child of a permission."); + } + + if ($this->detectLoop($parent, $child)) { + throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected."); } $this->db->createCommand() - ->update($this->itemTable, [ - 'name' => $item->getName(), - 'type' => $item->type, - 'description' => $item->description, - 'rule_name' => $item->ruleName, - 'data' => $item->data === null ? null : serialize($item->data), - ], [ - 'name' => $oldName === null ? $item->getName() : $oldName, - ]) + ->insert($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name]) ->execute(); - } - /** - * Saves the authorization data to persistent storage. - */ - public function save() - { + return true; } /** - * Removes all authorization data. + * @inheritdoc */ - public function clearAll() + public function removeChild($parent, $child) { - $this->clearAssignments(); - $this->db->createCommand()->delete($this->itemChildTable)->execute(); - $this->db->createCommand()->delete($this->itemTable)->execute(); + return $this->db->createCommand() + ->delete($this->itemChildTable, ['parent' => $parent->name, 'child' => $child->name]) + ->execute() > 0; } /** - * Removes all authorization assignments. + * @inheritdoc */ - public function clearAssignments() + public function getChildren($name) { - $this->db->createCommand()->delete($this->assignmentTable)->execute(); + $query = (new Query) + ->select(['name', 'type', 'description', 'rule_name', 'data', 'created_at', 'updated_at']) + ->from([$this->itemTable, $this->itemChildTable]) + ->where(['parent' => $name, 'name' => new Expression('child')]); + + $children = []; + foreach ($query->all($this->db) as $row) { + $children[$row['name']] = $this->populateItem($row); + } + + return $children; } /** * 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 + * @param Item $parent the parent item + * @param Item $child the child item to be added to the hierarchy * @return boolean whether a loop exists */ - protected function detectLoop($itemName, $childName) + protected function detectLoop($parent, $child) { - if ($childName === $itemName) { + if ($child->name === $parent->name) { return true; } - foreach ($this->getItemChildren($childName) as $child) { - if ($this->detectLoop($itemName, $child->getName())) { + foreach ($this->getChildren($child->name) as $grandchild) { + if ($this->detectLoop($parent, $grandchild)) { return true; } } - return false; } /** - * @return boolean whether the database is a SQLite database + * @inheritdoc */ - protected function usingSqlite() + public function assign($role, $userId, $rule = null, $data = null) { - return $this->_usingSqlite; - } + $assignment = new Assignment([ + 'userId' => $userId, + 'roleName' => $role->name, + 'createdAt' => time(), + ]); - /** - * Removes the specified rule. - * - * @param string $name the name of the rule to be removed - * @return boolean whether the rule exists in the storage and has been removed - */ - public function removeRule($name) - { - return $this->db->createCommand()->delete($this->ruleTable, ['name' => $name])->execute(); + $this->db->createCommand() + ->insert($this->assignmentTable, [ + 'user_id' => $assignment->userId, + 'item_name' => $assignment->roleName, + 'created_at' => $assignment->createdAt, + ])->execute(); + + return $assignment; } /** - * Saves the changes to the rule. - * - * @param Rule $rule the rule that has been changed. + * @inheritdoc */ - public function insertRule(Rule $rule) + public function revoke($role, $userId) { - $this->db->createCommand()->insert($this->ruleTable, ['name' => $rule->name, 'data' => serialize($rule)])->execute(); + return $this->db->createCommand() + ->delete($this->assignmentTable, ['user_id' => $userId, 'item_name' => $role->name]) + ->execute() > 0; } /** - * Updates existing rule. - * - * @param string $name the name of the rule to update - * @param Rule $rule new rule + * @inheritdoc */ - public function updateRule($name, Rule $rule) + public function revokeAll($userId) { - $this->db->createCommand()->update($this->ruleTable, ['name' => $rule->name, 'data' => serialize($rule)], ['name' => $name])->execute(); + return $this->db->createCommand() + ->delete($this->assignmentTable, ['user_id' => $userId]) + ->execute() > 0; } /** - * Returns rule given its name. - * - * @param string $name name of the rule. - * @return Rule + * Removes all authorization data. */ - public function getRule($name) + public function clearAll() { - $query = new Query; - $query->select(['data'])->from($this->ruleTable)->where(['name' => $name]); - $row = $query->createCommand($this->db)->queryOne(); - return $row === false ? null : unserialize($row['data']); + $this->clearAssignments(); + $this->db->createCommand()->delete($this->itemChildTable)->execute(); + $this->db->createCommand()->delete($this->itemTable)->execute(); + $this->db->createCommand()->delete($this->ruleTable)->execute(); } /** - * Returns all rules. - * - * @return Rule[] + * Removes all authorization assignments. */ - public function getRules() + public function clearAssignments() { - $query = new Query(); - $rows = $query->from($this->ruleTable)->createCommand($this->db)->queryAll(); - - $rules = []; - foreach ($rows as $row) { - $rules[$row['name']] = unserialize($row['data']); - } - return $rules; + $this->db->createCommand()->delete($this->assignmentTable)->execute(); } } diff --git a/framework/rbac/Item.php b/framework/rbac/Item.php index 1f8d3cbf9e..5a79087637 100644 --- a/framework/rbac/Item.php +++ b/framework/rbac/Item.php @@ -7,34 +7,25 @@ namespace yii\rbac; -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 Item[] $children All child items of this item. This property is read-only. - * @property string $name The item name. - * * @author Qiang Xue - * @author Alexander Kochetov * @since 2.0 */ class Item extends Object { - const TYPE_OPERATION = 0; - const TYPE_TASK = 1; - const TYPE_ROLE = 2; + const TYPE_ROLE = 1; + const TYPE_PERMISSION = 2; /** - * @var Manager the auth manager of this item + * @var integer the type of the item. This should be either [[TYPE_ROLE]] or [[TYPE_PERMISSION]]. */ - public $manager; + public $type; + /** + * @var string the name of the item. This must be globally unique. + */ + public $name; /** * @var string the item description */ @@ -48,158 +39,11 @@ class Item extends Object */ public $data; /** - * @var integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). - */ - public $type; - - private $_name; - private $_oldName; - - /** - * 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 = []) - { - Yii::trace('Checking permission: ' . $this->_name, __METHOD__); - if ($this->manager->executeRule($this->ruleName, $params, $this->data)) { - if ($this->_name == $itemName) { - return true; - } - foreach ($this->manager->getItemChildren($this->_name) as $item) { - if ($item->checkAccess($itemName, $params)) { - return true; - } - } - } - - return false; - } - - /** - * @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) { - $this->_oldName = $this->_name; - $this->_name = $value; - } - } - - /** - * 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->manager->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->manager->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->manager->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->manager->getItemChildren($this->_name); - } - - /** - * Assigns this item to a user. - * - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param Rule $rule the 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 + * @var integer UNIX timestamp representing the item creation time */ - public function assign($userId, Rule $rule = null, $data = null) - { - return $this->manager->assign($userId, $this->_name, $rule, $data); - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - * @see Manager::revoke - */ - public function revoke($userId) - { - return $this->manager->revoke($userId, $this->_name); - } - - /** - * Returns a value indicating whether this item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether the item has been assigned to the user. - * @see Manager::isAssigned - */ - public function isAssigned($userId) - { - return $this->manager->isAssigned($userId, $this->_name); - } - - /** - * Returns the item assignment information. - * @param mixed $userId the user ID (see [[\yii\web\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->manager->getAssignment($userId, $this->_name); - } - + public $createdAt; /** - * Saves an authorization item to persistent storage. + * @var integer UNIX timestamp representing the item updating time */ - public function save() - { - $this->manager->saveItem($this, $this->_oldName); - $this->_oldName = null; - } + public $updatedAt; } diff --git a/framework/rbac/Manager.php b/framework/rbac/Manager.php deleted file mode 100644 index cf497f0da6..0000000000 --- a/framework/rbac/Manager.php +++ /dev/null @@ -1,382 +0,0 @@ - AuthItem). This property is read-only. - * @property Item[] $roles Roles (name => AuthItem). This property is read-only. - * @property Item[] $tasks Tasks (name => AuthItem). This property is read-only. - * - * @author Qiang Xue - * @author Alexander Kochetov - * @since 2.0 - */ -abstract class Manager extends Component -{ - /** - * @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 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 = []; - - /** - * 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 $ruleName name of the 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 = '', $ruleName = null, $data = null) - { - return $this->createItem($name, Item::TYPE_ROLE, $description, $ruleName, $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 $ruleName name of the 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 = '', $ruleName = null, $data = null) - { - return $this->createItem($name, Item::TYPE_TASK, $description, $ruleName, $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 $ruleName name of the 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 = '', $ruleName = null, $data = null) - { - return $this->createItem($name, Item::TYPE_OPERATION, $description, $ruleName, $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 rule. - * - * @param string $ruleName name of the 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 rule execution returns true. If `$ruleName` is null, true will be returned. - * @throws InvalidConfigException if `$ruleName` does not correspond to a valid rule. - */ - public function executeRule($ruleName, $params, $data) - { - if ($ruleName === null) { - return true; - } - $rule = $this->getRule($ruleName); - if ($rule instanceof Rule) { - return $rule->execute($params, $data); - } else { - throw new InvalidConfigException("Rule not found: $ruleName"); - } - } - - /** - * 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 = ['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 [[\yii\web\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 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 = []); - - /** - * 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 $ruleName name of the rule associated with 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 = '', $ruleName = 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 $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 [[\yii\web\User::id]]) - * @param string $itemName the item name - * @param string $ruleName name of the 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, $ruleName = null, $data = null); - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @param string $itemName the item name - * @return boolean whether removal is successful - */ - abstract public function revoke($userId, $itemName); - - /** - * Revokes all authorization assignments from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - */ - abstract public function revokeAll($userId); - - /** - * Returns a value indicating whether the item has been assigned to the user. - * @param mixed $userId the user ID (see [[\yii\web\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 [[\yii\web\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 [[\yii\web\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); - - /** - * Removes the specified rule. - * @param string $name the name of the rule to be removed - * @return boolean whether the rule exists in the storage and has been removed - */ - abstract public function removeRule($name); - - /** - * Inserts new rule. - * - * @param Rule $rule the rule that needs to be stored. - */ - abstract public function insertRule(Rule $rule); - - /** - * Updates existing rule. - * - * @param string $name the name of the rule to update - * @param Rule $rule new rule - */ - abstract public function updateRule($name, Rule $rule); - - /** - * Returns rule given its name. - * - * @param string $name name of the rule. - * @return Rule - */ - abstract public function getRule($name); - - /** - * Returns all rules. - * - * @return Rule[] - */ - abstract public function getRules(); - - /** - * Saves the changes to an authorization assignment. - * @param Assignment $assignment the assignment that has been changed. - */ - abstract public function saveAssignment(Assignment $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/ManagerInterface.php b/framework/rbac/ManagerInterface.php new file mode 100644 index 0000000000..5250a691d6 --- /dev/null +++ b/framework/rbac/ManagerInterface.php @@ -0,0 +1,209 @@ + + * @since 2.0 + */ +interface ManagerInterface +{ + /** + * Checks if the user has the specified permission. + * @param string|integer $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[\yii\web\User::id]]. + * @param string $permissionName the name of the permission to be checked against + * @param array $params name-value pairs that will be passed to the rules associated + * with the roles and permissions assigned to the user. + * @return boolean whether the user has the specified permission. + * @throws \yii\base\InvalidParamException if $permissionName does not refer to an existing permission + */ + public function checkAccess($userId, $permissionName, $params = []); + + /** + * Creates a new Role object. + * Note that the newly created role is not added to the RBAC system yet. + * You must fill in the needed data and call [[add()]] to add it to the system. + * @param string $name the role name + * @return Role the new Role object + */ + public function createRole($name); + + /** + * Creates a new Permission object. + * Note that the newly created permission is not added to the RBAC system yet. + * You must fill in the needed data and call [[add()]] to add it to the system. + * @param string $name the permission name + * @return Permission the new Permission object + */ + public function createPermission($name); + + /** + * Adds a role, permission or rule to the RBAC system. + * @param Role|Permission|Rule $object + * @return boolean whether the role, permission or rule is successfully added to the system + * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique) + */ + public function add($object); + + /** + * Removes a role, permission or rule from the RBAC system. + * @param Role|Permission|Rule $object + * @return boolean whether the role, permission or rule is successfully removed + */ + public function remove($object); + + /** + * Updates the specified role, permission or rule in the system. + * @param string $name the old name of the role, permission or rule + * @param Role|Permission|Rule $object + * @return boolean whether the update is successful + * @throws \Exception if data validation or saving fails (such as the name of the role or permission is not unique) + */ + public function update($name, $object); + + /** + * Returns the named role. + * @param string $name the role name. + * @return Role the role corresponding to the specified name. Null is returned if no such role. + */ + public function getRole($name); + + /** + * Returns all roles in the system. + * @return Role[] all roles in the system. The array is indexed by the role names. + */ + public function getRoles(); + + /** + * Returns the roles that are assigned to the user via [[assign()]]. + * Note that child roles that are not assigned directly to the user will not be returned. + * @param string|integer $userId the user ID (see [[\yii\web\User::id]]) + * @return Role[] all roles directly or indirectly assigned to the user. The array is indexed by the role names. + */ + public function getRolesByUser($userId); + + /** + * Returns the named permission. + * @param string $name the permission name. + * @return Permission the permission corresponding to the specified name. Null is returned if no such permission. + */ + public function getPermission($name); + + /** + * Returns all permissions in the system. + * @return Permission[] all permissions in the system. The array is indexed by the permission names. + */ + public function getPermissions(); + + /** + * Returns all permissions that the specified role represents. + * @param string $roleName the role name + * @return Permission[] all permissions that the role represents. The array is indexed by the permission names. + */ + public function getPermissionsByRole($roleName); + + /** + * Returns all permissions that the user has. + * @param string|integer $userId the user ID (see [[\yii\web\User::id]]) + * @return Permission[] all permissions that the user has. The array is indexed by the permission names. + */ + public function getPermissionsByUser($userId); + + /** + * Returns the rule of the specified name. + * @param string $name the rule name + * @return Rule the rule object, or null if the specified name does not correspond to a rule. + */ + public function getRule($name); + + /** + * Returns all rules available in the system. + * @return Rule[] the rules indexed by the rule names + */ + public function getRules(); + + /** + * Adds an item as a child of another item. + * @param Item $parent + * @param Item $child + * @throws \yii\base\Exception if the parent-child relationship already exists or if a loop has been detected. + */ + public function addChild($parent, $child); + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param Item $parent + * @param Item $child + * @return boolean whether the removal is successful + */ + public function removeChild($parent, $child); + + /** + * Returns the child permissions and/or roles. + * @param string $name the parent name + * @return Item[] the child permissions and/or roles + */ + public function getChildren($name); + + /** + * Assigns a role to a user. + * + * @param Role $role + * @param string|integer $userId the user ID (see [[\yii\web\User::id]]) + * @param Rule $rule the rule to be associated with this assignment. If not null, the rule + * will be executed when [[allow()]] is called to check the user permission. + * @param mixed $data additional data associated with this assignment. + * @return Assignment the role assignment information. + * @throws \Exception if the role has already been assigned to the user + */ + public function assign($role, $userId, $rule = null, $data = null); + + /** + * Revokes a role from a user. + * @param Role $role + * @param string|integer $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether the revoking is successful + */ + public function revoke($role, $userId); + + /** + * Revokes all roles from a user. + * @param mixed $userId the user ID (see [[\yii\web\User::id]]) + * @return boolean whether the revoking is successful + */ + public function revokeAll($userId); + + /** + * Returns the assignment information regarding a role and a user. + * @param string|integer $userId the user ID (see [[\yii\web\User::id]]) + * @param string $roleName the role name + * @return Assignment the assignment information. Null is returned if + * the role is not assigned to the user. + */ + public function getAssignment($roleName, $userId); + + /** + * Returns all role assignment information for the specified user. + * @param string|integer $userId the user ID (see [[\yii\web\User::id]]) + * @return Assignment[] the assignments indexed by role names. An empty array will be + * returned if there is no role assigned to the user. + */ + public function getAssignments($userId); + + /** + * Removes all authorization data. + */ + public function clearAll(); + + /** + * Removes all authorization assignments. + */ + public function clearAssignments(); +} diff --git a/framework/rbac/Permission.php b/framework/rbac/Permission.php new file mode 100644 index 0000000000..e004cb6a9f --- /dev/null +++ b/framework/rbac/Permission.php @@ -0,0 +1,20 @@ + + * @since 2.0 + */ +class Permission extends Item +{ + /** + * @inheritdoc + */ + public $type = self::TYPE_PERMISSION; +} diff --git a/framework/rbac/PhpManager.php b/framework/rbac/PhpManager.php deleted file mode 100644 index 7dffe8351b..0000000000 --- a/framework/rbac/PhpManager.php +++ /dev/null @@ -1,644 +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. - * This can be either a file path or a path alias to the file. - * Make sure this file is writable by the Web server process if the authorization needs to be changed online. - * @see loadFromFile() - * @see saveToFile() - */ - public $authFile = '@app/data/rbac.php'; - - private $_items = []; // itemName => item - private $_children = []; // itemName, childName => child - private $_assignments = []; // userId, itemName => assignment - private $_rules = []; // ruleName => rule - - - /** - * Initializes the application component. - * This method overrides parent implementation by loading the authorization data - * from PHP script. - */ - public function init() - { - parent::init(); - $this->authFile = Yii::getAlias($this->authFile); - $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 [[\yii\web\User::id]]. - * @param array $params name-value pairs that would be passed to rules associated - * with the tasks and roles assigned to the user. A param with name '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 = []) - { - 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->executeRule($item->ruleName, $params, $item->data)) { - if (in_array($itemName, $this->defaultRoles)) { - return true; - } - if (isset($this->_assignments[$userId][$itemName])) { - /** @var Assignment $assignment */ - $assignment = $this->_assignments[$userId][$itemName]; - if ($this->executeRule($assignment->ruleName, $params, $assignment->data)) { - 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 Item $child */ - $child = $this->_items[$childName]; - /** @var Item $item */ - $item = $this->_items[$itemName]; - $this->checkItemChildType($item->type, $child->type); - 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 string|array $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] : []; - } - - $children = []; - 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 [[\yii\web\User::id]]) - * @param string $itemName the item name - * @param string $ruleName 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, $ruleName = 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([ - 'manager' => $this, - 'userId' => $userId, - 'itemName' => $itemName, - 'ruleName' => $ruleName, - 'data' => $data, - ]); - } - } - - /** - * Revokes an authorization assignment from a user. - * @param mixed $userId the user ID (see [[\yii\web\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; - } - } - - /** - * Revokes all authorization assignments from a user. - * @param mixed $userId the user ID (see [[\yii\web\User::id]]) - * @return boolean whether removal is successful - */ - public function revokeAll($userId) - { - if (isset($this->_assignments[$userId]) && is_array($this->_assignments[$userId])) { - foreach ($this->_assignments[$userId] as $itemName => $value) - 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 [[\yii\web\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 [[\yii\web\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 [[\yii\web\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] : []; - } - - /** - * 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 = []; - if ($userId === null) { - foreach ($this->_items as $name => $item) { - /** @var Item $item */ - if ($item->type == $type) { - $items[$name] = $item; - } - } - } elseif (isset($this->_assignments[$userId])) { - foreach ($this->_assignments[$userId] as $assignment) { - /** @var Assignment $assignment */ - $name = $assignment->itemName; - if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->type == $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 $rule 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 = '', $rule = 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([ - 'manager' => $this, - 'name' => $name, - 'type' => $type, - 'description' => $description, - 'ruleName' => $rule, - 'data' => $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 $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 $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 = []; - foreach ($this->_items as $name => $item) { - /** @var Item $item */ - $items[$name] = [ - 'type' => $item->type, - 'description' => $item->description, - 'ruleName' => $item->ruleName, - 'data' => $item->data, - ]; - if (isset($this->_children[$name])) { - foreach ($this->_children[$name] as $child) { - /** @var Item $child */ - $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] = [ - 'ruleName' => $assignment->ruleName, - 'data' => $assignment->data, - ]; - } - } - } - - $rules = []; - foreach ($this->_rules as $name => $rule) { - $rules[$name] = serialize($rule); - } - - $this->saveToFile(['items' => $items, 'rules' => $rules], $this->authFile); - } - - /** - * Loads authorization data. - */ - public function load() - { - $this->clearAll(); - - $data = $this->loadFromFile($this->authFile); - - if (isset($data['items'])) { - foreach ($data['items'] as $name => $item) { - $this->_items[$name] = new Item([ - 'manager' => $this, - 'name' => $name, - 'type' => $item['type'], - 'description' => $item['description'], - 'ruleName' => $item['ruleName'], - 'data' => $item['data'], - ]); - } - - foreach ($data['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([ - 'manager' => $this, - 'userId' => $userId, - 'itemName' => $name, - 'ruleName' => $assignment['ruleName'], - 'data' => $assignment['data'], - ]); - } - } - } - } - - if (isset($data['rules'])) { - foreach ($data['rules'] as $name => $ruleData) { - $this->_rules[$name] = unserialize($ruleData); - } - } - } - - /** - * Removes all authorization data. - */ - public function clearAll() - { - $this->clearAssignments(); - $this->_children = []; - $this->_items = []; - } - - /** - * Removes all authorization assignments. - */ - public function clearAssignments() - { - $this->_assignments = []; - } - - /** - * 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 Item $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 []; - } - } - - /** - * 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, "_rules[$name])) { - unset($this->_rules[$name]); - return true; - } else { - return false; - } - } - - - /** - * Saves the changes to the rule. - * - * @param Rule $rule the rule that has been changed. - */ - public function insertRule(Rule $rule) - { - $this->_rules[$rule->name] = $rule; - } - - /** - * Updates existing rule. - * - * @param string $name the name of the rule to update - * @param Rule $rule new rule - */ - public function updateRule($name, Rule $rule) - { - if ($rule->name !== $name) { - unset($this->_rules[$name]); - } - $this->_rules[$rule->name] = $rule; - } - - /** - * Returns rule given its name. - * - * @param string $name name of the rule. - * @return Rule - */ - public function getRule($name) - { - return isset($this->_rules[$name]) ? $this->_rules[$name] : null; - } - - /** - * Returns all rules. - * - * @return Rule[] - */ - public function getRules() - { - return $this->_rules; - } -} diff --git a/framework/rbac/Role.php b/framework/rbac/Role.php new file mode 100644 index 0000000000..0538789cb3 --- /dev/null +++ b/framework/rbac/Role.php @@ -0,0 +1,20 @@ + + * @since 2.0 + */ +class Role extends Item +{ + /** + * @inheritdoc + */ + public $type = self::TYPE_ROLE; +} diff --git a/framework/rbac/Rule.php b/framework/rbac/Rule.php index ee13735f05..edcbf810d3 100644 --- a/framework/rbac/Rule.php +++ b/framework/rbac/Rule.php @@ -10,8 +10,7 @@ namespace yii\rbac; use yii\base\Object; /** - * Rule represents a business constraint that may be assigned and the applied to - * an authorization item or assignment. + * Rule represents a business constraint that may be associated with a role, permission or assignment. * * @author Alexander Makarov * @since 2.0 @@ -22,13 +21,21 @@ abstract class Rule extends Object * @var string name of the rule */ public $name; + /** + * @var integer UNIX timestamp representing the rule creation time + */ + public $createdAt; + /** + * @var integer UNIX timestamp representing the rule updating time + */ + public $updatedAt; /** * Executes the rule. * - * @param array $params parameters passed to [[Manager::checkAccess()]]. - * @param mixed $data additional data associated with the authorization item or assignment. - * @return boolean whether the rule execution returns true. + * @param Item $item the auth item that this rule is associated with + * @param array $params parameters passed to [[ManagerInterface::allow()]]. + * @return boolean a value indicating whether the rule permits the auth item it is associated with. */ - abstract public function execute($params, $data); + abstract public function execute($item, $params); } diff --git a/framework/rbac/schema-mssql.sql b/framework/rbac/schema-mssql.sql index 6a96975e13..4862011b66 100644 --- a/framework/rbac/schema-mssql.sql +++ b/framework/rbac/schema-mssql.sql @@ -18,6 +18,8 @@ create table [auth_rule] ( [name] varchar(64) not null, [data] text, + [created_at] integer, + [updated_at] integer, primary key ([name]) ); @@ -28,6 +30,8 @@ create table [auth_item] [description] text, [rule_name] varchar(64), [data] text, + [created_at] integer, + [updated_at] integer, primary key ([name]), foreign key ([rule_name]) references [auth_rule] ([name]) on delete set null on update cascade, key [type] ([type]) @@ -46,9 +50,7 @@ create table [auth_assignment] ( [item_name] varchar(64) not null, [user_id] varchar(64) not null, - [rule_name] varchar(64), - [data] text, + [created_at] integer, primary key ([item_name], [user_id]), - foreign key ([item_name]) references [auth_item] ([name]) on delete cascade on update cascade, - foreign key ([rule_name]) references [auth_rule] ([name]) on delete set null on update cascade + foreign key ([item_name]) references [auth_item] ([name]) on delete cascade on update cascade ); diff --git a/framework/rbac/schema-mysql.sql b/framework/rbac/schema-mysql.sql index 21c21ba719..1a44a789e6 100644 --- a/framework/rbac/schema-mysql.sql +++ b/framework/rbac/schema-mysql.sql @@ -16,8 +16,10 @@ drop table if exists `auth_rule`; create table `auth_rule` ( - `name` varchar(64) not null, - `data` text, + `name` varchar(64) not null, + `data` text, + `created_at` integer, + `updated_at` integer, primary key (`name`) ) engine InnoDB; @@ -28,6 +30,8 @@ create table `auth_item` `description` text, `rule_name` varchar(64), `data` text, + `created_at` integer, + `updated_at` integer, primary key (`name`), foreign key (`rule_name`) references `auth_rule` (`name`) on delete set null on update cascade, key `type` (`type`) @@ -46,9 +50,7 @@ create table `auth_assignment` ( `item_name` varchar(64) not null, `user_id` varchar(64) not null, - `rule_name` varchar(64), - `data` text, + `created_at` integer, primary key (`item_name`, `user_id`), - foreign key (`item_name`) references `auth_item` (`name`) on delete cascade on update cascade, - foreign key (`rule_name`) references `auth_rule` (`name`) on delete set null on update cascade -) engine InnoDB; \ No newline at end of file + foreign key (`item_name`) references `auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; diff --git a/framework/rbac/schema-oci.sql b/framework/rbac/schema-oci.sql index e90936475f..44fca9ecfb 100644 --- a/framework/rbac/schema-oci.sql +++ b/framework/rbac/schema-oci.sql @@ -16,8 +16,10 @@ drop table if exists "auth_rule"; create table "auth_rule" ( - "name" varchar(64) not null, - "data" text, + "name" varchar(64) not null, + "data" text, + "created_at" integer, + "updated_at" integer, primary key ("name") ); @@ -28,6 +30,8 @@ create table "auth_item" "description" text, "rule_name" varchar(64), "data" text, + "created_at" integer, + "updated_at" integer, primary key ("name"), foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade, key "type" ("type") @@ -46,9 +50,7 @@ create table "auth_assignment" ( "item_name" varchar(64) not null, "user_id" varchar(64) not null, - "rule_name" varchar(64), - "data" text, + "created_at" integer, primary key ("item_name","user_id"), - foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade, - foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade + foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade ); diff --git a/framework/rbac/schema-pgsql.sql b/framework/rbac/schema-pgsql.sql index 134e5e3cf7..f2d77a44bc 100644 --- a/framework/rbac/schema-pgsql.sql +++ b/framework/rbac/schema-pgsql.sql @@ -18,6 +18,8 @@ create table "auth_rule" ( "name" varchar(64) not null, "data" text, + "created_at" integer, + "updated_at" integer, primary key ("name") ); @@ -28,6 +30,8 @@ create table "auth_item" "description" text, "rule_name" varchar(64), "data" text, + "created_at" integer, + "updated_at" integer, primary key ("name"), foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade ); @@ -47,9 +51,7 @@ create table "auth_assignment" ( "item_name" varchar(64) not null, "user_id" varchar(64) not null, - "rule_name" varchar(64), - "data" text, + "created_at" integer, primary key ("item_name","user_id"), - foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade, - foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade + foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade ); diff --git a/framework/rbac/schema-sqlite.sql b/framework/rbac/schema-sqlite.sql index ca9c06887f..f494185d2d 100644 --- a/framework/rbac/schema-sqlite.sql +++ b/framework/rbac/schema-sqlite.sql @@ -18,6 +18,8 @@ create table "auth_rule" ( "name" varchar(64) not null, "data" text, + "created_at" integer, + "updated_at" integer, primary key ("name") ); @@ -28,6 +30,8 @@ create table "auth_item" "description" text, "rule_name" varchar(64), "data" text, + "created_at" integer, + "updated_at" integer, primary key ("name"), foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade ); @@ -47,9 +51,7 @@ create table "auth_assignment" ( "item_name" varchar(64) not null, "user_id" varchar(64) not null, - "rule_name" varchar(64), - "data" text, + "created_at" integer, primary key ("item_name","user_id"), - foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade, - foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade + foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade ); diff --git a/tests/unit/framework/rbac/AuthorRule.php b/tests/unit/framework/rbac/AuthorRule.php index 8f9feb7d1e..a42072c29e 100644 --- a/tests/unit/framework/rbac/AuthorRule.php +++ b/tests/unit/framework/rbac/AuthorRule.php @@ -14,9 +14,8 @@ class AuthorRule extends Rule /** * @inheritdoc */ - public function execute($params, $data) + public function execute($item, $params) { - return $params['authorID'] == $params['userID']; + return $params['authorID'] == $params['user']; } } - \ No newline at end of file diff --git a/tests/unit/framework/rbac/DbManagerTestCase.php b/tests/unit/framework/rbac/DbManagerTestCase.php index 7f64b886d9..4e5334ec57 100644 --- a/tests/unit/framework/rbac/DbManagerTestCase.php +++ b/tests/unit/framework/rbac/DbManagerTestCase.php @@ -29,8 +29,6 @@ abstract class DbManagerTestCase extends ManagerTestCase } $this->auth = new DbManager(['db' => $this->getConnection()]); - $this->auth->init(); - $this->prepareData(); } protected function tearDown() diff --git a/tests/unit/framework/rbac/ManagerTestCase.php b/tests/unit/framework/rbac/ManagerTestCase.php index 40b2d84a08..267b1175c5 100644 --- a/tests/unit/framework/rbac/ManagerTestCase.php +++ b/tests/unit/framework/rbac/ManagerTestCase.php @@ -4,13 +4,59 @@ namespace yiiunit\framework\rbac; use yii\rbac\Assignment; use yii\rbac\Item; +use yii\rbac\Permission; +use yii\rbac\Role; use yiiunit\TestCase; abstract class ManagerTestCase extends TestCase { - /** @var \yii\rbac\Manager */ + /** + * @var \yii\rbac\ManagerInterface + */ protected $auth; + public function testCreateRoleAndPermission() + { + $role = $this->auth->createRole('admin'); + $this->assertTrue($role instanceof Role); + $this->assertEquals(Item::TYPE_ROLE, $role->type); + $this->assertEquals('admin', $role->name); + } + + public function testCreatePermission() + { + $permission = $this->auth->createPermission('edit post'); + $this->assertTrue($permission instanceof Permission); + $this->assertEquals(Item::TYPE_PERMISSION, $permission->type); + $this->assertEquals('edit post', $permission->name); + } + + public function testAdd() + { + $role = $this->auth->createRole('admin'); + $role->description = 'administrator'; + $this->assertTrue($this->auth->add($role)); + + $permission = $this->auth->createPermission('edit post'); + $permission->description = 'edit a post'; + $this->assertTrue($this->auth->add($permission)); + + $rule = new AuthorRule(['name' => 'is author', 'reallyReally' => true]); + $this->assertTrue($this->auth->add($rule)); + + // todo: check duplication of name + } +/* + public function testRemove() + { + + } + + public function testUpdate() + { + + } + public function testCreateItem() { $type = Item::TYPE_TASK; @@ -43,7 +89,7 @@ abstract class ManagerTestCase extends TestCase $this->assertNull($this->auth->getItem('unknown')); } - public function testRemoveAuthItem() + public function testRemoveItem() { $this->assertTrue($this->auth->getItem('updatePost') instanceof Item); $this->assertTrue($this->auth->removeItem('updatePost')); @@ -167,9 +213,12 @@ abstract class ManagerTestCase extends TestCase $this->setExpectedException('\yii\base\Exception'); $this->auth->addItemChild('readPost', 'readPost'); } + */ public function testGetRule() { + $this->prepareData(); + $rule = $this->auth->getRule('isAuthor'); $this->assertInstanceOf('yii\rbac\Rule', $rule); $this->assertEquals('isAuthor', $rule->name); @@ -178,13 +227,14 @@ abstract class ManagerTestCase extends TestCase $this->assertNull($rule); } - public function testInsertRule() + public function testAddRule() { + $this->prepareData(); + $ruleName = 'isReallyReallyAuthor'; $rule = new AuthorRule(['name' => $ruleName, 'reallyReally' => true]); - $this->auth->insertRule($rule); + $this->auth->add($rule); - /** @var AuthorRule $rule */ $rule = $this->auth->getRule($ruleName); $this->assertEquals($ruleName, $rule->name); $this->assertEquals(true, $rule->reallyReally); @@ -192,12 +242,13 @@ abstract class ManagerTestCase extends TestCase public function testUpdateRule() { + $this->prepareData(); + $rule = $this->auth->getRule('isAuthor'); $rule->name = "newName"; $rule->reallyReally = false; - $this->auth->updateRule('isAuthor', $rule); + $this->auth->update('isAuthor', $rule); - /** @var AuthorRule $rule */ $rule = $this->auth->getRule('isAuthor'); $this->assertEquals(null, $rule); @@ -206,7 +257,7 @@ abstract class ManagerTestCase extends TestCase $this->assertEquals(false, $rule->reallyReally); $rule->reallyReally = true; - $this->auth->updateRule('newName', $rule); + $this->auth->update('newName', $rule); $rule = $this->auth->getRule('newName'); $this->assertEquals(true, $rule->reallyReally); @@ -214,8 +265,10 @@ abstract class ManagerTestCase extends TestCase public function testGetRules() { + $this->prepareData(); + $rule = new AuthorRule(['name' => 'isReallyReallyAuthor', 'reallyReally' => true]); - $this->auth->insertRule($rule); + $this->auth->add($rule); $rules = $this->auth->getRules(); @@ -230,103 +283,87 @@ abstract class ManagerTestCase extends TestCase public function testRemoveRule() { - $this->auth->removeRule('isAuthor'); + $this->prepareData(); + + $this->auth->remove($this->auth->getRule('isAuthor')); $rules = $this->auth->getRules(); $this->assertEmpty($rules); } - public function testExecuteRule() - { - $this->assertTrue($this->auth->executeRule(null, [], null)); - $this->assertTrue($this->auth->executeRule('isAuthor', ['userID' => 1, 'authorID' => 1], null)); - $this->assertFalse($this->auth->executeRule('isAuthor', ['userID' => 1, 'authorID' => 2], null)); - } - public function testCheckAccess() { - $results = [ + $this->prepareData(); + + $testSuites = [ 'reader A' => [ 'createPost' => false, 'readPost' => true, 'updatePost' => false, - 'updateOwnPost' => false, - 'deletePost' => false, + 'updateAnyPost' => false, ], 'author B' => [ 'createPost' => true, 'readPost' => true, 'updatePost' => true, - 'updateOwnPost' => true, - 'deletePost' => false, - ], - 'editor C' => [ - 'createPost' => false, - 'readPost' => true, - 'updatePost' => true, - 'updateOwnPost' => false, - 'deletePost' => false, + 'updateAnyPost' => false, ], - 'admin D' => [ + 'admin C' => [ 'createPost' => true, 'readPost' => true, - 'updatePost' => true, - 'updateOwnPost' => false, - 'deletePost' => true, - ], - 'reader E' => [ - 'createPost' => false, - 'readPost' => false, 'updatePost' => false, - 'updateOwnPost' => false, - 'deletePost' => false, + 'updateAnyPost' => true, ], ]; $params = ['authorID' => 'author B']; - foreach (['reader A', 'author B', 'editor C', 'admin D'] as $user) { - $params['userID'] = $user; - foreach (['createPost', 'readPost', 'updatePost', 'updateOwnPost', 'deletePost'] as $operation) { - $result = $this->auth->checkAccess($user, $operation, $params); - $this->assertEquals($results[$user][$operation], $result); + foreach ($testSuites as $user => $tests) { + foreach ($tests as $permission => $result) { + $this->assertEquals($result, $this->auth->checkAccess($user, $permission, $params), "Checking $user can $permission"); } } } protected function prepareData() { - $this->auth->insertRule(new AuthorRule()); - - $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', 'isAuthor'); - $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'); - $this->auth->assign('reader E', 'reader'); + $rule = new AuthorRule; + $this->auth->add($rule); + + $createPost = $this->auth->createPermission('createPost'); + $createPost->description = 'create a post'; + $this->auth->add($createPost); + + $readPost = $this->auth->createPermission('readPost'); + $readPost->description = 'read a post'; + $this->auth->add($readPost); + + $updatePost = $this->auth->createPermission('updatePost'); + $updatePost->description = 'update a post'; + $updatePost->ruleName = $rule->name; + $this->auth->add($updatePost); + + $updateAnyPost = $this->auth->createPermission('updateAnyPost'); + $updateAnyPost->description = 'update any post'; + $this->auth->add($updateAnyPost); + + $reader = $this->auth->createRole('reader'); + $this->auth->add($reader); + $this->auth->addChild($reader, $readPost); + + $author = $this->auth->createRole('author'); + $this->auth->add($author); + $this->auth->addChild($author, $createPost); + $this->auth->addChild($author, $updatePost); + $this->auth->addChild($author, $reader); + + $admin = $this->auth->createRole('admin'); + $this->auth->add($admin); + $this->auth->addChild($admin, $author); + $this->auth->addChild($admin, $updateAnyPost); + + $this->auth->assign($reader, 'reader A'); + $this->auth->assign($author, 'author B'); + $this->auth->assign($admin, 'admin C'); } }