Commit 69786b01 by Alexander Makarov

Merge pull request #2974 from yiisoft/decouple-bizrules-from-auth-items

RBAC: decoupled rules from assignments and items, implemented php manager
parents c5ad45c7 57498582
......@@ -121,71 +121,94 @@ class PhpManager extends \yii\rbac\PhpManager
}
```
Now create custom rule class:
```php
namespace app\rbac;
use yii\rbac\Rule;
class NotGuestRule extends Rule
{
public function execute($params, $data)
{
return !Yii::$app->user->isGuest;
}
}
```
Then create permissions hierarchy in `@app/data/rbac.php`:
```php
<?php
use yii\rbac\Item;
use app\rbac\NotGuestRule;
$notGuest = new NotGuestRule();
return [
// HERE ARE YOUR MANAGEMENT TASKS
'manageThing0' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing1' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing3' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
// AND THE ROLES
'guest' => [
'type' => Item::TYPE_ROLE,
'description' => 'Guest',
'bizRule' => NULL,
'data' => NULL
'rules' => [
$rule->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
'user' => [
'type' => Item::TYPE_ROLE,
'description' => 'User',
'children' => [
'guest',
'manageThing0', // User can edit thing0
],
'ruleName' => $notGuest->name,
'data' => NULL
],
'bizRule' => 'return !Yii::$app->user->isGuest;',
'data' => NULL
],
'moderator' => [
'type' => Item::TYPE_ROLE,
'description' => 'Moderator',
'children' => [
'user', // Can manage all that user can
'manageThing1', // and also thing1
'moderator' => [
'type' => Item::TYPE_ROLE,
'description' => 'Moderator',
'children' => [
'user', // Can manage all that user can
'manageThing1', // and also thing1
],
'ruleName' => NULL,
'data' => NULL
],
'bizRule' => 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
'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
],
'bizRule' => NULL,
'data' => NULL
],
'godmode' => [
'type' => Item::TYPE_ROLE,
'description' => 'Super admin',
'children' => [
'admin', // can do all that admin can
'manageThing3', // and also thing3
'godmode' => [
'type' => Item::TYPE_ROLE,
'description' => 'Super admin',
'children' => [
'admin', // can do all that admin can
'manageThing3', // and also thing3
],
'ruleName' => NULL,
'data' => NULL
],
'bizRule' => NULL,
'data' => NULL
],
];
```
......
......@@ -79,6 +79,7 @@ Yii Framework 2 Change Log
- Enh #46: Added Image extension based on [Imagine library](http://imagine.readthedocs.org) (tonydspaniard)
- Enh #364: Improve Inflector::slug with `intl` transliteration. Improved transliteration char map. (tonydspaniard)
- Enh #497: Removed `\yii\log\Target::logUser` and added `\yii\log\Target::prefix` to support customizing message prefix (qiangxue)
- Enh #499: Decoupled `Rule` from RBAC `Item` (samdark, qiangxue)
- Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue)
- Enh #802: Added support for retrieving sub-array element or child object property through `ArrayHelper::getValue()` (qiangxue, cebe)
- Enh #938: Added `yii\web\View::renderAjax()` and `yii\web\Controller::renderAjax()` (qiangxue)
......
......@@ -27,9 +27,9 @@ class Assignment extends Object
*/
public $manager;
/**
* @var string the business rule associated with this assignment
* @var string name of the rule associated with this assignment
*/
public $bizRule;
public $ruleName;
/**
* @var mixed additional data for this assignment
*/
......
......@@ -40,9 +40,9 @@ class Item extends Object
*/
public $description;
/**
* @var string the business rule associated with this item
* @var string name of the rule associated with this item
*/
public $bizRule;
public $ruleName;
/**
* @var mixed the additional data associated with this item
*/
......@@ -66,7 +66,7 @@ class Item extends Object
public function checkAccess($itemName, $params = [])
{
Yii::trace('Checking permission: ' . $this->_name, __METHOD__);
if ($this->manager->executeBizRule($this->bizRule, $params, $this->data)) {
if ($this->manager->executeRule($this->ruleName, $params, $this->data)) {
if ($this->_name == $itemName) {
return true;
}
......@@ -146,17 +146,18 @@ class Item extends Object
/**
* Assigns this item to a user.
*
* @param mixed $userId the user ID (see [[\yii\web\User::id]])
* @param string $bizRule the business rule to be executed when [[checkAccess()]] is called
* @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
*/
public function assign($userId, $bizRule = null, $data = null)
public function assign($userId, Rule $rule = null, $data = null)
{
return $this->manager->assign($userId, $this->_name, $bizRule, $data);
return $this->manager->assign($userId, $this->_name, $rule, $data);
}
/**
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
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.
*
* @author Alexander Makarov <sam@rmcreative.ru>
* @since 2.0
*/
abstract class Rule extends Object
{
/**
* @var string name of the rule
*/
public $name;
/**
* 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.
*/
abstract public function execute($params, $data);
}
......@@ -12,15 +12,24 @@
drop table if exists [auth_assignment];
drop table if exists [auth_item_child];
drop table if exists [auth_item];
drop table if exists [auth_rule];
create table [auth_rule]
(
[name] varchar(64) not null,
[data] text,
primary key ([name])
);
create table [auth_item]
(
[name] varchar(64) not null,
[type] integer not null,
[description] text,
[biz_rule] text,
[rule_name] varchar(64),
[data] text,
primary key ([name]),
foreign key ([rule_name]) references [auth_rule] ([name]) on delete set null on update cascade,
key [type] ([type])
);
......@@ -37,8 +46,9 @@ create table [auth_assignment]
(
[item_name] varchar(64) not null,
[user_id] varchar(64) not null,
[biz_rule] text,
[rule_name] varchar(64),
[data] text,
primary key ([item_name],[user_id]),
foreign key ([item_name]) references [auth_item] ([name]) on delete cascade on update cascade
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
);
......@@ -12,15 +12,24 @@
drop table if exists `auth_assignment`;
drop table if exists `auth_item_child`;
drop table if exists `auth_item`;
drop table if exists `auth_rule`;
create table `auth_rule`
(
`name` varchar(64) not null,
`data` text,
primary key (`name`)
) engine InnoDB;
create table `auth_item`
(
`name` varchar(64) not null,
`type` integer not null,
`description` text,
`biz_rule` text,
`rule_name` varchar(64),
`data` text,
primary key (`name`),
foreign key (`rule_name`) references `auth_rule` (`name`) on delete set null on update cascade,
key `type` (`type`)
) engine InnoDB;
......@@ -28,7 +37,7 @@ create table `auth_item_child`
(
`parent` varchar(64) not null,
`child` varchar(64) not null,
primary key (`parent`,`child`),
primary key (`parent`, `child`),
foreign key (`parent`) references `auth_item` (`name`) on delete cascade on update cascade,
foreign key (`child`) references `auth_item` (`name`) on delete cascade on update cascade
) engine InnoDB;
......@@ -37,8 +46,9 @@ create table `auth_assignment`
(
`item_name` varchar(64) not null,
`user_id` varchar(64) not null,
`biz_rule` text,
`rule_name` varchar(64),
`data` text,
primary key (`item_name`,`user_id`),
foreign key (`item_name`) references `auth_item` (`name`) on delete cascade on update cascade
) engine InnoDB;
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
......@@ -12,15 +12,24 @@
drop table if exists "auth_assignment";
drop table if exists "auth_item_child";
drop table if exists "auth_item";
drop table if exists "auth_rule";
create table "auth_rule"
(
"name" varchar(64) not null,
"data" text,
primary key ("name")
);
create table "auth_item"
(
"name" varchar(64) not null,
"type" integer not null,
"description" text,
"biz_rule" text,
"rule_name" varchar(64),
"data" text,
primary key ("name"),
foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade,
key "type" ("type")
);
......@@ -37,8 +46,9 @@ create table "auth_assignment"
(
"item_name" varchar(64) not null,
"user_id" varchar(64) not null,
"biz_rule" text,
"rule_name" varchar(64),
"data" text,
primary key ("item_name","user_id"),
foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade
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
);
......@@ -12,15 +12,24 @@
drop table if exists "auth_assignment";
drop table if exists "auth_item_child";
drop table if exists "auth_item";
drop table if exists "auth_rule";
create table "auth_rule"
(
"name" varchar(64) not null,
"data" text,
primary key ("name")
);
create table "auth_item"
(
"name" varchar(64) not null,
"type" integer not null,
"description" text,
"biz_rule" text,
"rule_name" varchar(64),
"data" text,
primary key ("name")
primary key ("name"),
foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade
);
create index auth_item_type_idx on "auth_item" ("type");
......@@ -38,8 +47,9 @@ create table "auth_assignment"
(
"item_name" varchar(64) not null,
"user_id" varchar(64) not null,
"biz_rule" text,
"rule_name" varchar(64),
"data" text,
primary key ("item_name","user_id"),
foreign key ("item_name") references "auth_item" ("name") on delete cascade on update cascade
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
);
......@@ -9,36 +9,47 @@
* @since 2.0
*/
drop table if exists 'auth_assignment';
drop table if exists 'auth_item_child';
drop table if exists 'auth_item';
drop table if exists "auth_assignment";
drop table if exists "auth_item_child";
drop table if exists "auth_item";
drop table if exists "auth_rule";
create table 'auth_item'
create table "auth_rule"
(
"name" varchar(64) not null,
"data" text,
primary key ("name")
);
create table "auth_item"
(
"name" varchar(64) not null,
"type" integer not null,
"description" text,
"biz_rule" text,
"rule_name" varchar(64),
"data" text,
primary key ("name"),
key "type" ("type")
foreign key ("rule_name") references "auth_rule" ("name") on delete set null on update cascade
);
create table 'auth_item_child'
create index "auth_item_type_idx" on "auth_item" ("type");
create table "auth_item_child"
(
"parent" varchar(64) not null,
"child" varchar(64) not null,
primary key ("parent","child"),
foreign key ("parent") references 'auth_item' ("name") on delete cascade on update cascade,
foreign key ("child") references 'auth_item' ("name") on delete cascade on update cascade
foreign key ("parent") references "auth_item" ("name") on delete cascade on update cascade,
foreign key ("child") references "auth_item" ("name") on delete cascade on update cascade
);
create table 'auth_assignment'
create table "auth_assignment"
(
"item_name" varchar(64) not null,
"user_id" varchar(64) not null,
"biz_rule" text,
"rule_name" varchar(64),
"data" text,
primary key ("item_name","user_id"),
foreign key ("item_name") references 'auth_item' ("name") on delete cascade on update cascade
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
);
<?php
namespace yiiunit\framework\rbac;
use yii\rbac\Rule;
/**
* Checks if authorID matches userID passed via params
*/
class AuthorRule extends Rule
{
public $name = 'isAuthor';
public $reallyReally = false;
/**
* @inheritdoc
*/
public function execute($params, $data)
{
return $params['authorID'] == $params['userID'];
}
}
\ No newline at end of file
<?php
namespace yiiunit\framework\rbac;
use yii\db\Connection;
use yii\rbac\DbManager;
/**
* DbManagerTestCase
*/
abstract class DbManagerTestCase extends ManagerTestCase
{
protected $database;
protected $driverName = 'mysql';
/**
* @var Connection
*/
protected $db;
protected function setUp()
{
parent::setUp();
$databases = $this->getParam('databases');
$this->database = $databases[$this->driverName];
$pdo_database = 'pdo_'.$this->driverName;
if (!extension_loaded('pdo') || !extension_loaded($pdo_database)) {
$this->markTestSkipped('pdo and '.$pdo_database.' extension are required.');
}
$this->auth = new DbManager(['db' => $this->getConnection()]);
$this->auth->init();
$this->prepareData();
}
protected function tearDown()
{
parent::tearDown();
if ($this->db) {
$this->db->close();
}
$this->destroyApplication();
}
/**
* @param boolean $reset whether to clean up the test database
* @param boolean $open whether to open and populate test database
* @throws \yii\base\InvalidParamException
* @throws \yii\db\Exception
* @throws \yii\base\InvalidConfigException
* @return \yii\db\Connection
*/
public function getConnection($reset = true, $open = true)
{
if (!$reset && $this->db) {
return $this->db;
}
$db = new Connection;
$db->dsn = $this->database['dsn'];
if (isset($this->database['username'])) {
$db->username = $this->database['username'];
$db->password = $this->database['password'];
}
if (isset($this->database['attributes'])) {
$db->attributes = $this->database['attributes'];
}
if ($open) {
$db->open();
$lines = explode(';', file_get_contents(\Yii::getAlias('@yii/rbac/schema-'.$this->driverName.'.sql')));
foreach ($lines as $line) {
if (trim($line) !== '') {
$db->pdo->exec($line);
}
}
}
$this->db = $db;
return $db;
}
}
......@@ -8,7 +8,7 @@ use yiiunit\TestCase;
abstract class ManagerTestCase extends TestCase
{
/** @var \yii\rbac\PhpManager|\yii\rbac\DbManager */
/** @var \yii\rbac\Manager */
protected $auth;
public function testCreateItem()
......@@ -16,24 +16,24 @@ abstract class ManagerTestCase extends TestCase
$type = Item::TYPE_TASK;
$name = 'editUser';
$description = 'edit a user';
$bizRule = 'checkUserIdentity()';
$ruleName = 'isAuthor';
$data = [1, 2, 3];
$item = $this->auth->createItem($name, $type, $description, $bizRule, $data);
$item = $this->auth->createItem($name, $type, $description, $ruleName, $data);
$this->assertTrue($item instanceof Item);
$this->assertEquals($item->type, $type);
$this->assertEquals($item->name, $name);
$this->assertEquals($item->description, $description);
$this->assertEquals($item->bizRule, $bizRule);
$this->assertEquals($item->ruleName, $ruleName);
$this->assertEquals($item->data, $data);
// test shortcut
$name2 = 'createUser';
$item2 = $this->auth->createRole($name2, $description, $bizRule, $data);
$item2 = $this->auth->createRole($name2, $description, $ruleName, $data);
$this->assertEquals($item2->type, Item::TYPE_ROLE);
// test adding an item with the same name
$this->setExpectedException('\yii\base\Exception');
$this->auth->createItem($name, $type, $description, $bizRule, $data);
$this->auth->createItem($name, $type, $description, $ruleName, $data);
}
public function testGetItem()
......@@ -98,11 +98,11 @@ abstract class ManagerTestCase extends TestCase
public function testAssign()
{
$auth = $this->auth->assign('new user', 'createPost', 'rule', 'data');
$auth = $this->auth->assign('new user', 'createPost', 'isAuthor', 'data');
$this->assertTrue($auth instanceof Assignment);
$this->assertEquals($auth->userId, 'new user');
$this->assertEquals($auth->itemName, 'createPost');
$this->assertEquals($auth->bizRule, 'rule');
$this->assertEquals($auth->ruleName, 'isAuthor');
$this->assertEquals($auth->data, 'data');
$this->setExpectedException('\yii\base\Exception');
......@@ -168,14 +168,79 @@ abstract class ManagerTestCase extends TestCase
$this->auth->addItemChild('readPost', 'readPost');
}
public function testExecuteBizRule()
public function testGetRule()
{
$this->assertTrue($this->auth->executeBizRule(null, [], null));
$this->assertTrue($this->auth->executeBizRule('return 1 == true;', [], null));
$this->assertTrue($this->auth->executeBizRule('return $params[0] == $params[1];', [1, '1'], null));
if (!defined('HHVM_VERSION')) { // invalid code crashes on HHVM
$this->assertFalse($this->auth->executeBizRule('invalid;', [], null));
$rule = $this->auth->getRule('isAuthor');
$this->assertInstanceOf('yii\rbac\Rule', $rule);
$this->assertEquals('isAuthor', $rule->name);
$rule = $this->auth->getRule('nonExisting');
$this->assertNull($rule);
}
public function testInsertRule()
{
$ruleName = 'isReallyReallyAuthor';
$rule = new AuthorRule(['name' => $ruleName, 'reallyReally' => true]);
$this->auth->insertRule($rule);
/** @var AuthorRule $rule */
$rule = $this->auth->getRule($ruleName);
$this->assertEquals($ruleName, $rule->name);
$this->assertEquals(true, $rule->reallyReally);
}
public function testUpdateRule()
{
$rule = $this->auth->getRule('isAuthor');
$rule->name = "newName";
$rule->reallyReally = false;
$this->auth->updateRule('isAuthor', $rule);
/** @var AuthorRule $rule */
$rule = $this->auth->getRule('isAuthor');
$this->assertEquals(null, $rule);
$rule = $this->auth->getRule('newName');
$this->assertEquals("newName", $rule->name);
$this->assertEquals(false, $rule->reallyReally);
$rule->reallyReally = true;
$this->auth->updateRule('newName', $rule);
$rule = $this->auth->getRule('newName');
$this->assertEquals(true, $rule->reallyReally);
}
public function testGetRules()
{
$rule = new AuthorRule(['name' => 'isReallyReallyAuthor', 'reallyReally' => true]);
$this->auth->insertRule($rule);
$rules = $this->auth->getRules();
$ruleNames = [];
foreach ($rules as $rule) {
$ruleNames[] = $rule->name;
}
$this->assertContains('isReallyReallyAuthor', $ruleNames);
$this->assertContains('isAuthor', $ruleNames);
}
public function testRemoveRule()
{
$this->auth->removeRule('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()
......@@ -231,12 +296,14 @@ abstract class ManagerTestCase extends TestCase
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', 'return $params["authorID"] == $params["userID"];');
$task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'isAuthor');
$task->addChild('updatePost');
$role = $this->auth->createRole('reader');
......
<?php
namespace yiiunit\framework\rbac;
/**
* MySQLManagerTest
*/
class MySQLManagerTest extends DbManagerTestCase
{
}
<?php
namespace yiiunit\framework\rbac;
/**
* PgSQLManagerTest
*/
class PgSQLManagerTest extends DbManagerTestCase
{
protected $driverName = 'pgsql';
}
......@@ -7,6 +7,7 @@ use yii\rbac\PhpManager;
/**
* @group rbac
* @property \yii\rbac\PhpManager $auth
*/
class PhpManagerTest extends ManagerTestCase
{
......
<?php
namespace yiiunit\framework\rbac;
/**
* SqliteManagerTest
*/
class SqliteManagerTest extends DbManagerTestCase
{
protected $driverName = 'sqlite';
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment