CakePHPにACL入れました

苦心惨憺の末、ようやくACL入れられました。

参考にした主な記事。
http://cake.zista.jp/max/blog/view/0000000098
http://cake.zista.jp/max/blog/view/0000000099
AuthComponent + AclComponent + AclBehavior CakePHP1.2RC2 - 忍び歩く男 - SLYWALKER
CakePHPのACLにはまる...でも、出てくる?! | ブラジルの大地
http://book.cakephp.org/ja/view/648/Setting-up-permissions

特にZiSTA様の記事は、本当に、助かりました(T-T)必読です。


以下、長文です・・・


混迷した主な原因が、ACLの設定方法に、複数のやり方があることでした。


ACLの動作条件*1


このなかで、まず「DBに権限設定の登録」

  • PHPから行なう
  • シェルで行なう

ざっくりと、この2種類があります。

いずれか1つを行なって、DBの上記3テーブルにデータが正しく登録されればOK*2


また、「Authのauthorize設定」
こちらも、

  • アクション・モード
  • CRUD・モード

の2種類があって、どちらのモードにするかで、ACOパーミッションの設定方法が変わります。



今回のDBへの登録方法は、以下の通りです。


その他主な設定方針

  • authorize設定
  • ARO
    • 権限GROUPを作り、個々のUserはいずれかのGROUPに所属させる
  • ACO
    • controllers(全controller)ー各controllerーアクションの階層構造にする
  • パーミッション
    • 権限はGROUPごとに設定する。
      • USER個別には設定しない
    • コントローラ単位での権限設定を行なえるようにする

DB定義の変更

slywakerさんの記事を参考に、groupsテーブルを追加。
group_idの紐付けカラムを追加してusersテーブル作り直し。
注意点は、CakePHPのお約束:紐付けカラム名がgroup_idになることくらい。


そしてACL関連のテーブル追加。
app/config/db_acl.sqlからmysqlコマンドで追加。

[cake@cake app]$ mysql -u root -p DBNAME < app/config/db_acl.sql

コレだけで済みます。確かに(笑)

AROの登録

AclBehaviorを使って、groupsとusersにデータが追加・更新された時、同時にAROが自動設定されるようにします。


groupsとusersの各モデルは、slywakerさんの記事に倣って作成・改修。


そしてgroupsのcontrollerとviewを、bakeを使って作成。Authはまだいれません。
ブラウザから、http://CAKE_ROOT/groups/addにアクセスして、parent_id=0で権限グループを作成。

データ追加に伴って、arosテーブルにデータが自動挿入されたことを確認。

mysql> SELECT * FROM groups;
+----+----------+-----------+
| id | name     | parent_id |
+----+----------+-----------+
|  1 | admin    |         0 |
|  2 | subadmin |         0 |
|  3 | watcher  |         0 |
|  4 | member   |         0 |
+----+----------+-----------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM aros;
+----+-----------+-------+-------------+----------+------+------+
| id | parent_id | model | foreign_key | alias    | lft  | rght |
+----+-----------+-------+-------------+----------+------+------+
|  1 |      NULL | Group |           1 | Group::1 |    1 |    2 |
|  2 |      NULL | Group |           2 | Group::2 |    3 |    4 |
|  3 |      NULL | Group |           3 | Group::3 |    5 |    6 |
|  4 |      NULL | Group |           4 | Group::4 |    7 |    8 |
+----+-----------+-------+-------------+----------+------+------+
4 rows in set (0.00 sec)


users_controller.phpには、addでgroup_idのセットを追加。
デフォルトのgroup_id=4(一般member)。*3

Index: controllers/users_controller.php
===================================================================
--- controllers/users_controller.php    (revision 167)
+++ controllers/users_controller.php    (working copy)
@@ -192,7 +192,10 @@
                $this->set('users', $this->paginate());
        }

-       function _add() {
+       function _add($group_id=4) {
+               // ACL設定(デフォルト:一般ユーザ)
+               $this->data['User']['group_id'] = $group_id;
+
                // バリデーション
                $this->User->set($this->data);
                if ($this->User->validates()) {

http://CAKE_ROOT/users/add*4からuserを登録すると、arosが自動的に登録されました*5

mysql> SELECT id, name, group_id FROM users;
+----+---------------+----------+
| id | name          | group_id |
+----+---------------+----------+
|  1 | Administrator |        1 |
|  2 | Cake          |        4 |
+----+---------------+----------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM aros;
+----+-----------+-------+-------------+----------+------+------+
| id | parent_id | model | foreign_key | alias    | lft  | rght |
+----+-----------+-------+-------------+----------+------+------+
|  1 |      NULL | Group |           1 | Group::1 |    1 |    4 |
|  2 |      NULL | Group |           2 | Group::2 |    5 |    6 |
|  3 |      NULL | Group |           3 | Group::3 |    7 |    8 |
|  4 |      NULL | Group |           4 | Group::4 |    9 |   12 |
|  5 |         1 | User  |           1 | User::1  |    2 |    3 |
|  6 |         4 | User  |           2 | User::2  |   10 |   11 |
+----+-----------+-------+-------------+----------+------+------+
6 rows in set (0.00 sec)

↑これら↓を見ると、arosの構成がなんとなく把握できます。

[cake@cake console]$ ./cake acl view aro

Aro tree:
---------------------------------------------------------------
  [1]Group::1

    [5]User::1

  [2]Group::2

  [3]Group::3

  [4]Group::4

    [6]User::2

---------------------------------------------------------------

ACOの登録

次にacosの登録。
追加するコントローラ・アクションは権限別の設定が必要なもののみ。
とりあえず動作確認用のみ。

以下の通り*6

[cake@cake app]$ ./cake acl create aco root controllers

[cake@cake app]$ ./cake acl create aco controllers Users

[cake@cake app]$ ./cake acl create aco Users delete

[cake@cake console]$ ./cake acl create aco controllers Groups

追加結果。

[cake@cake console]$ ./cake acl view aco

Aco tree:
---------------------------------------------------------------
  [1]controllers

    [2]Users

      [3]delete

    [4]Groups

---------------------------------------------------------------

コントローラとアクションの階層図になっています。

このときのDB acosの状態

mysql> SELECT * FROM acos;
+----+-----------+-------+-------------+-------------+------+------+
| id | parent_id | model | foreign_key | alias       | lft  | rght |
+----+-----------+-------+-------------+-------------+------+------+
|  1 |      NULL |       |        NULL | controllers |    1 |    8 |
|  2 |         1 |       |        NULL | Users       |    2 |    5 |
|  3 |         2 |       |        NULL | delete      |    3 |    4 |
|  4 |         1 |       |        NULL | Groups      |    6 |    7 |
+----+-----------+-------+-------------+-------------+------+------+
4 rows in set (0.00 sec)


全コントローラのmasterであるcontrollersの設定を、app/controller/app_controller.phpのbeforeFilterに追加。

Index: controllers/app_controller.php
===================================================================
--- controllers/app_controller.php      (revision 167)
+++ controllers/app_controller.php      (working copy)
class AppController extends Controller
{
	var $isAdmin = false;

	function beforeFilter()
	{
                parent::beforeFilter();

+               // ACLのTopNode
+               $this->AuthPlus->actionPath = 'controllers/';
+
                // 認証関連設定
                if (Configure::read('mobile')) {
                        $this->AuthPlus->loginAction = '/m/users/login';

* AuthPlusは私が使っているAuthコンポーネントの拡張です。通常なら

$this->Auth->actionPath = 'controllers/';

パーミッションの設定

シェルから登録する場合の基本コマンド

cake acl {grant|deny} {model}.{foreign_key} {alias} {create|read|update|delete|all}

actionは指定しなくても良い。その場合controller単位での設定になる。


SuperAdministrator用の全権allow

[cake@cake console]$ ./cake acl grant Group.1 controllers all

一般ユーザ用の設定(試験用&一部)

  • 基本deny
  • Usersの読込みのみ許可
[cake@cake console]$ ./cake acl deny Group.4 controllers all

[cake@cake console]$ ./cake acl grant Group.4 Users read

その結果

mysql> SELECT * FROM aros_acos;
+----+--------+--------+---------+-------+---------+---------+
| id | aro_id | aco_id | _create | _read | _update | _delete |
+----+--------+--------+---------+-------+---------+---------+
|  1 |      1 |      1 | 1       | 1     | 1       | 1       |
|  2 |      4 |      1 | 0       | 0     | 0       | 0       |
|  3 |      4 |      2 | 0       | 1     | 0       | 0       |
+----+--------+--------+---------+-------+---------+---------+
3 rows in set (0.00 sec)

id=1 aro_id=1(Group.1)にaco_id=1(controllers)にall allow。
id=2 aro_id=4(Group.4)にaco_id=1(controllers)にall deny。
id=3 aro_id=4(Group.4)にaco_id=2(Users)にread allow 他 deny。
・・・と正しく設定されています。


ACL設定をDBで確認すると言う手法は、この本由来です。

オープンソース徹底活用CakePHPによるWebアプリケーション開発

オープンソース徹底活用CakePHPによるWebアプリケーション開発

パーミッション(aros_acos)の設定を確認できるのはDBだけのようなので、このヒントは助かりました*7

Authの設定

最後の仕上げ。
Authコンポーネントを改修して、Auth認証が通った後ACLによる権限チェックを行なうように設定します。


具体的には、app_controler.phpで、

class AppController extends Controller
{

+       var $components = array('AuthPlus', 'Acl');
(中略)
        function beforeFilter()
        {
                parent::beforeFilter();

               if ($this->AuthPlus) {
+                       // ACL関連
+                       $this->AuthPlus->actionPath = 'controllers/';
+                       $this->AuthPlus->authorize = 'crud';


コントローラ単位でのパーミッション設定を有効にするために、crud設定が必須です*8


index, view, add, edit, deleteのような基本アクション*9だけならこれだけでOKですが、
独自のアクション名を追加してる場合、追加アクションのcrudマッピング追加が必要になります。


これは、各コントローラで設定して、Authの拡張コンポーネントで読み込みます。

Index: controllers/users_controller.php
===================================================================
--- controllers/users_controller.php    (revision 169)
+++ controllers/users_controller.php    (working copy)

+       /* ACL */
+       // 追加アクション用 crudMap
+       var $actionMapPlus = array(
+               'listview' => 'read',
+               'change_password' => 'update',
+       );


Index: controllers/components/auth_plus.php
===================================================================
--- controllers/components/auth_plus.php        (revision 169)
+++ controllers/components/auth_plus.php        (working copy)
@@ -20,6 +20,18 @@

        function initialize(&$controller)
        {
+               // ACL: controllerごとのactionMap設定マージ
+               $this->actionMap = array_merge($this->actionMap, $controller->actionMapPlus);
+               $admin = Configure::read('Routing.admin');
+               if (!empty($admin)) {
+                       foreach ($controller->actionMapPlus as $k => $v) {
+                               $this->actionMap = array_merge(
+                                       $this->actionMap,
+                                       array($admin . '_'. $k => $v)
+                               );
+                       }
+               }
+
                parent::initialize($controller);

                // ログイン後リダイレクト設定

Index: controllers/app_controller.php
===================================================================
--- controllers/app_controller.php      (revision 169)
+++ controllers/app_controller.php      (working copy)
@@ -19,19 +19,32 @@
 {
        var $isAdmin = false;

+       var $components = array('AuthPlus', 'Acl');
+
+       /* ACL */
+       // 追加アクション用 crudMap
+       var $actionMapPlus = array();

追加アクションの内容に応じて、read, create, update, deleteを適宜割り当てます。

動作確認

以上を設定して、SuperAdministor権限のid=1でログインすると、Users,Groupsのすべてのアクションにアクセスできます。

一方、一般ユーザ権限のid=2でログインすると、readのみのusers/indexやusers/viewは表示できますが、delete権限のusers/delete、update権限のchange_passwordなどは実行できません。


実行できないアクションにアクセスした場合、前の画面に戻りますが、AuthコンポーネントのauthErrorに設定のメッセージがSession.Flushに入っています。
ビューに表示したい場合は、$session->flash('auth'); をechoさせると表示されます。入れるならlayoutかなと思います。

ひとまず区切り

全アクションの権限設定、実際に運用する場合の多少の問題(Usersから削除した際にaroから消えない、adminからパスワード操作した際の挙動)など、
環境に合わせての細かい修正がまだ要りますが、

「基本のACL」実装としては、ここで一区切り。
  最後に全改修分です*10

[cake@cake app]$ svn diff
Index: models/group.php
===================================================================
--- models/group.php    (revision 0)
+++ models/group.php    (revision 0)
@@ -0,0 +1,41 @@
+<?php
+class Group extends AppModel {
+
+       var $name = 'Group';
+       var $actsAs = array(
+               'Acl' => array('requester'),
+       );
+
+       function parentNode() {
+               if (!$this->id) {
+                       return null;
+               }
+               $data = $this->read();
+               if (!$data['Group']['parent_id']){
+                       return null;
+               } else {
+                       return array('model' => 'Group', 'foreign_key' => $data['Group']['parent_id']);
+               }
+       }
+
+       // 更新時に親IDを変更する
+       function save($data = null, $validate = true, $fieldList = array())
+       {
+               if (parent::save($data, $validate, $fieldList)) {
+                       $conditions = array(
+                               'model' => $this->name,
+                               'foreign_key' => $this->id,
+                       );
+
+                       App::import('Component', 'Acl');
+                       $Aro = new Aro;
+                       $Aro->id = $Aro->field('id', $conditions);
+                       $Aro->saveField('parent_id', $data['Group']['parent_id']);
+                       $Aro->saveField('alias', $this->name . '::' . $this->id);
+                       return true;
+               }
+               return false;
+       }
+
+}
+?>
Index: models/user.php
===================================================================
--- models/user.php     (revision 169)
+++ models/user.php     (working copy)
@@ -2,7 +2,9 @@
 class User extends AppModel {

        var $name = 'User';
+       var $belongsTo = array('Group');
        var $actsAs = array(
+               'Acl' => 'requester',
                'Cakeplus.AddValidationRule',
        );

@@ -50,7 +52,42 @@
                )
        );

+       // ACL
+       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('model' => 'Group', 'foreign_key' => $data['User']['group_id']);
+               }
+       }
+       // 更新時に親IDを変更する
+       function save($data = null, $validate = true, $fieldList = array())
+       {
+               if (parent::save($data, $validate, $fieldList)) {
+                       $conditions = array(
+                               'model' => $this->name,
+                               'foreign_key' => $this->id,
+                       );

+                       App::import('Component', 'Acl');
+                       $Aro = new Aro;
+                       $Aro->id = $Aro->field('id', $conditions);
+                       $Aro->saveField('parent_id', $data['User']['group_id']);
+                       $Aro->saveField('alias', $this->name . '::' . $this->id);
+                       return true;
+               }
+               return false;
+       }
+
+       /* validation */
        function betweenUsername($data)
        {
                $idLength = Configure::read('User.UserId.Length');
Index: controllers/components/auth_plus.php
===================================================================
--- controllers/components/auth_plus.php        (revision 169)
+++ controllers/components/auth_plus.php        (working copy)
@@ -20,6 +20,18 @@

        function initialize(&$controller)
        {
+               // ACL: controllerごとのactionMap設定マージ
+               $this->actionMap = array_merge($this->actionMap, $controller->actionMapPlus);
+               $admin = Configure::read('Routing.admin');
+               if (!empty($admin)) {
+                       foreach ($controller->actionMapPlus as $k => $v) {
+                               $this->actionMap = array_merge(
+                                       $this->actionMap,
+                                       array($admin . '_'. $k => $v)
+                               );
+                       }
+               }
+
                parent::initialize($controller);

                // ログイン後リダイレクト設定
Index: controllers/app_controller.php
===================================================================
--- controllers/app_controller.php      (revision 169)
+++ controllers/app_controller.php      (working copy)
@@ -19,19 +19,31 @@
 {
        var $isAdmin = false;

+       var $components = array('AuthPlus', 'Acl');
+
+       /* ACL */
+       // 追加アクション用 crudMap
+       var $actionMapPlus = array();
+
        function beforeFilter()
        {
                parent::beforeFilter();

+               if ($this->AuthPlus) {
+                       // ACL関連
+                       $this->AuthPlus->actionPath = 'controllers/';
+                       $this->AuthPlus->authorize = 'crud';

Index: controllers/users_controller.php
===================================================================
--- controllers/users_controller.php    (revision 169)
+++ controllers/users_controller.php    (working copy)
@@ -3,9 +3,16 @@

        var $name = 'Users';
        var $helpers = array('Html', 'Form');
-       var $components = array('AuthPlus');

+       /* ACL */
+       // 追加アクション用 crudMap
+       var $actionMapPlus = array(
+               'listview' => 'read',
+               'change_password' => 'update',
+       );
+
        function beforeFilter() {
@@ -192,7 +199,10 @@
                $this->set('users', $this->paginate());
        }

-       function _add() {
+       function _add($group_id=4) {
+               // ACL設定(デフォルト:一般ユーザ)
+               $this->data['User']['group_id'] = $group_id;
+
                // バリデーション
                $this->User->set($this->data);
                if ($this->User->validates()) {
Index: views/layouts/mobile_default.ctp
===================================================================
--- views/layouts/mobile_default.ctp    (revision 169)
+++ views/layouts/mobile_default.ctp    (working copy)
@@ -18,6 +18,9 @@
 <?php if ($session->flash()): ?>
 <div><?php $session->flash(); ?></div>
 <?php endif; ?>
+<?php if ($session->check('Message.auth')): ?>
+<div><?php $session->flash('auth'); ?></div>
+<?php endif; ?>
 <?php echo $content_for_layout; ?>
 <div id="footer"></div>
 <?php echo $cakeDebug; ?>
Index: views/layouts/default.ctp
===================================================================
--- views/layouts/default.ctp   (revision 169)
+++ views/layouts/default.ctp   (working copy)
@@ -33,6 +33,9 @@
                </div>
                <div id="content">
                        <?php $session->flash(); ?>
+                       <?php if ($session->check('Message.auth')): ?>
+                               <div><?php $session->flash('auth'); ?></div>
+                       <?php endif; ?>
                        <?php echo $content_for_layout; ?>
                </div>
                <div id="footer">

*1:抜けてたらすみません

*2:でもこれが判らなくて、2つとも行なってデータがグチャグチャになったり・・・

*3:そしてふと思った。メール登録制のセッション管理、これで対応しようか。usersに途中やめのデータ残りますが。

*4:現状、認証不要($this->Auth->allow)設定

*5:User::1=管理人は、試験用に$group_idいじって登録

*6:完了メッセージ略

*7:実装は全然別の方法でやったわけですが ^^;

*8:苦闘の跡。

*9:他にもあり。詳細はcake/libs/controller/components/auth.phpの$actionMap、$this->actionMap関連を参照

*10:関係ない差分は省いてます