security-authorization.md 19.6 KB
Newer Older
1 2 3
Authorization
=============

4
> Note: This section is under development.
Qiang Xue committed
5

6 7
Authorization is the process of verifying that a user has enough permission to do something. Yii provides two authorization
methods: Access Control Filter (ACF) and Role-Based Access Control (RBAC).
8

9 10

Access Control Filter
11 12
---------------------

13 14 15 16 17 18
Access Control Filter (ACF) is a simple authorization method that is best used by applications that only need some
simple access control. As its name indicates, ACF is an action filter that can be attached to a controller or a module
as a behavior. ACF will check a set of [[yii\filters\AccessControl::rules|access rules]] to make sure the current user
can access the requested action.

The code below shows how to use ACF which is implemented as [[yii\filters\AccessControl]]:
19 20

```php
21 22
use yii\filters\AccessControl;

23 24
class SiteController extends Controller
{
25 26 27 28
    public function behaviors()
    {
        return [
            'access' => [
29
                'class' => AccessControl::className(),
30 31 32 33
                'only' => ['login', 'logout', 'signup'],
                'rules' => [
                    [
                        'allow' => true,
34
                        'actions' => ['login', 'signup'],
35 36 37 38
                        'roles' => ['?'],
                    ],
                    [
                        'allow' => true,
39
                        'actions' => ['logout'],
40 41 42 43 44 45 46
                        'roles' => ['@'],
                    ],
                ],
            ],
        ];
    }
    // ...
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
}
```

In the code above ACF is attached to the `site` controller as a behavior. This is the typical way of using an action
filter. The `only` option specifies that the ACF should only be applied to `login`, `logout` and `signup` actions.
The `rules` option specifies the [[yii\filters\AccessRule|access rules]], which reads as follows:

- Allow all guest (not yet authenticated) users to access 'login' and 'signup' actions. The `roles` option
  contains a question mark `?` which is a special token recognized as "guests".
- Allow authenticated users to access 'logout' action. The `@` character is another special token recognized as
  authenticated users.

When ACF performs authorization check, it will examine the rules one by one from top to bottom until it finds
a match. The `allow` value of the matching rule will then be used to judge if the user is authorized. If none
of the rules matches, it means the user is NOT authorized and ACF will stop further action execution.

By default, ACF does only of the followings when it determines a user is not authorized to access the current action:

* If the user is a guest, it will call [[yii\web\User::loginRequired()]], which may redirect the browser to the login page.
* If the user is already authenticated, it will throw a [[yii\web\ForbiddenHttpException]].

You may customize this behavior by configuring the [[yii\filters\AccessControl::denyCallback]] property:

```php
[
    'class' => AccessControl::className(),
    'denyCallback' => function ($rule, $action) {
        throw new \Exception('You are not allowed to access this page');
    }
]
77 78
```

79 80 81 82 83 84 85 86 87 88 89 90 91 92
[[yii\filters\AccessRule|Access rules]] support many options. Below is a summary of the supported options.
You may also extend [[yii\filters\AccessRule]] to create your own customized access rule classes.

 * [[yii\filters\AccessRule::allow|allow]]: specifies whether this is an "allow" or "deny" rule.

 * [[yii\filters\AccessRule::actions|actions]]: specifies which actions this rule matches. This should
be an array of action IDs. The comparison is case-sensitive. If this option is empty or not set,
it means the rule applies to all actions.

 * [[yii\filters\AccessRule::controllers|controllers]]: specifies which controllers this rule
matches. This should be an array of controller IDs. The comparison is case-sensitive. If this option is
empty or not set, it means the rule applies to all controllers.

 * [[yii\filters\AccessRule::roles|roles]]: specifies which user roles that this rule matches.
93 94
   Two special roles are recognized, and they are checked via [[yii\web\User::isGuest]]:

95 96
     - `?`: matches a guest user (not authenticated yet)
     - `@`: matches an authenticated user
97 98 99

   Using other role names requires RBAC (to be described in the next section), and [[yii\web\User::can()]] will be called.
   If this option is empty or not set, it means this rule applies to all roles.
100

MarsuBoss committed
101
 * [[yii\filters\AccessRule::ips|ips]]: specifies which [[yii\web\Request::userIP|client IP addresses]] this rule matches.
102 103 104 105 106 107
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,
it means this rule applies to all IP addresses.

 * [[yii\filters\AccessRule::verbs|verbs]]: specifies which request method (e.g. `GET`, `POST`) this rule matches.
The comparison is case-insensitive.
108

109 110
 * [[yii\filters\AccessRule::matchCallback|matchCallback]]: specifies a PHP callable that should be called to determine
if this rule should be applied.
111

112 113
 * [[yii\filters\AccessRule::denyCallback|denyCallback]]: specifies a PHP callable that should be called when this rule
will deny the access.
114

115 116
Below is an example showing how to make use of the `matchCallback` option, which allows you to write arbitrary access
check logic:
117 118

```php
119 120
use yii\filters\AccessControl;

121 122
class SiteController extends Controller
{
123 124 125 126
    public function behaviors()
    {
        return [
            'access' => [
127
                'class' => AccessControl::className(),
128 129 130 131 132 133 134 135 136
                'only' => ['special-callback'],
                'rules' => [
                    [
                        'actions' => ['special-callback'],
                        'allow' => true,
                        'matchCallback' => function ($rule, $action) {
                            return date('d-m') === '31-10';
                        }
                    ],
137 138 139 140
                ],
            ],
        ];
    }
141

142 143 144 145 146
    // Match callback called! This page can be accessed only each October 31st
    public function actionSpecialCallback()
    {
        return $this->render('happy-halloween');
    }
147
}
148 149
```

150 151 152 153

Role based access control (RBAC)
--------------------------------

154
Role-Based Access Control (RBAC) provides a simple yet powerful centralized access control. Please refer to
155
the [Wikipedia](http://en.wikipedia.org/wiki/Role-based_access_control) for details about comparing RBAC
156 157 158
with other more traditional access control schemes.

Yii implements a General Hierarchical RBAC, following the [NIST RBAC model](http://csrc.nist.gov/rbac/sandhu-ferraiolo-kuhn-00.pdf).
159
It provides the RBAC functionality through the [[yii\rbac\ManagerInterface|authManager]] [application component](structure-application-components.md).
160 161 162 163 164 165 166 167 168

Using RBAC involves two parts of work. The first part is to build up the RBAC authorization data, and the second
part is to use the authorization data to perform access check in places where it is needed.

To facilitate our description next, we will first introduce some basic RBAC concepts.


### Basic Concepts

169
A role represents a collection of *permissions* (e.g. creating posts, updating posts). A role may be assigned
170 171 172 173 174
to one or multiple users. To check if a user has a specified permission, we may check if the user is assigned
with a role that contains that permission.

Associated with each role or permission, there may be a *rule*. A rule represents a piece of code that will be
executed during access check to determine if the corresponding role or permission applies to the current user.
175 176
For example, the "update post" permission may have a rule that checks if the current user is the post creator.
During access checking, if the user is NOT the post creator, he/she will be considered not having the "update post" permission.
177 178 179 180 181 182 183 184 185

Both roles and permissions can be organized in a hierarchy. In particular, a role may consist of other roles or permissions;
and a permission may consist of other permissions. Yii implements a *partial order* hierarchy which includes the
more special *tree* hierarchy. While a role can contain a permission, it is not true vice versa.


### Configuring RBAC Manager

Before we set off to define authorization data and perform access checking, we need to configure the
186
[[yii\base\Application::authManager|authManager]] application component. Yii provides two types of authorization managers:
187
[[yii\rbac\PhpManager]] and [[yii\rbac\DbManager]]. The former uses a PHP script file to store authorization
188
data, while the latter stores authorization data in a database. You may consider using the former if your application
189 190
does not require very dynamic role and permission management.

191 192 193
#### configuring authManager with `PhpManager`

The following code shows how to configure the `authManager` in the application configuration using the [[yii\rbac\PhpManager]] class:
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208

```php
return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
        ],
        // ...
    ],
];
```

The `authManager` can now be accessed via `\Yii::$app->authManager`.

209
> Tip: By default, [[yii\rbac\PhpManager]] stores RBAC data in files under `@app/rbac/` directory. Make sure the directory
210
  and all the files in it are writable by the Web server process if permissions hierarchy needs to be changed online.
sitawit committed
211

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
#### configuring authManager with `DbManager`

The following code shows how to configure the `authManager` in the application configuration using the [[yii\rbac\DbManager]] class:

```php
return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\DbManager',
        ],
        // ...
    ],
];
```

`DbManager` uses four database tables to store its data: 

- [[yii\rbac\DbManager::$itemTable|itemTable]]: the table for storing authorization items. Defaults to "auth_item".
- [[yii\rbac\DbManager::$itemChildTable|itemChildTable]]: the table for storing authorization item hierarchy. Defaults to "auth_item_child".
- [[yii\rbac\DbManager::$assignmentTable|assignmentTable]]: the table for storing authorization item assignments. Defaults to "auth_assignment".
- [[yii\rbac\DbManager::$ruleTable|ruleTable]]: the table for storing rules. Defaults to "auth_rule".

Before you can go on you need to create those tables in the database. To do this, you can use the migration stored in `@yii/rbac/migrations`:

`yii migrate --migrationPath=@yii/rbac/migrations`

The `authManager` can now be accessed via `\Yii::$app->authManager`.
240 241 242

### Building Authorization Data

Alexander Makarov committed
243
Building authorization data is all about the following tasks:
244

Alexander Makarov committed
245 246 247 248 249
- defining roles and permissions;
- establishing relations among roles and permissions;
- defining rules;
- associating rules with roles and permissions;
- assigning roles to users.
250

Alexander Makarov committed
251
Depending on authorization flexibility requirements the tasks above could be done in different ways.
252

Valentin Ts committed
253
If your permissions hierarchy doesn't change at all and you have a fixed number of users you can create a
254
[console command](tutorial-console.md#create-command) that will initialize authorization data once via APIs offered by `authManager`:
255 256

```php
Alexander Makarov committed
257 258 259
<?php
namespace app\commands;

sitawit committed
260
use Yii;
Alexander Makarov committed
261 262 263 264 265 266 267 268 269 270
use yii\console\Controller;

class RbacController extends Controller
{
    public function actionInit()
    {
        $auth = Yii::$app->authManager;

        // add "createPost" permission
        $createPost = $auth->createPermission('createPost');
271
        $createPost->description = 'Create a post';
Alexander Makarov committed
272 273 274 275
        $auth->add($createPost);

        // add "updatePost" permission
        $updatePost = $auth->createPermission('updatePost');
276
        $updatePost->description = 'Update post';
Alexander Makarov committed
277 278 279 280 281 282 283 284 285 286 287 288 289 290
        $auth->add($updatePost);

        // add "author" role and give this role the "createPost" permission
        $author = $auth->createRole('author');
        $auth->add($author);
        $auth->addChild($author, $createPost);

        // add "admin" role and give this role the "updatePost" permission
        // as well as the permissions of the "author" role
        $admin = $auth->createRole('admin');
        $auth->add($admin);
        $auth->addChild($admin, $updatePost);
        $auth->addChild($admin, $author);

291
        // Assign roles to users. 1 and 2 are IDs returned by IdentityInterface::getId()
Alexander Makarov committed
292
        // usually implemented in your User model.
293 294
        $auth->assign($author, 2);
        $auth->assign($admin, 1);
Alexander Makarov committed
295 296
    }
}
297 298
```

299
After executing the command with `yii rbac/init` we'll get the following hierarchy:
300 301 302 303 304

![Simple RBAC hierarchy](images/rbac-hierarchy-1.png "Simple RBAC hierarchy")

Author can create post, admin can update post and do everything author can.

Alexander Makarov committed
305
If your application allows user signup you need to assign roles to these new users once. For example, in order for all
306
signed up users to become authors in your advanced application template you need to modify `frontend\models\SignupForm::signup()`
Alexander Makarov committed
307 308 309
as follows:

```php
310
public function signup()
Alexander Makarov committed
311
{
312 313 314 315 316 317 318
    if ($this->validate()) {
        $user = new User();
        $user->username = $this->username;
        $user->email = $this->email;
        $user->setPassword($this->password);
        $user->generateAuthKey();
        $user->save(false);
Alexander Makarov committed
319 320 321

        // the following three lines were added:
        $auth = Yii::$app->authManager;
322 323
        $authorRole = $auth->getRole('author');
        $auth->assign($authorRole, $user->getId());
Alexander Makarov committed
324 325 326

        return $user;
    }
327 328

    return null;
Alexander Makarov committed
329 330 331 332 333 334 335
}
```

For applications that require complex access control with dynamically updated authorization data, special user interfaces
(i.e. admin panel) may need to be developed using APIs offered by `authManager`.


336 337 338
### Using Rules

As aforementioned, rules add additional constraint to roles and permissions. A rule is a class extending
339
from [[yii\rbac\Rule]]. It must implement the [[yii\rbac\Rule::execute()|execute()]] method. In the hierarchy we've
340
created previously author cannot edit his own post. Let's fix it. First we need a rule to verify that the user is the post author:
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366

```php
namespace app\rbac;

use yii\rbac\Rule;

/**
 * Checks if authorID matches user passed via params
 */
class AuthorRule extends Rule
{
    public $name = 'isAuthor';

    /**
     * @param string|integer $user the user ID.
     * @param Item $item the role or permission that this rule is associated with
     * @param array $params parameters passed to ManagerInterface::checkAccess().
     * @return boolean a value indicating whether the rule permits the role or permission it is associated with.
     */
    public function execute($user, $item, $params)
    {
        return isset($params['post']) ? $params['post']->createdBy == $user : false;
    }
}
```

367 368
The rule above checks if the `post` is created by `$user`. We'll create a special permission `updateOwnPost` in the
command we've used previously:
369 370

```php
371 372
$auth = Yii::$app->authManager;

373 374 375 376 377
// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);

// add the "updateOwnPost" permission and associate the rule with it.
378
$updateOwnPost = $auth->createPermission('updateOwnPost');
379
$updateOwnPost->description = 'Update own post';
380 381 382
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);

383
// "updateOwnPost" will be used from "updatePost"
384
$auth->addChild($updateOwnPost, $updatePost);
385

386 387 388 389
// allow "author" to update their own posts
$auth->addChild($author, $updateOwnPost);
```

390 391 392
Now we've got the following hierarchy:

![RBAC hierarchy with a rule](images/rbac-hierarchy-2.png "RBAC hierarchy with a rule")
393 394 395 396 397 398 399 400 401 402 403 404 405

### Access Check

With the authorization data ready, access check is as simple as a call to the [[yii\rbac\ManagerInterface::checkAccess()]]
method. Because most access check is about the current user, for convenience Yii provides a shortcut method
[[yii\web\User::can()]], which can be used like the following:

```php
if (\Yii::$app->user->can('createPost')) {
    // create post
}
```

406 407 408 409 410
If the current user is Jane with ID=1 we're starting at `createPost` and trying to get to `Jane`:

![Access check](images/rbac-access-check-1.png "Access check")

In order to check if user can update post we need to pass an extra parameter that is required by the `AuthorRule` described before:
411 412

```php
413
if (\Yii::$app->user->can('updatePost', ['post' => $post])) {
414 415 416 417
    // update post
}
```

418 419 420 421 422 423 424 425 426 427 428 429
Here's what happens if current user is John:


![Access check](images/rbac-access-check-2.png "Access check")

We're starting with the `updatePost` and going through `updateOwnPost`. In order to pass it `AuthorRule` should return
`true` from its `execute` method. The method receives its `$params` from `can` method call so the value is
`['post' => $post]`. If everything is OK we're getting to `author` that is assigned to John.

In case of Jane it is a bit simpler since she's an admin:

![Access check](images/rbac-access-check-3.png "Access check")
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444

### Using Default Roles

A default role is a role that is *implicitly* assigned to *all* users. The call to [[yii\rbac\ManagerInterface::assign()]]
is not needed, and the authorization data does not contain its assignment information.

A default role is usually associated with a rule which determines if the role applies to the user being checked.

Default roles are often used in applications which already have some sort of role assignment. For example, an application
may have a "group" column in its user table to represent which privilege group each user belongs to.
If each privilege group can be mapped to a RBAC role, you can use the default role feature to automatically
assign each user to a RBAC role. Let's use an example to show how this can be done.

Assume in the user table, you have a `group` column which uses 1 to represent the administrator group and 2 the author group.
You plan to have two RBAC roles `admin` and `author` to represent the permissions for these two groups, respectively.
445
You can set up the RBAC data as follows,
446 447 448 449 450


```php
namespace app\rbac;

451
use Yii;
452 453 454
use yii\rbac\Rule;

/**
455
 * Checks if user group matches
456 457 458 459 460 461 462
 */
class UserGroupRule extends Rule
{
    public $name = 'userGroup';

    public function execute($user, $item, $params)
    {
463 464 465 466 467 468 469
        if (!Yii::$app->user->isGuest) {
            $group = Yii::$app->user->identity->group;
            if ($item->name === 'admin') {
                return $group == 1;
            } elseif ($item->name === 'author') {
                return $group == 1 || $group == 2;
            }
470
        }
471
        return false;
472 473 474
    }
}

475 476
$auth = Yii::$app->authManager;

477 478 479 480 481 482 483
$rule = new \app\rbac\UserGroupRule;
$auth->add($rule);

$author = $auth->createRole('author');
$author->ruleName = $rule->name;
$auth->add($author);
// ... add permissions as children of $author ...
484 485 486 487 488 489

$admin = $auth->createRole('admin');
$admin->ruleName = $rule->name;
$auth->add($admin);
$auth->addChild($admin, $author);
// ... add permissions as children of $admin ...
490 491
```

492 493 494 495 496
Note that in the above, because "author" is added as a child of "admin", when you implement the `execute()` method
of the rule class, you need to respect this hierarchy as well. That is why when the role name is "author",
the `execute()` method will return true if the user group is either 1 or 2 (meaning the user is in either "admin"
group or "author" group).

MarsuBoss committed
497
Next, configure `authManager` by listing the two roles in [[yii\rbac\BaseManager::$defaultRoles]]:
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515

```php
return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\PhpManager',
            'defaultRoles' => ['admin', 'author'],
        ],
        // ...
    ],
];
```

Now if you perform an access check, both of the `admin` and `author` roles will be checked by evaluating
the rules associated with them. If the rule returns true, it means the role applies to the current user.
Based on the above rule implementation, this means if the `group` value of a user is 1, the `admin` role
would apply to the user; and if the `group` value is 2, the `author` role would apply.