diff --git a/.travis.yml b/.travis.yml index 5d00f88..ded8aeb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ services: before_script: - composer self-update && composer --version - - composer require satooshi/php-coveralls 0.6.* --dev - - composer require guzzle/http v3.7.3 --dev + - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist + - composer require guzzle/http v3.7.3 --dev --prefer-dist - mysql -e 'CREATE DATABASE yiitest;'; - psql -U postgres -c 'CREATE DATABASE yiitest;'; - tests/unit/data/travis/apc-setup.sh diff --git a/composer.json b/composer.json index 8da3f1c..5ab93fa 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,7 @@ "irc": "irc://irc.freenode.net/yii", "source": "https://github.com/yiisoft/yii2" }, + "minimum-stability": "dev", "replace": { "yiisoft/yii2-bootstrap": "self.version", "yiisoft/yii2-debug": "self.version", diff --git a/docs/guide/index.md b/docs/guide/index.md index 91c4210..666de98 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -56,7 +56,7 @@ Security and access control - [Authorization](authorization.md) - Access control and RBAC - [Security](security.md) - Hashing and verifying passwords, encryption - [Views security](view.md#security) - how to prevent XSS -- Role based access control +- [RBAC](rbac.md) - Role-based Access Control Data providers, lists and grids =============================== @@ -84,4 +84,4 @@ References ========== - [Model validation reference](validation.md) -- [Official Composer documentation](http://getcomposer.org) \ No newline at end of file +- [Official Composer documentation](http://getcomposer.org) diff --git a/docs/guide/model.md b/docs/guide/model.md index 465cb16..abf2b48 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -94,7 +94,7 @@ few sections, the concept of scenarios is mainly used for data validation and ma Associated with each scenario is a list of attributes that are *active* in that particular scenario. For example, in the `login` scenario, only the `username` and `password` attributes are active; while in the `register` scenario, -additional attributes such as `email` are *active*. +additional attributes such as `email` are *active*. When an attribute is *active* this means that it is subject to validation. Possible scenarios should be listed in the `scenarios()` method. This method returns an array whose keys are the scenario names and whose values are lists of attributes that should be active in that scenario: @@ -119,6 +119,9 @@ We may do so by prefixing an exclamation character to the attribute name when de ['username', 'password', '!secret'] ``` +In this example `username`, `password` and `secret` are *active* attributes but only `username` and `password` are +considered safe for massive assignment. + Identifying the active model scenario can be done using one of the following approaches: ```php @@ -136,13 +139,17 @@ class EmployeeController extends \yii\web\Controller // third way $employee = Employee::find()->where('id = :id', [':id' => $id])->one(); if ($employee !== null) { - $employee->setScenario('managementPanel'); + $employee->scenario = 'managementPanel'; } } } ``` -The example above presumes that the model is based upon [Active Record](active-record.md). For basic form models, scenarios are rarely needed, as the basic form model is normally tied directly to a single form. +The example above presumes that the model is based upon [Active Record](active-record.md). For basic form models, +scenarios are rarely needed, as the basic form model is normally tied directly to a single form. +The default implementation of the `scenarios()`-method will return all scenarios found in the `rules()` +declaration (explained in the next section) so in simple cases you do not need to define scenarios. + Validation ---------- @@ -170,11 +177,11 @@ instance of a [[\yii\validators\Validator]] child class, or an array with the fo ```php [ - 'attribute1, attribute2, ...', + ['attribute1', 'attribute2', ...], 'validator class or alias', // specifies in which scenario(s) this rule is active. // if not given, it means it is active in all scenarios - 'on' => 'scenario1, scenario2, ...', + 'on' => ['scenario1', 'scenario2', ...], // the following name-value pairs will be used // to initialize the validator properties 'property1' => 'value1', diff --git a/docs/guide/rbac.md b/docs/guide/rbac.md new file mode 100644 index 0000000..28d8f5c --- /dev/null +++ b/docs/guide/rbac.md @@ -0,0 +1,122 @@ +Using RBAC +=========== + +Lacking proper documentation, this guide is a stub copied from a [topic on the forum](http://www.yiiframework.com/forum/index.php/topic/49104-does-anyone-have-a-working-example-of-rbac/page__view__findpost__p__229098). + + +First af all, you modify your config (web.php or main.php), +```php +'authManager' => [ + 'class' => 'app\components\PhpManager', // THIS IS YOUR AUTH MANAGER + 'defaultRoles' => ['guest'], +], +``` + +Next, create the manager itself (app/components/PhpManager.php) +```php +authFile === NULL) + $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; // HERE GOES YOUR RBAC TREE FILE + + parent::init(); + + if (!Yii::$app->user->isGuest) { + $this->assign(Yii::$app->user->identity->id, Yii::$app->user->identity->role); // we suppose that user's role is stored in identity + } + } +} +``` + +Now, the rules tree (@app/data/rbac.php): +```php + ['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], + 'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL], + + // AND THE ROLES + 'guest' => [ + 'type' => Item::TYPE_ROLE, + 'description' => 'Guest', + 'bizRule' => NULL, + 'data' => NULL + ], + + 'user' => [ + 'type' => Item::TYPE_ROLE, + 'description' => 'User', + 'children' => [ + 'guest', + 'manageThing0', // User can edit thing0 + ], + '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 + ], + '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 + ], + 'bizRule' => NULL, + 'data' => NULL + ], + + 'godmode' => [ + 'type' => Item::TYPE_ROLE, + 'description' => 'Super admin', + 'children' => [ + 'admin', // can do all that admin can + 'manageThing3', // and also thing3 + ], + 'bizRule' => NULL, + 'data' => NULL + ], + +]; +``` + +As a result, you can now add access control filters to controllers +```php +public function behaviors() +{ + return [ + 'access' => [ + 'class' => 'yii\web\AccessControl', + 'except' => ['something'], + 'rules' => [ + [ + 'allow' => true, + 'roles' => ['manageThing1'], + ], + ], + ], + ]; +} +``` diff --git a/framework/yii/data/Sort.php b/framework/yii/data/Sort.php index 7612641..8a1b36c 100644 --- a/framework/yii/data/Sort.php +++ b/framework/yii/data/Sort.php @@ -12,6 +12,7 @@ use yii\base\InvalidConfigException; use yii\base\Object; use yii\helpers\Html; use yii\helpers\Inflector; +use yii\web\Request; /** * Sort represents information relevant to sorting. @@ -240,7 +241,10 @@ class Sort extends Object { if ($this->_attributeOrders === null || $recalculate) { $this->_attributeOrders = []; - $params = $this->params === null ? $_GET : $this->params; + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->get() : []; + } if (isset($params[$this->sortVar]) && is_scalar($params[$this->sortVar])) { $attributes = explode($this->separators[0], $params[$this->sortVar]); foreach ($attributes as $attribute) { @@ -332,7 +336,10 @@ class Sort extends Object */ public function createUrl($attribute) { - $params = $this->params === null ? $_GET : $this->params; + if (($params = $this->params) === null) { + $request = Yii::$app->getRequest(); + $params = $request instanceof Request ? $request->get() : []; + } $params[$this->sortVar] = $this->createSortVar($attribute); $route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; $urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; diff --git a/framework/yii/helpers/BaseSecurity.php b/framework/yii/helpers/BaseSecurity.php index 8e86654..03af81d 100644 --- a/framework/yii/helpers/BaseSecurity.php +++ b/framework/yii/helpers/BaseSecurity.php @@ -75,6 +75,9 @@ class BaseSecurity */ public static function decrypt($data, $password) { + if ($data === null) { + return null; + } $module = static::openCryptModule(); $ivSize = mcrypt_enc_get_iv_size($module); $iv = StringHelper::substr($data, 0, $ivSize); diff --git a/framework/yii/rbac/DbManager.php b/framework/yii/rbac/DbManager.php index 5bbc7ab..e9e1730 100644 --- a/framework/yii/rbac/DbManager.php +++ b/framework/yii/rbac/DbManager.php @@ -277,6 +277,18 @@ class DbManager extends Manager } /** + * Revokes all authorization assignments from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether removal is successful + */ + public function revokeAll($userId) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, ['user_id' => $userId]) + ->execute() > 0; + } + + /** * Returns a value indicating whether the item has been assigned to the user. * @param mixed $userId the user ID (see [[User::id]]) * @param string $itemName the item name diff --git a/framework/yii/rbac/Manager.php b/framework/yii/rbac/Manager.php index 1710a77..a1bf47a 100644 --- a/framework/yii/rbac/Manager.php +++ b/framework/yii/rbac/Manager.php @@ -269,6 +269,12 @@ abstract class Manager extends Component */ abstract public function revoke($userId, $itemName); /** + * Revokes all authorization assignments from a user. + * @param mixed $userId the user ID (see [[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 [[User::id]]) * @param string $itemName the item name diff --git a/framework/yii/rbac/PhpManager.php b/framework/yii/rbac/PhpManager.php index 57ede09..78e4d8c 100644 --- a/framework/yii/rbac/PhpManager.php +++ b/framework/yii/rbac/PhpManager.php @@ -221,6 +221,22 @@ class PhpManager extends Manager } /** + * Revokes all authorization assignments from a user. + * @param mixed $userId the user ID (see [[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 [[User::id]]) * @param string $itemName the item name diff --git a/framework/yii/redis/ActiveQuery.php b/framework/yii/redis/ActiveQuery.php index eabd843..2174901 100644 --- a/framework/yii/redis/ActiveQuery.php +++ b/framework/yii/redis/ActiveQuery.php @@ -56,7 +56,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface * Executes the query and returns all results as an array. * @param Connection $db the database connection used to execute the query. * If this parameter is not given, the `db` application component will be used. - * @return ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. + * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned. */ public function all($db = null) { @@ -215,20 +215,20 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface /** * Returns the query result as a scalar value. - * The value returned will be the first column in the first row of the query results. - * @param string $column name of the column to select + * The value returned will be the specified attribute in the first record of the query results. + * @param string $attribute name of the attribute to select * @param Connection $db the database connection used to execute the query. * If this parameter is not given, the `db` application component will be used. - * @return string|boolean the value of the first column in the first row of the query result. - * False is returned if the query result is empty. + * @return string the value of the specified attribute in the first record of the query result. + * Null is returned if the query result is empty. */ - public function scalar($column, $db = null) + public function scalar($attribute, $db = null) { $record = $this->one($db); - if ($record === null) { - return false; + if ($record !== null) { + return $record->$attribute; } else { - return $record->$column; + return null; } } diff --git a/framework/yii/web/UrlRule.php b/framework/yii/web/UrlRule.php index af227cd..a2e34f9 100644 --- a/framework/yii/web/UrlRule.php +++ b/framework/yii/web/UrlRule.php @@ -288,7 +288,7 @@ class UrlRule extends Object // match params in the pattern foreach ($this->_paramRules as $name => $rule) { - if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { + if (isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) { $tr["<$name>"] = urlencode($params[$name]); unset($params[$name]); } elseif (!isset($this->defaults[$name]) || isset($params[$name])) { diff --git a/tests/unit/framework/data/SortTest.php b/tests/unit/framework/data/SortTest.php index dca2fcb..c21990e 100644 --- a/tests/unit/framework/data/SortTest.php +++ b/tests/unit/framework/data/SortTest.php @@ -19,6 +19,12 @@ use yii\data\Sort; */ class SortTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testGetOrders() { $sort = new Sort([ diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index 6aa2a45..a2b5bee 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -40,6 +40,12 @@ class Post3 extends Object */ class ArrayHelperTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testToArray() { $object = new Post1; diff --git a/tests/unit/framework/rbac/ManagerTestCase.php b/tests/unit/framework/rbac/ManagerTestCase.php index 3bf80ad..cbf8de8 100644 --- a/tests/unit/framework/rbac/ManagerTestCase.php +++ b/tests/unit/framework/rbac/ManagerTestCase.php @@ -119,6 +119,12 @@ abstract class ManagerTestCase extends TestCase $this->assertFalse($this->auth->revoke('author B', 'author')); } + public function testRevokeAll() + { + $this->assertTrue($this->auth->revokeAll('reader E')); + $this->assertFalse($this->auth->isAssigned('reader E', 'reader')); + } + public function testGetAssignments() { $this->auth->assign('author B', 'deletePost'); @@ -201,6 +207,13 @@ abstract class ManagerTestCase extends TestCase 'updateOwnPost' => false, 'deletePost' => true, ], + 'reader E' => [ + 'createPost' => false, + 'readPost' => false, + 'updatePost' => false, + 'updateOwnPost' => false, + 'deletePost' => false, + ], ]; $params = ['authorID' => 'author B']; @@ -245,5 +258,6 @@ abstract class ManagerTestCase extends TestCase $this->auth->assign('author B', 'author'); $this->auth->assign('editor C', 'editor'); $this->auth->assign('admin D', 'admin'); + $this->auth->assign('reader E', 'reader'); } }