🫠

LaravelのPolicyに関して、初歩的な事で躓いたのでまとめてみた。

2024/01/30に公開

始めに

今回業務でLaravelのPolicyを利用して認可処理の実装を行いました。
ただドキュメントを読んだ際に、初歩的な部分がわかっておらず、
実装の際に詰まることが多かったです。
そこで今回は本当に初歩的な内容なのですが、Policyに関してまとめたいと思います。

設計

今回使用している技術は以下になります。(一部)
laravel 9.44.0
PHP 8.1.8
Composer 2.4.4
MySQL 8.0.28

今回は例として、図書館のスタッフと図書館の情報が保存されているテーブルがあり、
図書館のスタッフを登録・編集・削除を行う際と、図書館の情報編集は、
管理者権限がないと行えない様に、Policyで設定したいと思います。

テーブル構成

権限の設定

権限 出来ること
管理者権限 図書館のスタッフの登録・編集・削除、図書館の情報更新
一般権限 自分自身の情報編集

Policyとは?

Policyとは、特定のモデルまたはリソースに関する認可ロジックを集めたクラスです。
公式ドキュメントでは、
Postモデルの更新を行う際に、Postを更新しようとしているUserのidと、
更新しようとしているPostUserのidが同じかを、
PostPolicyを作成して、判定する例を紹介しています。
この例だとシンプルで分かりやすいですね。

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    /**
     * 指定した投稿をユーザーが更新可能かを判定
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Post  $post
     * @return bool
     */
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

https://readouble.com/laravel/8.x/ja/authorization.html

今回の設計だと、管理者権限を持っている場合に関しての情報を記載しないといけないです。
まずは図書館の情報を更新する場合を考えます。

LibraryPolicyを作成して、管理者のみ更新できる様にする

1.Policyを作成する

make:policyコマンドを使用して、空のポリシークラスを生成します。
リソースの表示、作成、更新、削除に関連するポリシーメソッドのサンプルを含んだクラスを生成する場合は、
コマンドの実行時に--modelオプションを指定します。
(今回の場合だと更新だけなので、オプションを指定してもしなくても大丈夫です)

php artisan make:policy LibraryPolicy --model=Library

2.Policyを登録する

作成したPolicyをAuthServiceProviderに登録します。

class AuthServiceProvider extends ServiceProvider
{
  use App\Models\Library;
  use App\Policies\LibraryPolicy;

  protected $policies = [
     Library::class => LibraryPolicy::class,
  ];
 }

3.Policyにupdateメソッドを追加する

Policyクラスを登録したら、認可するアクションごとにメソッドを追加できます。
今回はLibraryクラスを更新できるのはlibrary_staffの管理者権限を持つもののみになります。
従って、updateメソッドを下記のように記載します。

namespace App\Policies;

use App\Enum\LibraryStaffRole;
use App\Models\Library;
use App\Models\LibraryStaff;
use Illuminate\Auth\Access\HandlesAuthorization;

class LibraryPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can update the model.
     * @param LibraryStaff $libraryStaff
     * @param Library $library
     * @return bool
     */
    public function update(LibraryStaff $libraryStaff, Library $library): bool
    {
        // 権限が管理者の場合は図書館の情報の更新を許可する
        return $libraryStaff->role === LibraryStaffRole::OWNER;
    }
}

4.Policyを利用して、アクションの認可をコントローラーに記載する

Policyに認可するアクションを追加したら、実際にコントローラーで使用します。

class LibraryController extends Controller
{
    /**
     * 図書館情報を更新する
     * @param UpdateRequest $request
     * @return UpdatedResource
     */
    public function update(UpdateRequest $request): UpdatedResource
    {
       // 図書館スタッフインスタンスを取得 $libaryStaff
       // 図書館インスタンスを取得 $library
       // 図書館の更新情報をUpdateRequestから取得するメソッド
       $params = $request->getParams();
       if ($libraryStaff->can('update', $library)) {
          $this->libraryService->update($params);
       } else {
          abort(403, '更新が許可されていません。');
       }
    }
}

今回のポイントとして、Policyの方で$libraryの情報を使用して、
認可を確認する必要はないのですが、
引数として $library(Libraryインスタンス) を渡す必要があるということです。
実際に、公式ドキュメントでも、
『updateメソッドは引数としてUserとPostインスタンスを受け取り、
そのユーザーが指定したPostを更新する権限があるかどうかを示す
trueまたはfalseを返す必要があります。』
という風に記載があります。
しかし私はそこの理解ができておらず、Policyで使用しないのであれば、
引数として渡さなくて良いのではないか……?と勘違いしてしまい、
最初下記のようなコードを書いていました。

class LibraryController extends Controller
{
    /**
     * 図書館情報を更新する
     * @param UpdateRequest $request
     * @return UpdatedResource
     */
    public function update(UpdateRequest $request): UpdatedResource
    {
       // 図書館スタッフインスタンスを取得 $libaryStaff
       // 図書館インスタンスを取得 $library
       // 図書館の更新情報をRequestから取得するメソッド
       $params = $request->getParams();
       // $libraryを引数として渡していない
       if ($libraryStaff->can('update') {
          $this->libraryService->update($params);
       } else {
          abort(403, '更新が許可されていません。');
       }
    }
}

そのため認可が通らず、403エラーが出続けることになりました。
ただ一部createメソッドなどはLibraryインスタンスを受け取る必要はないです。
(まだ作成されていないので当然なのですが)
モデルのないメソッドの例として紹介されていたのが、
権限に関わる例でしたので、権限での設定だったら$libraryはいらないんだな!と
思い込んでしまったのが原因だったのだと思います。
更新メソッドの話なので、少し考えればわかることなのですが……。
※ドキュメントの例

/**
 * 指定ユーザーが投稿を作成可能か確認
 *
 * @param  \App\Models\User  $user
 * @return bool
 */
public function create(User $user)
{
    return $user->role == 'writer';
}

それでは次に、図書館のスタッフの登録・編集・削除の処理をPolicyで制御したいと思います。

LibraryStaffPolicyを作成して、それぞれのメソッドを追加する

1.LibraryStaffPolicyに処理を記載する

※Policyの登録、作成は同じ処理ですので割愛します。

作成

先ほども触れた通り、引数はLibraryStaffのみです。

class LibraryStaffPolicy
{
    use HandlesAuthorization;

    /**
     * Determine whether the user can create models.
     *
     * @param LibraryStaff $libraryStaff
     * @return bool
     */
    public function create(LibraryStaff $libraryStaff): bool
    { 
            // 権限が管理者の場合はスタッフの追加を許可する
        return $libraryStaff->role === LibraryStaffRole::OWNER;
    }

編集

編集の場合だけ、
図書館スタッフが管理者権限を持っている場合か、
編集したい図書館スタッフと編集しようとしている図書館スタッフが、
同じidであれば編集できるようにする必要があります。
update(LibraryStaff $libraryStaff LibraryStaff $libraryStaff)
でコマンドで作成されていますが、これだと分かりにくいので、
現在ログインしているスタッフ($currentLibraryStaff)、
そして編集したいスタッフ($targetLibraryStaff)という変数名に変更します。

class LibraryStaffPolicy
{
    /**
     * Determine whether the user can update the model.
     *
     * @param LibraryStaff $currentLibraryStaff
     * @param LibraryStaff $targetLibraryStaff
     * @return bool
     */
    public function update(LibraryStaff $currentLibraryStaff, LibraryStaff $targetLibraryStaff): bool
    {
        // 権限が管理者の場合は図書館スタッフの情報の更新を許可する
        if ($libraryStaff->role === LibraryStaffRole::OWNER) {
            return true;
        }
	// 編集したい図書館スタッフと編集しようとしている図書館スタッフが同じであれば編集を許可する
        if ($currentLibraryStaff->id === $targetLibraryStaff->id) {
            return true;
        }
	// それ以外は許可しない
        return false;
    }
}

削除

削除の場合も引数の情報をわかりやすく変更します。
後は権限の設定は、追加の時と同じですね。

class LibraryStaffPolicy
{
    /**
     * Determine whether the user can delete the model.
     *
     *  @param LibraryStaff $currentLibraryStaff
     * @param LibraryStaff $targetLibraryStaff
     * @return bool
     */
    public function delete(LibraryStaff $currentLibraryStaff, LibraryStaff $targetLibraryStaff): bool
    {
             // 権限が管理者の場合はスタッフの削除を許可する
        return $libraryStaff->role === LibraryStaffRole::OWNER;
    }
}

2.Policyを利用して、アクションの認可をコントローラーに記載する

後は同じように、Policyに認可するアクションを追加したら、
LibraryStaffコントローラーで使用します。

スタッフ新規作成

ここでPolicyでは引数が一つですが、クラス名をcanメソッドに渡す必要があります。
クラス名は、アクションを認可するときに使用するポリシーを決定するために使用されます。

class LibraryStaffController extends Controller
{
    /**
     * スタッフ新規作成
     * @param PostRequest $request
     * @return CreatedResource
     */
    public function store(PostRequest $request): CreatedResource
    {
       $params = $request->getParams();
       // ログインしている図書館スタッフのインスタンスを取得 $libraryStaff
      if ($libraryStaff->can('create', LibraryStaff::class)) {
           $this->staffMemberService->create($params);
      } else {
         abort(403, '作成が許可されていません。');
      }
 }       

スタッフ編集

こちらも図書館の情報を更新する際と同じ様なものです。
ログインしている図書館スタッフの権限もしくはidを確認し、
編集対象の図書館スタッフのインスタンスを渡すことで、
編集が許可されているかを確認することができます。

class LibraryStaffController extends Controller
{
     /**
     * スタッフ情報を編集する
     * @param UpdateRequest $request
     * @return UpdatedResource
     */
     public function update(UpdateRequest $request): UpdatedResource
    {
       $params = $request->getParams();
       // ログインしている図書館スタッフのインスタンスを取得 $currentLibraryStaff
       // 編集しようとしている図書館スタッフのインスタンスを取得 $targetLibraryStaff
      if ($currentLibraryStaff->can('update', $targetLibraryStaff)) {
           $this->staffMemberService->update($params);
      } else {
         abort(403, '削除が許可されていません。');
      }
 }       

スタッフ削除

こちらも図書館スタッフの更新と似ています。
権限のみで判断されるので、$currentLibraryStaffの情報自体は使用しませんが、
引数としてインスタンスを渡す必要があるのは同じです。

class LibraryStaffController extends Controller
{
     /**
     * スタッフを削除する
     * @param DestroyRequest $request
     * @return DestroyResource
     */
     public function destroy(DestroyRequest $request): DestroyResource
    {
       $params = $request->getParams();
       // ログインしている図書館スタッフのインスタンスを取得 $currentLibraryStaff
       // 編集しようとしている図書館スタッフのインスタンスを取得 $targetLibraryStaff
      if ($currentLibraryStaff->can('delete', $targetLibraryStaff)) {
           $this->staffMemberService->delete($params);
      } else {
         abort(403, '削除が許可されていません。');
      }
 }       

これで、Policyを使用して、それぞれのアクションの認可を設定することができました!

まとめ

そこまで複雑ではないはずですし、Policyを使えば簡単に認可を設定できる筈なのですが、
ドキュメントを読んだ際に、思い込んでしまったことから抜け出せずに、
時間を溶かしてしまいました。
あまり同じ様なところで躓く人はいらっしゃらないかもしれないのですが、
備忘録として残しておければと思います。
ここまでお読み下さりありがとうございました。
誰かの知見になれば幸いです。

参考文献

Laravel 8.x 認可

Arsaga Developers Blog

Discussion