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

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

# 親記事
[Laravel 5.7で基本的なCRUDを作る - Qiita](https://qiita.com/sutara79/items/ef30fcdfb7afcb2188ea)


# ポリシーを作る
:link: readouble.com: [ポリシーの作成](https://readouble.com/laravel/5.7/ja/authorization.html#generating-policies)

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

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

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


# ポリシーを登録する
:link: readouble.com: [ポリシーの登録](https://readouble.com/laravel/5.7/ja/authorization.html#registering-policies)

```diff: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,
     ];
```


# ポリシーの記述
:link: readouble.com: [ポリシーの記述](https://readouble.com/laravel/5.7/ja/authorization.html#writing-policies)

## 管理者かどうかを判別する
まず、ID番号1番を管理者とする設定を`boot()`内に追加します。
:link: [Laravel5.4でグローバル変数 一言多いプログラマーの独り言](http://programmer-jobs.blogspot.jp/2017/02/laravel-5-4-global-variables.html)

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

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

```php: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`メソッドを追加します。

```php: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を返した場合、その認可の可否はポリシーメソッドにより決まります。

:link: readouble.com: [ポリシーフィルター](https://readouble.com/laravel/5.7/ja/authorization.html#policy-filters)


## オーナーかどうかを確認する
編集と削除の権限を判断する`edit`メソッドをPostポリシーに追加します。

```php: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が一致するかどうかを確認します。

```php: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;
    }
```

# コントローラで認可する
:link: readouble.com: [コントローラヘルパによる認可](https://readouble.com/laravel/5.7/ja/authorization.html#via-controller-helpers)

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

```diff: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]);
     }
```

```diff: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エラーが表示されるようになりました。
ただし、管理者は制限されません。

# ビューで認可を確認する
:link: [Bladeテンプレートによる認可](https://readouble.com/laravel/5.7/ja/authorization.html#via-blade-templates)

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

```diff: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
```

```diff: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`メソッドを使えば実現できます。

```html: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()`でログインしているかどうかを確認しています。
:link: Laravel API: [check()](https://laravel.com/api/5.7/Illuminate/Contracts/Auth/Guard.html#method_check)

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

```diff: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`の記述が必要です。

```diff: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
```

```diff: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
```
![001.png](https://qiita-image-store.s3.amazonaws.com/0/27816/ea71d320-c4c8-7557-716e-6c2ee098b09b.png)


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