Yiiのアクセスコントロールの簡単な作り方


Yiiには、DBなどの外部データに定義を置けるロールベースのアクセスコントロールの仕組みが搭載されています。

[認証と権限付与 6. ロールベースアクセスコントロール](http://www.yiiframework.com/doc/guide/1.1/ja/topics.auth#sec-7)

が、これがちょっと回りくどいんですよね。

そこで、もっと簡単なアクセスコントロールを作ってみましょう。

まず、`protected/config/main.php` の中で、`Yii::app()->user` のインスタンスをデフォルトの `CWebUser` から独自のものにすり替えます。

```php
<?php

	// application components
	'components'=>array(
		'user'=>array(
			'class'=>'WebUser', // これ
			// enable cookie-based authentication
			'allowAutoLogin'=>true,
		),
		// 他のコンポーネントいろいろ
	),
```

デフォルトの `CWebUser` を継承して `protected/components/WebUser.php` に自分の `WebUser` を作ります。

## CWebUser::isGuest みたいなものをもっといろいろ

```php
<?php
/**
 * Class WebUser
 * 
 * @property bool isAdmin
 */
class WebUser extends CWebUser
{
	/**
	 * @return bool
	 */
	public function getIsAdmin()
	{
		if ($this->isGuest) {
			return false;
		}
		$dbUser = MyUser::model()->findByAttributes(array(
			'name' => $this->name,
		));
		return $dbUser && $dbUser->isAdmin;
	}
}
```

これで、`Yii::app()->user->isGuest` 以外に、`Yii::app()->user->isAdmin` なんて問い合わせ方もOKになりました。

この方法なら、自分のモデルに合わせて任意にカスタマイズできます。ちょうど、初期プロジェクトにデフォルトである `protected/components/UserIdentity.php` のパスワード認証みたいな感じです。あれも、DBのパスワードと照合するように、自分独自の仕様で書き換えますよね。

ということで、これをコントローラの `accessRules` メソッドの実装で使うとどうなるかというと…

```php
<?php
class MyController extends Controller {

	public function accessRules()
	{
		return array(
			array('allow', // メンバーは表示だけ
				'actions'=>array('index', 'view'),
				'users'=>array('@'),
			),
			array('allow', // はいここでAdminだけ更新可能っと
				'actions'=>array('create','update','delete'),
				'expression'=>'$user->isAdmin',
			),
			array('deny',  // ログインしてね
				'users'=>array('*'),
			),
		);
	}
```

ユーザ名がズバリ `admin` じゃないとダメだったのが、任意のロジックでログインユーザが管理者かどうかを聞けるようになりました。一般ユーザが管理者権限を付与されるなんてことも…

## IAuthManager::checkAccess() への委譲をやめる

さらにそこから、`CWebUser::checkAccess()` をオーバーライドして `IAuthManager` への委譲を奪うと、アプリケーションから見たら高級なロールベースのアクセスコントロールに見えながら、その実は自分のモデル独自の実装で動いているという形にできます。

```php
<?php
/**
 * Class WebUser
 *
 * @property User $dbUser
 * @property bool isAdmin
 * @property bool canAccessMaterialPicture
 * @property bool canAccessFinePicture
 */
class WebUser extends CWebUser
{
	/* @var User */
	private $_dbUser;

	/**
	 * @param string $operation
	 * @param array $params
	 * @param bool $allowCaching
	 * @return bool
	 */
	public function checkAccess($operation, $params = array(), $allowCaching = true)
	{
		$dbUser = $this->getDbUser($allowCaching);
		// ユーザのモデルインスタンスが operation ができるロールを持っているか
		return $dbUser->hasRoleToDo($operation);
	}

	/**
	 * @param bool $allowCaching
	 * @throws CException
	 * @return User
	 */
	public function getDbUser($allowCaching = true)
	{
		if ($allowCaching && isset($this->_dbUser)) {
			return $this->_dbUser;
		} else {
			$user = MyUser::model()->findByAttributes(array(
				'name' => $this->name
			));
			if (empty($user)) {
				throw new CException("ログインユーザがデータベースに存在しません。");
			}
			if ($allowCaching) {
				$this->_dbUser = $user;
				return $user;
			}
			else {
				unset($this->_dbUser);
			}
			return $user;
		}
	}

	/**
	 * @return bool
	 */
	public function getIsAdmin()
	{
		// return $this->dbUser->isAdmin;
		return $ths->checkAccess('admin');
	}
}
```

これで、フルのRBACをするほどのことはない、もっと簡単なロールモデルでもOKになりました。階層的なロールモデルを持っていて、`CDbAuthManager` でDB上にそのロールの階層構造を構築する、というような方法では設計過剰になるケースも多々あります。いっぽうこの方法では、小さな仕様を `MyUser:: hasRoleToDo($operation)` あたりにスクラッチで書いて閉じ込めることができます。

使い方は `IAuthManager` がある場合と同じです。

```php
if (Yii::app()->user->checkAccess('admin')) {
	// 大事なこと
}
```

ということは…

```php
<?php
class MyController extends Controller {

	public function accessRules()
	{
		return array(
			array('allow', // メンバーは表示だけ
				'actions'=>array('index', 'view'),
				'users'=>array('@'),
			),
			array('allow', // AdminとWriterだけ更新可能
				'actions'=>array('create','update','delete'),
				'roles'=>array('admin', 'writer'),
			),
			array('deny',  // ログインしてね
				'users'=>array('*'),
			),
		);
	}
```

やった。 `expression` が `roles` になりました。

`accessRules()` から eval コードを追い出せました。勝手なコンポーネントの仕様に依存することもありません。後でフルのRBACに透過的に入れ替えることもできます。めでたしめでたし。

Yiiは、オブジェクト指向的に一貫したルールで、どんなコンポーネントでも勝手に置換できるのがいいですね。