その記事を書いた本人でなければ編集や削除ができない、というような認可の判断をポリシーで行います。
親記事
Laravel 5.7で基本的なCRUDを作る - Qiita
ポリシーを作る
readouble.com: ポリシーの作成
# 「ユーザー」向け
> php artisan make:policy UserPolicy
# 「記事」向け
> php artisan make:policy PostPolicy
--model
オプションを追加するとCRUDの各アクションに応じたメソッドが自動で生成されますが、今回はそこまで複雑なものは必要はありません。
ポリシーを登録する
readouble.com: ポリシーの登録
+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でグローバル変数 一言多いプログラマーの独り言
public function boot()
{
// グローバル変数
// 管理者のID番号を1とする
// 参照: https://stackoverflow.com/questions/28356193/
config(['admin_id' => 1]);
}
現在のログインユーザーのID番号が1番かどうかを確認するisAdmin
メソッドをUserモデルに定義します。
Laravel5.1.11で追加されたGateを試してみる(その3) - Qiita
/**
* 現在のユーザー、または引数で渡された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
メソッドを追加します。
/**
* 管理者には全ての行動を認可する。
* 参照: 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ポリシーに追加します。
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が一致するかどうかを確認します。
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: コントローラヘルパによる認可
コントローラのedit
、update
、destroy
アクションの冒頭に、authorize
による認可を追加します。
class PostController extends Controller
{
public function edit(Post $post)
{
// update, destroyでも同様に
+ $this->authorize('edit', $post);
return view('posts.edit', ['post' => $post]);
}
class UserController extends Controller
{
public function edit(User $user)
{
// update, destroyでも同様に
+ $this->authorize('edit', $user);
return view('users.edit', ['user' => $user]);
}
これで、自分の記事以外の記事を編集しようとすると403エラーが表示されるようになりました。
ただし、管理者は制限されません。
ビューで認可を確認する
権限がなければ編集ボタンと削除ボタンをはじめから表示させないようにします。
{{-- 編集・削除ボタン --}}
+ @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
{{-- 編集・削除ボタン --}}
+ @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
メソッドを使えば実現できます。
<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()
管理者の編集・削除は不可とする
たとえ管理者自身であっても、管理者のアカウントは編集や削除ができないようにします。
{{-- 編集・削除ボタン --}}
+ {{-- 管理者のページを表示中の場合は、編集・削除ボタンを表示させない --}}
+ @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
の記述が必要です。
+ @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
{{-- 記事の編集・削除ボタンのカラム --}}
- @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
だけで未ログイン状態を除外してくれていたのですが…。
仕様変更なのか、私のコーディングに不備があるのか、不明です。