Merge pull request #14123 from cebe/access-rule-params

Alternative implementation for passing parameters to AccessRule roles
This commit is contained in:
Carsten Brandt
2017-05-31 17:48:02 +02:00
committed by GitHub
4 changed files with 122 additions and 13 deletions

View File

@ -100,6 +100,9 @@ The comparison is case-sensitive. If this option is empty or not set, it means t
Using other role names will trigger the invocation of [[yii\web\User::can()]], which requires enabling RBAC
(to be described in the next subsection). If this option is empty or not set, it means this rule applies to all roles.
* [[yii\filters\AccessRule::roleParams|roleParams]]: specifies the parameters that will be passed to [[yii\web\User::can()]].
See the section below describing RBAC rules to see how it can be used. If this option is empty or not set, then no parameters will be passed.
* [[yii\filters\AccessRule::ips|ips]]: specifies which [[yii\web\Request::userIP|client IP addresses]] this rule matches.
An IP address can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix.
For example, '192.168.*' matches all IP addresses in the segment '192.168.'. If this option is empty or not set,
@ -356,6 +359,7 @@ created previously author cannot edit his own post. Let's fix it. First we need
namespace app\rbac;
use yii\rbac\Rule;
use app\models\Post;
/**
* Checks if authorID matches user passed via params
@ -487,6 +491,35 @@ public function behaviors()
If all the CRUD operations are managed together then it's a good idea to use a single permission, like `managePost`, and
check it in [[yii\web\Controller::beforeAction()]].
In the above example, no parameters are passed with the roles specified for accessing an action, but in case of the
`updatePost` permission, we need to pass a `post` parameter for it to work properly.
You can pass parameters to [[yii\web\User::can()]] by specifying [[yii\filters\AccessRule::roleParams|roleParams]] on
the access rule:
```php
[
'allow' => true,
'actions' => ['update'],
'roles' => ['updatePost'],
'roleParams' => function() {
return ['post' => Post::findOne(Yii::$app->request->get('id'))];
},
],
```
In the above example, [[yii\filters\AccessRule::roleParams|roleParams]] is a Closure that will be evaluated when
the access rule is checked, so the model will only be loaded when needed.
If the creation of role parameters is a simple operation, you may just specify an array, like so:
```php
[
'allow' => true,
'actions' => ['update'],
'roles' => ['updatePost'],
'roleParams' => ['postId' => Yii::$app->request->get('id')];
],
```
### Using Default Roles <span id="using-default-roles"></span>
A default role is a role that is *implicitly* assigned to *all* users. The call to [[yii\rbac\ManagerInterface::assign()]]

View File

@ -59,6 +59,7 @@ Yii Framework 2 Change Log
- Bug #14074: Fixed default value of `yii\console\controllers\FixtureController::$globalFixtures` to contain valid class name (lynicidn)
- Bug #14094: Fixed bug when single `yii\web\UrlManager::createUrl()` call my result multiple calls of `yii\web\UrlRule::createUrl()` for the same rule (rossoneri)
- Bug #14133: Fixed bug when calculating timings with mixed nested profile begin and end in `yii\log\Logger::calculateTimings()` (bizley)
- Enh #8426: `yii\filters\AccessRule` now allows passing parameters to the role checking function (fsateler, cebe, Faryshta)
- Enh #8641: Enhanced `yii\console\Request::resolve()` to prevent passing parameters, that begin from digits (silverfire)
- Enh #11288: Added support for caching of `yii\web\UrlRule::createUrl()` results in `yii\web\UrlManager` for rules with defaults (rob006)
- Enh #12528: Added option to disable query logging and profiling in DB command (cebe)

View File

@ -7,6 +7,7 @@
namespace yii\filters;
use Closure;
use yii\base\Component;
use yii\base\Action;
use yii\base\InvalidConfigException;
@ -55,8 +56,43 @@ class AccessRule extends Component
* In this case, [[User::can()]] will be called to check access.
*
* If this property is not set or empty, it means this rule applies to all roles.
* @see $roleParams
*/
public $roles;
/**
* @var array|Closure parameters to pass to the [[User::can()]] function for evaluating
* user permissions in [[$roles]].
*
* If this is an array, it will be passed directly to [[User::can()]]. For example for passing an
* ID from the current request, you may use the following:
*
* ```php
* ['postId' => Yii::$app->request->get('id')]
* ```
*
* You may also specify a closure that returns an array. This can be used to
* evaluate the array values only if they are needed, for example when a model needs to be
* loaded like in the following code:
*
* ```php
* 'rules' => [
* [
* 'allow' => true,
* 'actions' => ['update'],
* 'roles' => ['updatePost'],
* 'roleParams' => function($rule) {
* return ['post' => Post::findOne(Yii::$app->request->get('id'))];
* },
* ],
* ],
* ```
*
* A reference to the [[AccessRule]] instance will be passed to the closure as the first parameter.
*
* @see $roles
* @since 2.0.12
*/
public $roleParams = [];
/**
* @var array list of user IP addresses that this rule applies to. An IP address
* can contain the wildcard `*` at the end so that it matches IP addresses with the same prefix.
@ -161,8 +197,13 @@ class AccessRule extends Component
if (!$user->getIsGuest()) {
return true;
}
} elseif ($user->can($role)) {
return true;
} else {
if (!isset($roleParams)) {
$roleParams = $this->roleParams instanceof Closure ? call_user_func($this->roleParams, $this) : $this->roleParams;
}
if ($user->can($role, $roleParams)) {
return true;
}
}
}

View File

@ -2,6 +2,7 @@
namespace yiiunit\framework\filters;
use Closure;
use Yii;
use yii\base\Action;
use yii\filters\AccessRule;
@ -10,6 +11,7 @@ use yii\web\Request;
use yii\web\User;
use yiiunit\framework\filters\stubs\MockAuthManager;
use yiiunit\framework\filters\stubs\UserIdentity;
use yiiunit\framework\rbac\AuthorRule;
/**
* @group filters
@ -81,10 +83,20 @@ class AccessRuleTest extends \yiiunit\TestCase
$updatePost->description = 'Update post';
$auth->add($updatePost);
// add "updateOwnPost" permission
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update post';
$updateRule = new AuthorRule();
$auth->add($updateRule);
$updateOwnPost->ruleName = $updateRule->name;
$auth->add($updateOwnPost);
$auth->addChild($updateOwnPost, $updatePost);
// add "author" role and give this role the "createPost" permission
$author = $auth->createRole('author');
$auth->add($author);
$auth->addChild($author, $createPost);
$auth->addChild($author, $updateOwnPost);
// add "admin" role and give this role the "updatePost" permission
// as well as the permissions of the "author" role
@ -142,14 +154,34 @@ class AccessRuleTest extends \yiiunit\TestCase
*/
public function matchRoleProvider() {
return [
['create', true, 'user1', true],
['create', true, 'user2', true],
['create', true, 'user3', null],
['create', true, 'unknown', null],
['create', false, 'user1', false],
['create', false, 'user2', false],
['create', false, 'user3', null],
['create', false, 'unknown', null],
['create', true, 'user1', [], true],
['create', true, 'user2', [], true],
['create', true, 'user3', [], null],
['create', true, 'unknown', [], null],
['create', false, 'user1', [], false],
['create', false, 'user2', [], false],
['create', false, 'user3', [], null],
['create', false, 'unknown', [], null],
// user2 is author, can only edit own posts
['update', true, 'user2', ['authorID' => 'user2'], true],
['update', true, 'user2', ['authorID' => 'user1'], null],
// user1 is admin, can update all posts
['update', true, 'user1', ['authorID' => 'user1'], true],
['update', true, 'user1', ['authorID' => 'user2'], true],
// unknown user can not edit anything
['update', true, 'unknown', ['authorID' => 'user1'], null],
['update', true, 'unknown', ['authorID' => 'user2'], null],
// user2 is author, can only edit own posts
['update', true, 'user2', function() { return ['authorID' => 'user2']; }, true],
['update', true, 'user2', function() { return ['authorID' => 'user1']; }, null],
// user1 is admin, can update all posts
['update', true, 'user1', function() { return ['authorID' => 'user1']; }, true],
['update', true, 'user1', function() { return ['authorID' => 'user2']; }, true],
// unknown user can not edit anything
['update', true, 'unknown', function() { return ['authorID' => 'user1']; }, null],
['update', true, 'unknown', function() { return ['authorID' => 'user2']; }, null],
];
}
@ -160,17 +192,19 @@ class AccessRuleTest extends \yiiunit\TestCase
* @param string $actionid the action id
* @param boolean $allow whether the rule should allow access
* @param string $userid the userid to check
* @param array|Closure $roleParams params for $roleParams
* @param boolean $expected the expected result or null
*/
public function testMatchRole($actionid, $allow, $userid, $expected) {
public function testMatchRole($actionid, $allow, $userid, $roleParams, $expected) {
$action = $this->mockAction();
$auth = $this->mockAuthManager();
$request = $this->mockRequest();
$rule = new AccessRule([
'allow' => $allow,
'roles' => ['createPost'],
'actions' => ['create'],
'roles' => [$actionid === 'create' ? 'createPost' : 'updatePost'],
'actions' => [$actionid],
'roleParams' => $roleParams,
]);
$action->id = $actionid;