Yii2でもかんたんRBAC(ロールベースアクセスコントロール)

この記事は Yii 1.1 用に書いた [Yiiのアクセスコントロールの簡単な作り方](http://qiita.com/tanakahisateru/items/6759ed45195026400694) の Yii2 版です。

YiiにはRBAC(ロールベースアクセスコントロール)と呼ばれる機能が備わっています。これは、ユーザーのロールを階層的に定義して、それぞれのロールに権限を持たせるという、非常に本格的なものです。

http://www.yiiframework.com/doc-2.0/guide-security-authorization.html#role-based-access-control-rbac

使い方としては、1 と比べてずいぶん気軽なメソッド名になりました。

```php
if (!Yii::$app->user->can('manage')) {
    throw new ForbiddenHttpException('アクセスできません');
}
```

また、Yii 2 になって、AccessControl アクションフィルタもビヘイビア内のオブジェクト構成情報で完結する綺麗なかたちになり、なにかと一貫性が高まってカスタマイズしやすくなりました。

```php
use yii\filters\AccessControl;

class SomeController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'rules' => [
                    [
                        'allow' => true,
                        'roles' => ['manage'],
                    ],
                ],
            ],
        ];
    }
```

が、まだまだ定義が...

使いやすいのはいいけど、機能の開発中にデータベースにロール階層を定義したり、ロールを構成するPHPコードをメンテしたりするのは面倒です。簡単なアプリケーションやプロトタイピング段階では、逆にデメリットになってしまいます。

そこでまたまた、かんたんRBACです。Yii 1 のときは CWebUser をすり替えました。Yii 2 ではもっと手軽にやれます。

RBAC のメカニズムのキーは、`Yii::$app->user->can(...)` です。このメソッドは `Yii::$app->authManager` を使う前提で実装されています。

というわけで、Applicationのコンフィグに `authManager` が必要です。このデフォルトのコンポーネントは勝手に生成されないので、なにか明示的に作る必要があります。

`config/web.php` をこう変更しましょう。(Application の構成情報のことで、もしかしたら別の名前のファイルかもしれません)

```php
return [
    // ...
    'components' => [
        // ...
        'authManager' => [
            'class' => '\app\AuthManager',
        ],
        // ...
    ],
];
```

本当なら、 `\yii\rbac\ManagerInterface` のすべてのメソッドを実装するクラスとして、`yii\rbac\DbManager` か `yii\rbac\PhpManager` あるいはその互換実装が必要なのですが、このインターフェース、メソッド数がけっこう多い。

実はこの ManagerInterface 、`\yii\web\User` の `can()` が `checkAccsess` メソッドを呼んでいるだけで、他に外から使わているメソッドはひとつもないのです。

```php
public function checkAccess($userId, $permissionName, $params = []);
```

他のメソッドの大多数は、ユーザーがロール階層構築に利用するために定義されていて、こっちから呼び出さなければ使われることはありません。いくらか呼び出されているメソッドも、内部的にロール階層構築に使う以外の目的では使われていません。

※ 個人的には、`ManagerInterface` の中から `checkAccsess()` だけ実装したインターフェースを取り出して、`User` は `ManagerInterface` ではなくその小さなインターフェースに依存するという設計が、インターフェース分離原則的に適切だったんじゃないかと思います。

と、いうことは... `authManager` はこんなダックタイプでも動くわけで...

```php
<?php
namespace app;

use yii\base\Component;

class AuthManager extends Component
{
    /**
     * @param string|integer $userId
     * @param string $permissionName
     * @param array $params
     * @return boolean
     * @throws \yii\base\InvalidParamException
     */
    public function checkAccess($userId, $permissionName, $params = [])
    {
        // TODO: なにか実装する
        // TODO: 想定外の $permissionName なら InvalidParamException を投げる
    }
}
```

じゃあ、どう実装するか

```php
public function checkAccess($userId, $permissionName, $params = [])
{
    if (empty($userId)) {
        return false;
    }
    $dbUser = \app\models\UserActiveRecord::findOne($userId);
    switch ($permissionName) {
        case 'admin':
            return $dbUser->is_admin;
        case 'manage':
            return $dbUser->is_admin || $dbUser->is_manager;
        default:
            throw new \yii\base\InvalidParamException("Unknown role '{$permissionName}'.");
    }
}
```

ロール2つで済むならこんな感じで完了です。

データベースにアクセスしているので何度も問い合わせられたら困ると思うかもしれませんが、可否を調べた結果は `\yii\web\User` がしばらく保持します。必要以上に呼ばれることはありません。

これを使い、ユーザーが `manage` できるかどうか、できるならパス、もしかしたらログインしたらできるかもしれない場合はログインへ、ログインしてるのにダメなら HTTP の 403 エラー確定、という制限はこう書けます。

```php
$user = Yii::$app->user;
if (!$user->can('manage')) {
    if ($user->isGuest) {
        $user->loginRequired();
    } else {
        throw new ForbiddenHttpException('oops');
    }
}
```

で、こんなのがコントローラーの `action1` にも `action2` にも書いてある場合、 `AccessControl` フィルタビヘイビアでまとめるとこうなります。

```php
class SomeController extends Controller
{
    public function behaviors()
    {
        return [
            'access' => [
                'class' => AccessControl::className(),
                'rules' => [
                    [
                        'actions' => ['action1', 'action2'],
                        'allow' => true,
                        'roles' => ['manage'],
                    ],
                ],
            ],
        ];
    }
```

おてがるですね。

ダックタイプな実装にすり替えても動くのがミソです。インターフェースを合わせないと将来の変更が心配な人は、`yii\rbac\PhpManager` あたりのコードを継承しておいてもいいんじゃないでしょうか。