CakePHP 4 で Authorization プラグインを用いてアクセス権限を設定する方法

はじめに

今日は CakePHP 4 で Authorization プラグインを用いた、ユーザーのアクセス権限を設定する方法をご紹介します。

CakePHP
4.1.2
CakePHP Authorization
2.0.0
PHP
7.4.5
MariaDB
10.4.11
目次
  1. 下準備
  2. Authorization プラグインのインストール
  3. 特定アクション(コントローラ)のアクセス許可
  4. エンティティの内容でアクセス可否
  5. 応用: ユーザー権限とエンティティの内容で判定
  6. 基本的にはすべて許可とする
  7. おわりに

1. 下準備

今回はサンプルプログラムとして、ブログを想定して、記事の一覧、編集、削除の機能を用意しました。
あくまで動作確認用なので、編集と削除は Flash メッセージの表示だけにしています。

ログイン機能は Authentication プラグインを使って実装しました。
コードは割愛しますが、過去の投稿「CakePHP 4 で CakePHP Authentication プラグインを使ってユーザー認証を実装」などを参考に実装して頂ければと思います。

CREATE TABLE articles (
  id int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id int(11) NOT NULL,
  title varchar(255) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE users (
  id int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  username varchar(255) NOT NULL,
  password varchar(255) NOT NULL,
  job varchar(255) DEFAULT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO articles (id, user_id, title) VALUES
(1, 3, 'King の投稿'),
(2, 4, 'Queen の投稿');

-- パスワードは全て 「p@ssw0rd」です
INSERT INTO users (id, username, password, job) VALUES
(1, 'Joker', '$2y$10$2NJl7BcETFs9QHjjy91kJ.pGiJe1JdF8BX9jvIrNTUAEOOH.HiRrW', 'admin'),
(2, 'Ace', '$2y$10$2NJl7BcETFs9QHjjy91kJ.pGiJe1JdF8BX9jvIrNTUAEOOH.HiRrW', 'manager'),
(3, 'King', '$2y$10$2NJl7BcETFs9QHjjy91kJ.pGiJe1JdF8BX9jvIrNTUAEOOH.HiRrW', 'author'),
(4, 'Queen', '$2y$10$2NJl7BcETFs9QHjjy91kJ.pGiJe1JdF8BX9jvIrNTUAEOOH.HiRrW', 'author');
/src/Controller/ArticlesController.php
<?php
declare(strict_types=1);

namespace App\Controller;

class ArticlesController extends AppController
{
    public function delete($id)
    {
        $article = $this->Articles->get($id);

        $this->Flash->success("ID: {$id} を削除しました");

        return $this->redirect(['action' => 'index']);
    }

    public function edit($id)
    {
        $article = $this->Articles->get($id);

        $this->Flash->success("ID: {$id} を更新しました");

        return $this->redirect(['action' => 'index']);
    }

    public function index()
    {
        $articles = $this->Articles->find()
            ->contain('Users')
            ->all();
        $this->set(compact('articles'));
    }
}
/src/Model/Entity/Article.php
<?php
declare(strict_types=1);

namespace App\Model\Entity;

use Cake\ORM\Entity;

class Article extends Entity
{
    protected $_accessible = [
        '*' => true
    ];
}
/src/Model/Table/ArticlesTable.php
<?php
declare(strict_types=1);

namespace App\Model\Table;

use Cake\ORM\Table;

class ArticlesTable extends Table
{
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
        ]);
    }
}
/templates/Articles/index.php
<table>
    <?php foreach ($articles as $article): ?>
        <tr>
            <td><?= h($article->id) ?></td>
            <td><?= h($article->user->username) ?></td>
            <td><?= h($article->title) ?></td>
            <td class="actions">
                <?= $this->Html->link('Edit', ['action' => 'edit', $article->id]) ?>
                <?= $this->Html->link('Delete', ['action' => 'delete', $article->id]) ?>
            </td>
        </tr>
    <?php endforeach; ?>
</table>

2. Authorization プラグインのインストール

composer で cakephp/authorization プラグインをインストールし、
使用するためのコードを Application.php と AppController.php に追加します。

> cd \path\to\cakephp4
> composer require cakephp/authorization:^2.0
/src/Application.php
<?php
...
// ↓ 下記を追加
use Authorization\AuthorizationService;
use Authorization\AuthorizationServiceInterface;
use Authorization\AuthorizationServiceProviderInterface;
use Authorization\Middleware\AuthorizationMiddleware;
use Authorization\Policy\OrmResolver;
use Psr\Http\Message\ResponseInterface;
// ↓ もしなければ追加
use Psr\Http\Message\ServerRequestInterface;
...
// ↓ implements に AuthorizationServiceProviderInterface を追加
class Application extends BaseApplication implements
    AuthenticationServiceProviderInterface, AuthorizationServiceProviderInterface
{
    ...
    public function bootstrap(): void
    {
        ...
        $this->addPlugin('Authorization'); // ← 追加
    }

    ...
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue
            ...
            // ↓これを追加(直前の ->add(~); のセミコロン削除を忘れずに)
            ->add(new AuthorizationMiddleware($this));

        return $middlewareQueue;
    }

    ...

    // ↓ これを追加
    public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
    {
        $resolver = new OrmResolver();

        return new AuthorizationService($resolver);
    }
}
/src/Controller/AppController.php
public function initialize(): void
{
    ...
    // ▼これを追加
    $this->loadComponent('Authorization.Authorization');
}

3. 特定アクション(コントローラ)のアクセス許可

前述のように Authorization を導入すると、各画面に認可処理を入れないとエラーになってしまいます。

ログインやログアウトのように、任意のアクションに対して全ユーザーへのアクセスを許可する場合は、skipAuthorization() を使うことができます。

beforeFilter 内で条件を付けずに skipAuthorization() を実行すれば、コントローラ全体で認可不要にできますね

/src/Controller/UsersController.php
public function beforeFilter(\Cake\Event\EventInterface $event)
{
    ...
    // ↓ 下記を追加
    if (in_array($this->request->getParam('action'), ['login', 'logout'])) {
      $this->Authorization->skipAuthorization();
    }
}

記事一覧も全ユーザーがアクセス可能なので、認可不要にします。先の例では beforeFilter に記述しましたが、直接アクションに書いても差支えありません。

/src/Controller/ArticlesController.php
public function index()
{
    // ↓ 追加
    $this->Authorization->skipAuthorization();
    ...
}

4. エンティティの内容でアクセス可否

例えば「ログインユーザーが投稿者である場合だけ許可」などのように、ユーザー情報とエンティティの内容によってアクセス可否を設定する場合は、Policy を使うことができます。

試しに、記事の編集と削除は投稿者のみが行えるように実装してみました。

/Model/Entity/Article.php が無いとエラーになりますので、ご注意ください
/src/Policy/ArticlePolicy.php
<?php
declare(strict_types=1);

namespace App\Policy;

use App\Model\Entity\Article;
use Authorization\IdentityInterface;

class ArticlePolicy
{
    public function canDelete(IdentityInterface $user, Article $article)
    {
        return $user->id === $article->user_id;
    }

    public function canUpdate(IdentityInterface $user, Article $article)
    {
        return $user->id === $article->user_id;
    }
}
/src/Controller/ArticlesController.php
public function delete($id)
{
    $article = $this->Articles->get($id);
    // ↓ 追加
    $this->Authorization->authorize($article, 'delete');
    ...
}

public function edit($id)
{
    $article = $this->Articles->get($id);
    // ↓ 追加
    $this->Authorization->authorize($article, 'update');
    ...
}

先の「1. 下準備」でご紹介したデータベースを使用されている場合は、King や Queen でログインをして、それぞれの投稿でのみ「Edit」と「Delete」で Flash メッセージが表示されることを確認してください。

なお Policy ファイルも Bake で生成することができます。

bin/cake bake policy --type entity Article

5. 応用: ユーザー権限とエンティティの内容で判定

users.job が manager のユーザーは、全ユーザーの投稿を編集できるようにしてみます。

/src/Model/Entity/User.php と /src/Model/Table/UsersTable.php がない方は、それらを bake などで作った後に一度ログアウトして、再度ログインしてください。

/src/Policy/ArticlePolicy.php
public function canUpdate(IdentityInterface $user, Article $article)
{
    // ↓ $user->is_manager の条件を追加
    return ($user->is_manager || $user->id === $article->user_id);
}
/src/Model/Entity/Users.php
// ↓ これを追加
protected function _getIsManager()
{
    return $this->job === 'manager';
}

次に管理者アカウントには、Article エンティティに関する全操作を許可します。
これは before() を使うことで、簡単に実装できます。

/src/Policy/ArticlePolicy.php
<?php
...
// ↓ 追加
use Authorization\Policy\BeforePolicyInterface;

// ↓「implements BeforePolicyInterface」を追加
class ArticlePolicy implements BeforePolicyInterface
{
    // ↓ これを追加
    public function before($user, $resource, $action)
    {
        if ($user->is_admin) {
            return true;
        }
    }
    ...
}
/src/Model/Entity/User.php
// ↓ これを追加
protected function _getIsAdmin()
{
    return $this->job === 'admin';
}

6. 基本的にはすべて許可とする

基本的には全ページを「許可」とするためには Application.php に追記した AuthorizationMiddleware で 'requireAuthorizationCheck' => false を指定します。

src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
      ...
      // ↓ 第2引数を追加
      ->add(new AuthorizationMiddleware($this, [
        'requireAuthorizationCheck' => false
      ]));

7. おわりに

Authorization プラグインについても公式ドキュメントに詳しく説明が書いてあるのですが、CakePHP と違って日本語版が提供されておらず、読むのがちょっと大変かもしれませんね。

僕自身、最初読んだときはイメージがつかめなかったのですが、試しながら何度も読み返すうちに、徐々に分かってきました。

今回紹介しきれなかった機能もありますので、是非公式ドキュメントも目を通していただければと思っています。