CakePHP2で簡単にACLを使う方法

チョーシドウダ?カラダァ?どうも、高橋です!
今朝、CakePHP2.3.0-beta版がリリースされましたね!

今回のリリースでは、パフォーマンス、セキュリティ、使いやすさを向上させる新機能が追加されました。
また完全に下位互換があるので、2.2系からのマイグレーションは(おそらく)容易でしょう。

というわけで、早速Cake2.3を使ってACLを実現してみたいと思います。

イメージが掴みにくいと思ったのでデモを作成しました。(がんばりました!
ログイン:http://s1.asial.co.jp/~yuya/blog/20121029/users/login
username: adminuser / password: 0000
ACL管理 :http://s1.asial.co.jp/~yuya/blog/20121029/admin/acl

またCakePHPのセットアップについては以下の記事をご覧ください。
http://blog.asial.co.jp/1040

ACLとは

ACLとは、ユーザやグループを元に権限を制御することです。
例えば、ファイルやフォルダのパーミッションですね。

これをWebに置き換えると、↓のような感じです。
「アシアル(グループ)の高橋(ユーザ)はアシアルブログを編集を許可する(権限)」

とっても簡単ですね。

ACLを利用することで、通常の認証機構では困難なフレキシブルな権限管理をいとも簡単に実現出来ます。

プラグインの設定

っていきなりプラグインに頼るんかい!ってそのリアクション、ありがとうございます。
簡単に扱うというテーマには合致しているので問題ないはず。

ダウンロードはこちら(直リンです)
http://www.alaxos.ch/blaxos/downloads/get/alaxos_acl_2.2.0.zip

このプラグインはCakePHP製CMSで有名なcroogoでも採用されている実績があります。
またファイル名に「2.2.0」とかいう意味深な文字列がありますが、気にしないでおきましょう。

1. プラグインを配置する

解凍すると「Acl」というフォルダになるので「app/Plugin/Acl」のように配置します。

2. adminルーティングを有効にする

app/Config/core.php の113行目付近のコメントアウトを解除します。


//Configure::write('Routing.prefixes', array('admin'));
↓
Configure::write('Routing.prefixes', array('admin'));

 

3. プラグインと設定の読み込み

app/Config/bootstrap.php の145行目付近に以下のコードを追加します。


CakePlugin::load('Acl', array('bootstrap' => true));

これで完了。

必要なテーブルを作成する

まずユーザやグループのテーブルを作成します。
ACLではARO(リクエストオブジェクト)の役割を成します。


CREATE TABLE users (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    password CHAR(40) NOT NULL,
    group_id INT(11) NOT NULL,
    created DATETIME,
    modified DATETIME
);
CREATE TABLE groups (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    created DATETIME,
    modified DATETIME
);

次に制御するページのテーブルです。
こちらはACO(コントロールオブジェクト)となります。


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
);
CREATE TABLE widgets (
    id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    part_no VARCHAR(12),
    quantity INT(11)
);

 

MVCの作成 というかBake

 


cd /Users/yuya/html/cake/app
./Config/cake bake all
1
./Config/cake bake all
2
./Config/cake bake all
3
./Config/cake bake all
4

上手に焼けましたーーッ!
あ、まだコンソールは閉じないでくださいね。

ACLテーブルの初期化

コンソールで一行実行するだけなので簡単です。


./Console/cake schema create DbAcl
y
y

これで acos、aros、aros_acos という
ACLに関連する3つのテーブルが初期化されました。

グループとユーザを追加する(AROの追加)

追加はDBから直接行ったりはせず、Cakeのアプリケーションで行います。
そこで認証機構の準備とログイン処理を作成します。

app/Controller/UsersController.php


<?php
App::uses('AppController', 'Controller');
class UsersController extends AppController {
    public function login() {
        if ($this->request->is('post')) {
            if ($this->Auth->login()) {
                $this->redirect($this->Auth->redirect());
            } else {
                $this->Session->setFlash('Your username or password was incorrect.');
            }
        }
    }
    public function logout() {
        //Leave empty for now.
    }
~~~~

app/View/Users/login.ctp


<?php
echo $this->Form->create('User', array('action' => 'login'));
echo $this->Form->inputs(array(
    'legend' => __('Login'),
    'username',
    'password'
));
echo $this->Form->end('Login');

app/Model/User.php


<?php
App::uses('AppModel', 'Model');
App::uses('AuthComponent', 'Controller/Component');
class User extends AppModel {
    public function beforeSave($options = array()) {
        $this->data['User']['password'] = AuthComponent::password($this->data['User']['password']);
        return true;
    }
~~~

app/Controller/AppController.php


<?php
App::uses('Controller', 'Controller');
class AppController extends Controller {
    public $components = array(
        'Acl',
        'Auth' => array(
            'authorize' => array(
                'Actions' => array('actionPath' => 'controllers')
            )
        ),
        'Session'
    );
    public $helpers = array('Html', 'Form', 'Session');
    public function beforeFilter() {
        //Configure AuthComponent
        $this->Auth->loginAction = array('controller' => 'users', 'action' => 'login');
        $this->Auth->logoutRedirect = array('controller' => 'users', 'action' => 'login');
        $this->Auth->loginRedirect = array('controller' => 'posts', 'action' => 'add');
    }
}

 

リクエスタとしてビヘイビアに設定

app/Model/User.php


<?php
App::uses('AppModel', 'Model');
App::uses('AuthComponent', 'Controller/Component');
class User extends AppModel {
    public $belongsTo = array('Group');
    public $actsAs = array('Acl' => array('type' => 'requester'));
    public function parentNode() {
        if (!$this->id  & & empty($this->data)) {
            return null;
        }
        if (isset($this->data['User']['group_id'])) {
            $groupId = $this->data['User']['group_id'];
        } else {
            $groupId = $this->field('group_id');
        }
        if (!$groupId) {
            return null;
        } else {
            return array('Group' => array('id' => $groupId));
        }
    }
~~~~

app/Model/Group.php


<?php
App::uses('AppModel', 'Model');
class Group extends AppModel {
    public $actsAs = array('Acl' => array('type' => 'requester'));
    public function parentNode() {
        return null;
    }
~~~

これからグループとユーザを追加しますが、「http://~~~~/users」 にアクセスしても
ログインページにリダイレクトされてしまうでしょう。

一時的に認証を解除するためのコードを挿入します。

app/Controller/UsersController.php


<?php
App::uses('AppController', 'Controller');
class UsersController extends AppController {
    // すべてのアクションを許可する
    public function beforeFilter() {
        parent::beforeFilter();
        $this->Auth->allow();
    }
    public function login() {
        if ($this->request->is('post')) {
            if ($this->Auth->login()) {
                $this->redirect($this->Auth->redirect());
            } else {
                $this->Session->setFlash('Your username or password was incorrect.');
            }
        }
    }
    public function logout() {
        //Leave empty for now.
    }
~~~

app/Controller/GroupsController.php


<?php
App::uses('AppController', 'Controller');
class GroupsController extends AppController {
    public function beforeFilter() {
        parent::beforeFilter();
        $this->Auth->allow();
    }
~~~

ここで何回やっても許可したはずなのにログインページに飛ぶ!!ってハマったんですが
前は $this->Auth->allow('*'); ですべてのアクションを許可だったはずなんですが
今は $this->Auth->allow(); になっているようです。気をつけてください。

余談ですが、他の指定法としては


$this->Auth->allow(array('edit', 'add'));
$this->Auth->allow('edit', 'add');

があります。

グループ・ユーザーのデータ登録(AROの追加)

さて、無事にページが表示できたらデータを登録していきます。
絶対にグループから登録してください(重要

今回は形式的に
グループ:administrators、managers、users
ユーザ :adminuser、manageuser、useruser

というパターンを作成しましょう。
グループやユーザの追加は必ずCakeのアプリケーションから追加してください。


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

このようにAROが追加できたら、認証のコードは外しておきましょう。

コントローラとアクション(ACO)の登録

ここからはWebでの操作となります!ACLプラグインさんオナシャス!

http://~~~~/admin/acl」にアクセスしてください。

こんなページが表示されましたか?だったらOK
いま「Missing ACOs」といって不足分のACOを表示してくれています。

ページ下部の「Synchronize ACOs」をクリックしてください。

すると、なにやらエラーが出ましたね。
「The Role model is not configured to act as an ACL requester」

んー・・・・・・あぁ!なるほどっ!!
どうやら、プラグイン内の初期設定ではgroupはroleという名前で動作するようです。

app/Plugin/Acl/Config/bootstrap.php の14行目付近


Configure :: write('acl.aro.role.model', 'Role');
↓
Configure :: write('acl.aro.role.model', 'Group');

これで再度、ページを更新してみる。

無事メニューが表示されたので「Synchronize actions ACOs」をクリック

すると・・・

「The ACO datatable is already synchronized」

はい、ガッツポーズ

あとは「Permissions」の「Roles permissions」で権限を設定してみてください。
アクション単位で権限を制御できるのは非常に強力です。

あと地味にAjaxでページ遷移なしで設定変更できるのもストレスがなくて良い感じですね。

まとめですが、やることは多かったけど、全体を通してみると内容は簡単だったと思います。
それでもって実践でもかなり使えるレベルの認証なのでオススメです。

---

締めですが、私は11月3日のPHPMatsuri2012@福岡に参加予定ですので
もしお会いできる方がいればよろしくお願いします!
http://www.phpmatsuri.net/2012/

また、前日の11月2日にも「MonacaとHTML5ではじめるハイブリットアプリ勉強会」というイベントを開催しますので興味のある方は当日でも参加してくださいね〜!(私はハンズオン担当でーす
http://atnd.org/event/monaca121102 (無料です

それでは、福岡で会いましょう