Laravel5.7: 認可 (記事の編集はオーナーに限る、などの制限)


その記事を書いた本人でなければ編集や削除ができない、というような認可の判断をポリシーで行います。

親記事

Laravel 5.7で基本的なCRUDを作る - Qiita

ポリシーを作る

readouble.com: ポリシーの作成

PowerShell
# 「ユーザー」向け
> php artisan make:policy UserPolicy

# 「記事」向け
> php artisan make:policy PostPolicy

--modelオプションを追加するとCRUDの各アクションに応じたメソッドが自動で生成されますが、今回はそこまで複雑なものは必要はありません。

ポリシーを登録する

readouble.com: ポリシーの登録

app/Providers/AuthServiceProvider.php
+use App\User;
+use App\Policies\UserPolicy;
+use App\Post;
+use App\Policies\PostPolicy;

 class AuthServiceProvider extends ServiceProvider
 {
     protected $policies = [
         'App\Model' => 'App\Policies\ModelPolicy',
+        User::class => UserPolicy::class,
+        Post::class => PostPolicy::class,
     ];

ポリシーの記述

readouble.com: ポリシーの記述

管理者かどうかを判別する

まず、ID番号1番を管理者とする設定をboot()内に追加します。
Laravel5.4でグローバル変数 一言多いプログラマーの独り言

app/Providers/AppServiceProvider.php
    public function boot()
    {
        // グローバル変数
        // 管理者のID番号を1とする
        // 参照: https://stackoverflow.com/questions/28356193/
        config(['admin_id' => 1]);
    }

現在のログインユーザーのID番号が1番かどうかを確認するisAdminメソッドをUserモデルに定義します。
Laravel5.1.11で追加されたGateを試してみる(その3) - Qiita

app/User.php
    /**
     * 現在のユーザー、または引数で渡されたIDが管理者かどうかを返す
     *
     * @param  number  $id  User ID
     * @return boolean
     */
    public function isAdmin($id = null) {
        $id = ($id) ? $id : $this->id;
        return $id == config('admin_id');
    }

全ての権限を持つ管理者ならば細かな権限チェックは不要なので、beforeメソッドを使って他のメソッドの前に実行します。
その際、管理者でない場合の戻り値はnullにします。
falseにすると、全認可を禁止することになってしまうからです。
UserPolicy.phpにも同様にbeforeメソッドを追加します。

app/Policies/PostPolicy.php
    /**
     * 管理者には全ての行動を認可する。
     * 参照: https://qiita.com/inaka_phper/items/09e730bf5a0abeb9e51a
     *
     * @param $user
     * @param $ability
     * @return mixed
     */
    public function before($user, $ability)
    {
        return $user->isAdmin() ? true : null;
    }

ユーザーに対して全認可を禁止したい場合は、beforeメソッドからfalseを返します。nullを返した場合、その認可の可否はポリシーメソッドにより決まります。

readouble.com: ポリシーフィルター

オーナーかどうかを確認する

編集と削除の権限を判断するeditメソッドをPostポリシーに追加します。

app/Policies/PostPolicy.php
use App\Post; // 忘れずにインポートすること
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    (中略)

    /**
     * 編集と削除の認可を判断する。
     *
     * @param  \App\User $user 現在ログインしているユーザー
     * @param  \App\Post $post 現在表示している投稿
     * @return mixed
     */
    public function edit(User $user, Post $post)
    {
        return $user->id == $post->user_id;
    }

Userポリシーのeditメソッドでは、下記のようにログインユーザーのIDとプロフィールページのユーザーIDが一致するかどうかを確認します。

app/Policies/UserPolicy.php
class UserPolicy
{
    (中略)

    /**
     * 編集と削除の認可を判断する。
     *
     * @param  \App\User $user  現在ログインしているユーザー
     * @param  \App\User $model 現在表示しているプロフィールページのユーザー
     * @return mixed
     */
    public function edit(User $user, User $model)
    {
        return $user->id == $model->id;
    }

コントローラで認可する

readouble.com: コントローラヘルパによる認可

コントローラのeditupdatedestroyアクションの冒頭に、authorizeによる認可を追加します。

app/Http/Controllers/PostController.php
 class PostController extends Controller
 {
     public function edit(Post $post)
     {
         // update, destroyでも同様に
+        $this->authorize('edit', $post);
         return view('posts.edit', ['post' => $post]);
     }
app/Http/Controllers/UserController.php
 class UserController extends Controller
 {
     public function edit(User $user)
     {
         // update, destroyでも同様に
+        $this->authorize('edit', $user);
         return view('users.edit', ['user' => $user]);
     }

これで、自分の記事以外の記事を編集しようとすると403エラーが表示されるようになりました。
ただし、管理者は制限されません。

ビューで認可を確認する

Bladeテンプレートによる認可

権限がなければ編集ボタンと削除ボタンをはじめから表示させないようにします。

resources/views/posts/show.blade.php
     {{-- 編集・削除ボタン --}}
+    @can('edit', $post)
         <div>
             <a href="{{ url('posts/'.$post->id.'/edit') }}" class="btn btn-primary">
                 {{ __('Edit') }}
             </a>
             @component('components.btn-del')
                 @slot('controller', 'posts')
                 @slot('id', $post->id)
                 @slot('name', $post->title)
             @endcomponent
         </div>
+    @endcan
resources/views/users/show.blade.php
     {{-- 編集・削除ボタン --}}
+    @can('edit', $user)
         <div>
             <a href="{{ url('users/'.$user->id.'/edit') }}" class="btn btn-primary">
                 {{ __('Edit') }}
             </a>
             @component('components.btn-del')
                 @slot('controller', 'users')
                 @slot('id', $user->id)
                 @slot('name', $user->name)
             @endcomponent
         </div>
+    @endcan

(中略)

                 {{-- 記事の編集・削除ボタンのカラム --}}
-                <th></th>
+                @can('edit', $user) <th></th> @endcan

(中略)

+                @can('edit', $user)
                     <td nowrap>
                         <a href="{{ url('posts/'.$post->id.'/edit') }}" class="btn btn-primary">
                             {{ __('Edit') }}
                         </a>
                         @component('components.btn-del')
                             @slot('controller', 'posts')
                             @slot('id', $post->id)
                             @slot('name', $post->title)
                         @endcomponent
                     </td>
+                @endcan

これで、ユーザーが403エラーに頻繁に遭遇するという不便さを解消できます。
ただし、URLを直接書き換えてeditページへ移動しようとすると403エラーとなります。
これは当然の挙動でしょう。

管理者だけに表示したい

「オーナー、または管理者かどうか」ではなく単純に「管理者かどうか」だけを判断したい場合があります。
これはポリシーとは関係ありませんが、Userモデルで定義したisAdminメソッドを使えば実現できます。

resources/views/users/index.blade.php
    <h1>{{ $title }}</h1>

    @if (Auth::check() && Auth::user()->isAdmin())
        {{-- 管理者にのみ、「ユーザー作成」のメニューを表示する --}}
        <div class="mb-2">
            <a href="{{ url('users/create') }}" class="btn btn-primary">
                {{ __('Create') }}
            </a>
        </div>
    @endif

いきなりAuth::user()->isAdmin()とするとログインしていない場合にエラーとなるため、まずはAuth::check()でログインしているかどうかを確認しています。
Laravel API: check()

管理者の編集・削除は不可とする

たとえ管理者自身であっても、管理者のアカウントは編集や削除ができないようにします。

resources/views/users/show.blade.php
     {{-- 編集・削除ボタン --}}
+    {{-- 管理者のページを表示中の場合は、編集・削除ボタンを表示させない --}}
+    @if (Auth::check() && !Auth::user()->isAdmin($user->id))
         @can('edit', $user)
             <div>
                 <a href="{{ url('users/'.$user->id.'/edit') }}" class="btn btn-primary">
                     {{ __('Edit') }}
                 </a>
                 @component('components.btn-del')
                     @slot('controller', 'users')
                     @slot('id', $user->id)
                     @slot('name', $user->title)
                 @endcomponent
             </div>
         @endcan
+    @endif

表示中のプロフィールページのユーザーID($user->id)を、先ほど作ったisAdmin()の引数にしています。


暫定処置: @authを追加する

2018年9月16日現在、下記のような@authの記述が必要です。

resources/views/posts/show.blade.php
+    @auth
         @can('edit', $post)
             <div class="edit">
                 <a href="{{ url('posts/'.$post->id.'/edit') }}" class="btn btn-primary">
                     {{ __('Edit') }}
                 </a>
                 @component('components.btn-del')
                     @slot('controller', 'posts')
                     @slot('id', $post->id)
                     @slot('name', $post->title)
                 @endcomponent
             </div>
         @endcan
+    @endauth
resources/views/users/show.blade.php
                     {{-- 記事の編集・削除ボタンのカラム --}}
-                    @can('edit', $user) <th></th> @endcan
+                    @auth
+                        @can('edit', $user)
+                            <th></th>
+                        @endcan
+                    @endauth

(中略)

+                        @auth
                             @can('edit', $user)
                                 <td nowrap>
                                     <a href="{{ url('posts/' . $post->id . '/edit') }}" class="btn btn-primary">
                                         {{ __('Edit') }}
                                     </a>
                                     @component('components.btn-del')
                                         @slot('controller', 'posts')
                                         @slot('id', $post->id)
                                         @slot('name', $post->title)
                                     @endcomponent
                                 </td>
                             @endcan
+                        @endauth

そうしなければ、未ログイン状態でshowページを開くと、下記のようなエラーが表示されます。

ErrorException (E_ERROR)
ReflectionFunction::__construct() expects parameter 1 to be string, array given

Laravel5.6では@canだけで未ログイン状態を除外してくれていたのですが…。
仕様変更なのか、私のコーディングに不備があるのか、不明です。