CakePHP3のACLでアクセス管理

環境
----
- PHP 5.5.24
- CakePHP 3.0.11


やりたいこと
----------
- CakePHP3でログイン管理する(Auth)

- CakePHP3でロール毎に権限を管理する(ACL)
    - Cakeのビュー画面でACLの権限を選択できるようにする([CakePhp3-AclManager](https://github.com/JcPires/CakePhp3-AclManager)、[CakePHP Acl Plugin](https://github.com/cakephp/acl)プラグインを使う)


アプリケーションの準備
----------------------
1. CakePHP3をインストールする。config/app.phpのDBを設定する

2. ユーザーとグループのテーブルを作成する
    - 下記参考ページのようなusersとgroupsのテーブルを用意する
        - テーブル参考: [CakePHP2 アプリケーション準備](http://book.cakephp.org/2.0/ja/tutorials-and-examples/simple-acl-controlled-application/simple-acl-controlled-application.html#id1) ※passwordの長さを60に修正する

3. usersとgroupsをbake allする
  - bakeコマンド参考: [Code Generation with Bake](http://book.cakephp.org/3.0/en/bake/usage.html#code-generation-with-bake)


プラグインをインストール
------------------------

- インストールするプラグイン
  - [CakePhp3-AclManager](https://github.com/JcPires/CakePhp3-AclManager)
  - [CakePHP Acl Plugin](https://github.com/cakephp/acl)    

- composer.jsonに「"cakephp/acl": "dev-master"」「"jcpires/cakephp3-aclmanager": "dev-master"」を追記しcomposer updateする

```diff
    "require": {
        "php": ">=5.4.16",
        "cakephp/cakephp": "~3.0",
        "mobiledetect/mobiledetectlib": "2.*",
        "cakephp/migrations": "~1.0",
        "cakephp/plugin-installer": "*",
+       "cakephp/acl": "dev-master",
+       "jcpires/cakephp3-aclmanager": "dev-master"
    },
```


Configの設定
------------
- config/bootstrap.phpの適当な箇所に「Plugin::load('Acl', ['bootstrap' => true]);」を追記する


Controllerの設定
----------------

### AppController.php

```php
<?php
namespace App\Controller;

use Cake\Controller\Controller;
use Cake\Controller\ComponentRegistry;
use Cake\Event\Event;
use Acl\Controller\Component\AclComponent;

class AppController extends Controller
{
    public function beforeFilter(Event $event)
    {
        // グループ、ユーザー登録後コメントアウトする
        $this->Auth->allow();
    }

    public function initialize()
    {
        parent::initialize();
        $this->loadComponent('Acl.Acl');
        $this->loadComponent('Flash');
        $this->loadComponent('Auth', [
            'authorize' => 'Controller',

            // 権限無しページに飛ぶと無限ループになったり、変なURLにリダイレクトされるのを防ぐ
            'unauthorizedRedirect' => false,

            'authError' => 'アクセス権限がありません',

            //'loginRedirect' => [
            //    'controller' => 'Users',
            //    'action' => 'index'
            //],
        ]);
    }

    public function isAuthorized($user)
    {
        $Collection = new ComponentRegistry();
        $acl = new AclComponent($Collection);
        $controller = $this->request->controller;
        $action     = $this->request->action;
        return $acl->check(['Users' => ['id' => $user['id']]], "$controller/$action");
    }
 
}
```

### GroupsController.php
- 下記を追記

```php
use Cake\Event\Event;
use JcPires\AclManager\Event\PermissionsEditor;
```

```php
    public $helpers = [            
        'AclManager' => [
            'className' => 'JcPires/AclManager.AclManager'
        ]
    ];

    public function add()
    {
        $group = $this->Groups->newEntity();
        if ($this->request->is('post')) {
            $group = $this->Groups->patchEntity($group, $this->request->data);
            if ($this->Groups->save($group)) {
                // デフォルトアクセス全許可にしたい場合は下記のコメントアウトを外す
                //if (isset($this->request->data['parent_id'])) {
                //    $parent = $this->request->data['parent_id'];
                //} else {
                //    $parent = null;
                //}
                //$this->eventManager()->on(new PermissionsEditor());
                //$perms = new Event('Permissions.addAro', $this, [
                //    'Aro' => $group,
                //    'Parent' => $parent,
                //    'Model' => 'Groups'
                //]);
                //$this->eventManager()->dispatch($perms);

                $this->Flash->success(__('The group has been saved.'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error(__('The group could not be saved. Please, try again.'));
            }
        }
        $this->set(compact('group'));
        $this->set('_serialize', ['group']);
    }

    public function edit($id = null)
    {
        $group = $this->Groups->get($id, [
            'contain' => []
        ]);

        $this->loadComponent('JcPires/AclManager.AclManager');
        $EditablePerms = $this->AclManager->getFormActions();

        if ($this->request->is(['patch', 'post', 'put'])) {
            $group = $this->Groups->patchEntity($group, $this->request->data);
            if ($this->Groups->save($group)) {

                $this->eventManager()->on(new PermissionsEditor());
                $perms = new Event('Permissions.editPerms', $this, [
                    'Aro' => $group,
                    'datas' => $this->request->data
                ]);
                $this->eventManager()->dispatch($perms);

                $this->Flash->success(__('The group has been saved.'));
                return $this->redirect(['action' => 'index']);
            } else {
                $this->Flash->error(__('The group could not be saved. Please, try again.'));
            }
        }
        $this->set(compact('group', 'EditablePerms'));
        $this->set('_serialize', ['group', 'EditablePerms']);
    }

```

### UsersController.php
- 下記を追記

```php
use Cake\Event\Event;
```

```php
    public function beforeFilter(Event $event)
    {
        parent::beforeFilter($event);
        $this->Auth->allow(['login']);
    }

	public function login()
	{
	    if ($this->request->is('post')) {
	        $user = $this->Auth->identify();
	        
	        if ($user) {
	            $this->Auth->setUser($user);
	            return $this->redirect($this->Auth->redirectUrl());
	        }
	        $this->Flash->error(__('Invalid username or password, try again'));
	    }
	}

	public function logout()
	{
	    return $this->redirect($this->Auth->logout());
	}
```

Modelの設定
-----------

### Entity/Group.php
- 下記を追記

```php
    public function parentNode()
    {
        return null;
    }
```

### Entity/User.php
- 下記を追記

```php
use Cake\ORM\TableRegistry;
use Cake\Auth\DefaultPasswordHasher;
```

```php
    protected function _setPassword($password)
    {
        return (new DefaultPasswordHasher)->hash($password);
    }

    public function parentNode()
    {
        if (!$this->id) {
            return null;
        }
        if (isset($this->group_id)) {
            $group_id = $this->group_id;
        } else {
            $users_table = TableRegistry::get('Users');
            $user = $users_table->find('all', ['fields' => ['group_id']])->where(['id' => $this->id])->first();
            $group_id = $user->group_id;
        }
        if (!$group_id) {
            return null;
        }
        
        return ['Groups' => ['id' => $group_id]];
    }
```

### Table/UsersTable.php
- public function initialize内に追記

```php
        $this->addBehavior('Acl.Acl', ['type' => 'requester']);
```

### Table/GroupsTable.php
- public function initialize内に追記

```php
        $this->addBehavior('Acl.Acl', ['type' => 'requester']);
```

Templateの設定
--------------

### Groups/edit.ctp
- Form内に下記を追記

```php
        <?php foreach ($EditablePerms as $Acos) :?>
            <?php foreach ($Acos as $controllerPath => $actions) :?>
                <?php if (!empty($actions)) :?>
                    <h4><?= $controllerPath ;?></h4>
                    <?php foreach ($actions as $action) :?>
                        <?php ($this->AclManager->checkGroup($group, 'App/'.$controllerPath.'/'.$action)) ? $val = 1 : $val = 0 ?>
                        <?= $this->Form->label('App/'.$controllerPath.'/'.$action, $action);?>
                        <?= $this->Form->select('App/'.$controllerPath.'/'.$action, [0 => 'No', 1 => 'Yes'], ['value' => $val]) ;?>
                    <?php endforeach ;?>
                <?php endif;?>
            <?php endforeach ;?>
        <?php endforeach ;?>
```

### Users/login.ctp
- 新規作成

```html+php
<div class="users form">
<?= $this->Flash->render('auth') ?>
<?= $this->Form->create() ?>
    <fieldset>
        <legend><?= __('Please enter your username and password') ?></legend>
        <?= $this->Form->input('username') ?>
        <?= $this->Form->input('password') ?>
    </fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>
```



ACLプラグインの修正
-------------------
- /vendor/cakephp/acl/src/AclExtras.phpのrootNodeの値をAppに変更する

```diff
+    public $rootNode = 'App';
-    public $rootNode = 'controllers';
```


ACLテーブルの作成
-----------------

```console
   $ bin/cake Migrations.migrations migrate -p Acl
```

ACOデータの作成
---------------

```console
   $ bin/cake acl_extras aco_sync
```


グループとユーザーを作成
------------------------
1. 「http://アプリ名/groups/add」にアクセスしてグループを追加する
2. 「http://アプリ名/groups/edit/1」にアクセスして権限を設定する
3. 「http://アプリ名/users/add」にアクセスしてユーザーを追加する


動作確認
--------
1. 「http://アプリ名/users/logout」にアクセスしてログアウトする
2. AppController.phpの「$this->Auth->allow();」をコメントアウトする
3. 「http://アプリ名/」にアクセスしてログイン画面にリダイレクトされるか確認する
4. 登録したユーザでログインし権限通りのACLができているか確認する


補足
----
- 新しくControllerやActionを追加した場合は下記のコマンドで更新する

```console
   $ bin/cake acl_extras aco_update
```