time 2017/10/17
ACLコンポーネントを使ってみた結果を結論から言うと、ACL(アクセス制御リスト)ってすごい便利だと思います。
ACLとは、マニュアルにも書いてあるように「優れた保守性と管理性を保ちつつ、アプリケーションのパーミッションをきめ細かく設定する」機能を提供してくれます。
例えば、管理者しかアクセスできないコントローラーやアクションを設定することができます。
「これは是非とも使いたい」と思い、マニュアルを見たのですが、説明がすごく分かりにくかった・・・
というわけで、自分のためにもブログに残しておきます。
(文章力がないのでマニュアルより分かりづらくなりそうですが・・・)
適当なコントローラーにアクションをいくつか作成し、管理者(administrators)、運営者(managers)、利用者(users)の3つのグループとユーザーを作成。グループごとにパーミッションを設定していく流れで説明していきたいと思います。
1・データベースの準備
まずはデータベースの準備を行います。
認証とACLはセットとなるのが一般的なので、まずはuserテーブルから作成することにします。
グループごとにパーミッションを設定していく予定なので、usersテーブルには以下のフィールドを用意し作成します。
(※Bakeで作成しても問題ありません。)
▼usersテープルの作成
CREATE TABLE users ( id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(80) NOT NULL, group_id INT(11) NOT NULL, created DATETIME, modified DATETIME );
次に、usersテーブルの外部キーとなるgroupsテーブルを作成します。
▼groupsテープルの作成
CREATE TABLE groups ( id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, created DATETIME, modified DATETIME );
その他のテーブルを作成します。
今回はテストなので、適当なテーブルを1つ作成することにします。
▼その他のテープルの作成
CREATE TABLE posts ( id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id INT(11) NOT NULL, title VARCHAR(255) NOT NULL, body TEXT, created DATETIME, modified DATETIME );
ACL関連のテーブルを作成します。
Bakeコマンドで『cake schema create DbAcl』を実行するか、以下のSQLでテーブルを作成します。
(このSQL文は、CakePHP1.3.10では『/cake/console/templates/skel/config/schema/db_acl.php』にも書いてあります。)
▼ACL関連のテーブルを作成
CREATE TABLE acos ( id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, parent_id INTEGER(10) DEFAULT NULL, model VARCHAR(255) DEFAULT '', foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, alias VARCHAR(255) DEFAULT '', lft INTEGER(10) DEFAULT NULL, rght INTEGER(10) DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE aros_acos ( id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, aro_id INTEGER(10) UNSIGNED NOT NULL, aco_id INTEGER(10) UNSIGNED NOT NULL, _create CHAR(2) NOT NULL DEFAULT 0, _read CHAR(2) NOT NULL DEFAULT 0, _update CHAR(2) NOT NULL DEFAULT 0, _delete CHAR(2) NOT NULL DEFAULT 0, PRIMARY KEY(id) ); CREATE TABLE aros ( id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, parent_id INTEGER(10) DEFAULT NULL, model VARCHAR(255) DEFAULT '', foreign_key INTEGER(10) UNSIGNED DEFAULT NULL, alias VARCHAR(255) DEFAULT '', lft INTEGER(10) DEFAULT NULL, rght INTEGER(10) DEFAULT NULL, PRIMARY KEY (id) );
これで、データーベースの準備は終了です。
繰り返しになりますが、ACLはあくまで「アプリケーションのパーミッションをきめ細かく設定する」ものなので、認証(ログイン処理など)とは異なります。
認証はAuthコンポーネントを使用して行います。
(Authコンポーネントについては、「CakePHP Authコンポーネントを使ってみた」を参照。)
今回は、すべてのコントローラーにAuthコンポーネントを使用したいので、『AppController』を作成したいと思います。
▼『AppController』の作成と認証処理
class AppController extends Controller { public $components = array('Auth', 'Acl', 'Session'); public function beforeFilter() { // Authコンポーネントの設定 /* * モードの指定 * [actions] でアクション単位のパーミッションチェック(アクセス可能か確認)を行う * [crud] でコントローラー単位のパーミッションチェック(CRUDによる確認)を行う */ $this->Auth->authorize = 'actions'; $this->Auth->loginRedirect = array('controller' => 'posts', 'action' => 'index'); $this->Auth->logoutRedirect = array('controller' => 'users', 'action' => 'login'); // ↓グループとユーザーとパーミッションを登録したら削除する $this->Auth->allow('*'); } }
ACLでもっとも苦しんだのが『$this->Auth->authorize』でした・・・
実は、『$this->Auth->authorize』に指定する値でパーミッションをチェックする方法が大きく変わります。
『actions』を指定すると、アクションに対するアクセス可能か確認し、『crud』を指定するとコントローラーに対してCRUDによるパーミッションの確認となります。
つまり、『actions』ではCRUDによる確認はできず、『crud』ではコントローラーだけにCRUDによる確認をすることができるということになります。
今回のケースでは、アクションごとにパーミッションを設定したいので『actions』を指定します。
ちなみに、マニュアルの5.1 アクセス制御リストは『crud』を使った場合の説明
11.2 ACL を制御するシンプルなアプリケーションは『actions』を使った場合の説明となっています。
マニュアルを読むときは、このことを意識しながら見ないと私みたいに苦労します・・・
zistaさんの『CakePHPのACLを理解する(Authコンポーネントから探る) その2』がとても参考になりました。詳しく知りたい方は、是非一度ご覧ください。
次は、AROを作成していきます。
グループとユーザーをACLに関連付け、自動的にarosテーブルにも登録する処理を作成します。
AclBehaviorを使用した以下のコードをGroupモデルとUserモデルに書くことによって、グループやユーザーを登録すると自動的にarosテーブルにも登録してくれるようになります。
▼Userモデル
class User extends AppModel { public $name = 'User'; public $actsAs = array('Acl' => 'requester'); public $belongsTo = array( 'Group' => array( 'className' => 'Group', 'foreignKey' => 'group_id', 'conditions' => '', 'fields' => '', 'order' => '' ) ); public $hasMany = array( 'Post' => array( 'className' => 'Post', 'foreignKey' => 'user_id', 'dependent' => false, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'exclusive' => '', 'finderQuery' => '', 'counterQuery' => '' ) ); /* 以下を追加 --------------------------------- */ // モデルとACLテーブルを自動的に結びつける処理 public function parentNode() { if (!$this->id && empty($this->data)) return null; $data = $this->data; if (empty($this->data)) $data = $this->read(); if (!$data['User']['group_id']) { return null; } else { return array('Group' => array('id' => $data['User']['group_id'])); } } // ユーザーの所属するグループが変更された時の処理 public function afterSave($created) { if (!$created) { $parent = $this->parentNode(); $parent = $this->node($parent); $node = $this->node(); $aro = $node[0]; $aro['Aro']['parent_id'] = $parent[0]['Aro']['id']; $this->Aro->save($aro); } } }
▼Groupモデル
class Group extends AppModel { public $name = 'Group'; public $actsAs = array('Acl' => array('requester')); public $hasMany = array( 'User' => array( 'className' => 'User', 'foreignKey' => 'group_id', 'dependent' => false, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'exclusive' => '', 'finderQuery' => '', 'counterQuery' => '' ) ); /* 以下を追加 --------------------------------- */ // モデルとACLテーブルを自動的に結びつける処理 public function parentNode() { return null; } }
これで、自動的にarosテーブルにも登録されるようになりましたので、グループとユーザーを登録するアクションを作成し登録していきます。
管理者(administrators)、運営者(managers)、利用者(users)の3つのグループとそれぞれのグループに所属するユーザーを登録したら次に進みます。
ACOの作成ですが、マニュアルに便利なコードがあったので、それをそのまま使用したいと思います。
以下がコードです。
▼ACOの作成を自動化するコード
// ACOの作成 public function build_acl() { if (!Configure::read('debug')) return $this->_stop(); $log = array(); $aco =& $this->Acl->Aco; $root = $aco->node('controllers'); if (!$root) { $aco->create(array('parent_id' => null, 'model' => null, 'alias' => 'controllers')); $root = $aco->save(); $root['Aco']['id'] = $aco->id; $log[] = 'Created Aco node for controllers'; } else { $root = $root[0]; } App::import('Core', 'File'); $Controllers = Configure::listObjects('controller'); $appIndex = array_search('App', $Controllers); if ($appIndex !== false ) unset($Controllers[$appIndex]); $baseMethods = get_class_methods('Controller'); $baseMethods[] = 'buildAcl'; $Plugins = $this->_getPluginControllerNames(); $Controllers = array_merge($Controllers, $Plugins); // look at each controller in app/controllers foreach ($Controllers as $ctrlName) { $methods = $this->_getClassMethods($this->_getPluginControllerPath($ctrlName)); // Do all Plugins First if ($this->_isPlugin($ctrlName)) { $pluginNode = $aco->node('controllers/'.$this->_getPluginName($ctrlName)); if (!$pluginNode) { $aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $this->_getPluginName($ctrlName))); $pluginNode = $aco->save(); $pluginNode['Aco']['id'] = $aco->id; $log[] = 'Acoを作成しました ' . $this->_getPluginName($ctrlName) . ' Plugin'; } } // find / make controller node $controllerNode = $aco->node('controllers/'.$ctrlName); if (!$controllerNode) { if ($this->_isPlugin($ctrlName)) { $pluginNode = $aco->node('controllers/' . $this->_getPluginName($ctrlName)); $aco->create(array('parent_id' => $pluginNode['0']['Aco']['id'], 'model' => null, 'alias' => $this->_getPluginControllerName($ctrlName))); $controllerNode = $aco->save(); $controllerNode['Aco']['id'] = $aco->id; $log[] = 'Created Aco node for ' . $this->_getPluginControllerName($ctrlName) . ' ' . $this->_getPluginName($ctrlName) . ' Plugin Controller'; } else { $aco->create(array('parent_id' => $root['Aco']['id'], 'model' => null, 'alias' => $ctrlName)); $controllerNode = $aco->save(); $controllerNode['Aco']['id'] = $aco->id; $log[] = 'Created Aco node for ' . $ctrlName; } } else { $controllerNode = $controllerNode[0]; } //clean the methods. to remove those in Controller and private actions. foreach ($methods as $k => $method) { if (strpos($method, '_', 0) === 0) { unset($methods[$k]); continue; } if (in_array($method, $baseMethods)) { unset($methods[$k]); continue; } $methodNode = $aco->node('controllers/'.$ctrlName.'/'.$method); if (!$methodNode) { $aco->create(array('parent_id' => $controllerNode['Aco']['id'], 'model' => null, 'alias' => $method)); $methodNode = $aco->save(); $log[] = 'Created Aco node for '. $method; } } } if(count($log)>0) $this->set('logs', $log); } private function _getClassMethods($ctrlName = null) { App::import('Controller', $ctrlName); if (strlen(strstr($ctrlName, '.')) > 0) { // plugin's controller $num = strpos($ctrlName, '.'); $ctrlName = substr($ctrlName, $num+1); } $ctrlclass = $ctrlName . 'Controller'; return get_class_methods($ctrlclass); } private function _isPlugin($ctrlName = null) { $arr = String::tokenize($ctrlName, '/'); if (count($arr) > 1) { return true; } else { return false; } } private function _getPluginControllerPath($ctrlName = null) { $arr = String::tokenize($ctrlName, '/'); if (count($arr) == 2) { return $arr[0] . '.' . $arr[1]; } else { return $arr[0]; } } private function _getPluginName($ctrlName = null) { $arr = String::tokenize($ctrlName, '/'); if (count($arr) == 2) { return $arr[0]; } else { return false; } } private function _getPluginControllerName($ctrlName = null) { $arr = String::tokenize($ctrlName, '/'); if (count($arr) == 2) { return $arr[1]; } else { return false; } } /** * Get the names of the plugin controllers ... * * This function will get an array of the plugin controller names, and * also makes sure the controllers are available for us to get the * method names by doing an App::import for each plugin controller. * * @return array of plugin names. * */ private function _getPluginControllerNames() { App::import('Core', 'File', 'Folder'); $paths = Configure::getInstance(); $folder =& new Folder(); $folder->cd(APP . 'plugins'); // Get the list of plugins $Plugins = $folder->read(); $Plugins = $Plugins[0]; $arr = array(); // Loop through the plugins foreach($Plugins as $pluginName) { // Change directory to the plugin $didCD = $folder->cd(APP . 'plugins'. DS . $pluginName . DS . 'controllers'); // Get a list of the files that have a file name that ends // with controller.php $files = $folder->findRecursive('.*_controller\.php'); // Loop through the controllers we found in the plugins directory foreach($files as $fileName) { // Get the base file name $file = basename($fileName); // Get the controller name $file = Inflector::camelize(substr($file, 0, strlen($file)-strlen('_controller.php'))); if (!preg_match('/^'. Inflector::humanize($pluginName). 'App/', $file)) { if (!App::import('Controller', $pluginName.'.'.$file)) { debug('Error importing '.$file.' for plugin '.$pluginName); } else { // Now prepend the Plugin name ... // This is required to allow us to fetch the method names. $arr[] = Inflector::humanize($pluginName) . "/" . $file; } } } } return $arr; }
これをアクションとして実行するとACOが自動的に作成されます。
実行するアクションは『build_acl』なので、GroupsControllerのアクションにした場合は『http://ドメイン/groups/build_acl』といった感じで実行します。
ACOの作成が終了したら、パーミッションの設定を行っていきます。
5・パーミッションを設定する
ここまででACLを使用する準備が完了したので、実際にパーミッションを設定していきます。
今回のケースでは、『$this->Auth->authorize』に『actions』を指定したので、アクションごとにパーミッションを設定することになります。
設定するパーミッションは、
管理者(administrators)グループには、GroupsController・UsersController・PostsControllerの全てのアクションに対してアクセスを許可。
運営者(managers)グループには、UsersControllerのlogin・logoutアクションとPostsControllerの全てのアクションに対してアクセスを許可。
利用者(users)グループには、UsersControllerのlogin・logoutアクションとPostsControllerのindex・show・addアクションに対してのアクセスを許可
と、します。
また、各コントローラーのアクションは、
GroupsControllerにはindex・show・add・delete・build_aclアクション。
UsersControllerにはlogin・logout・index・show・add・deleteアクション。
PostsControllerにはindex・show・add・deleteアクション。
があるものとします。
パーミッションの設定は、『$this->Acl->allow($aroAlias, $acoAlias)』と『$this->Acl->deny($aroAlias, $acoAlias)』で行っていきます。
▼パーミッションを設定する
// パーミッションの設定 public function build_parmition() { // 管理者のパーミッションを設定 $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/index'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/show'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/add'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/delete'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/groups/build_acl'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/login'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/logout'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/index'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/show'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/add'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/users/delete'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/index'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/show'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/add'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 1), 'controllers/posts/delete'); // 運営者のパーミッションを設定 $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/index'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/show'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/add'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/delete'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/groups/build_acl'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/login'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/logout'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/index'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/show'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/add'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 2), 'controllers/users/delete'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/index'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/show'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/add'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 2), 'controllers/posts/delete'); // 利用者のパーミッションを設定 $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/index'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/show'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/add'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/delete'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/groups/build_acl'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/login'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/logout'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/index'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/show'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/add'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/users/delete'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/index'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/show'); $this->Acl->allow(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/add'); $this->Acl->deny(array('model' => 'Group', 'foreign_key' => 3), 'controllers/posts/delete'); }
グループごとにパーミッションを設定するので、aroAliasにはGroupモデルを使用しています。
上記のアクションを適当なコントローラーに配置し、実行します。
これでパーミッションの設定は完了です。
『build_parmition』アクションは、パーミッション設定後は削除してしまってオッケーです。
AppControllerの『$this->Auth->allow(‘*’);』をコメントアウトか削除し、動作確認を行います。
ちゃんと動作していれば、リンクからの推移なら推移元のページにリダイレクト、直接URLを入力ならトップページにリダイレクトされると思います。
エラーメッセージを出力したい場合は、『$this->Auth->authError』にエラーメッセージを指定します。
▼エラーメッセージの例
$this->Auth->authError = 'アクセス権限がありません。';
あとは、『$session->flash』でエラーメッセージをビューに表示します。
▼エラーメッセージの例
echo $session->flash('auth');
基本的なACLの使い方は以上です。
あとは、パーミッションを簡単に変更するアクションを作成したりして、使いやすいようにしていくだけです。
ACLコンポーネントを使用する上で、私が思う注意点は『$this->Auth->authorize』に指定した値でパーミッションチェックの方法が変わるという事だと思います。
私のように、マニュアルや他の方のブログなど読んで混乱する人は、この事を強く意識した方が理解への近道になるかもしれませんね。
さすがに長文になりました・・・
最後まで読んでくださった方。ありがとうございました。