Yiiには、DBなどの外部データに定義を置けるロールベースのアクセスコントロールの仕組みが搭載されています。
が、これがちょっと回りくどいんですよね。
そこで、もっと簡単なアクセスコントロールを作ってみましょう。
まず、protected/config/main.php
の中で、Yii::app()->user
のインスタンスをデフォルトの CWebUser
から独自のものにすり替えます。
<?php
// application components
'components'=>array(
'user'=>array(
'class'=>'WebUser', // これ
// enable cookie-based authentication
'allowAutoLogin'=>true,
),
// 他のコンポーネントいろいろ
),
デフォルトの CWebUser
を継承して protected/components/WebUser.php
に自分の WebUser
を作ります。
CWebUser::isGuest みたいなものをもっといろいろ
<?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
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
/**
* 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
がある場合と同じです。
if (Yii::app()->user->checkAccess('admin')) {
// 大事なこと
}
ということは…
<?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は、オブジェクト指向的に一貫したルールで、どんなコンポーネントでも勝手に置換できるのがいいですね。