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』に指定した値でパーミッションチェックの方法が変わるという事だと思います。
私のように、マニュアルや他の方のブログなど読んで混乱する人は、この事を強く意識した方が理解への近道になるかもしれませんね。
さすがに長文になりました・・・
最後まで読んでくださった方。ありがとうございました。