Laravel Authorization

Laravel 提供兩種主要的 authorizing action:gate 跟 policy

可以把 gates 跟 policies 想像成 routes 跟 controllers。

Gates are most applicable to actions which are not related to any model or resource, such as viewing an administrator dashboard. In contrast, policies should be used when you wish to authorize an action for a particular model or resource.

通常 gates 跟 policies 會混合使用。

Gates

Gates are simply closures that determine if a user is authorized to perform a given action.

gates 通常定義在 App\Providers\AuthServiceProvider::boot(),用 Gate facade 來定義:

1
2
3
4
5
6
7
8
public function boot()
{
$this->registerPolicies();

Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}

也可以用 class callback 形式:

1
Gate::define('update-post', [PostPolicy::class, 'update']);

定義 gate 後,要在需要擋權限的地方(例如 controller action)使用 Gate::allows()Gate::denies()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post)
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}

// Update the post...
}
}

不用自己丟目前登入的 user instance 進去,laravel 會 handle 這件事。

如果不是要看現在登入的 user,而是要 authorize 另外的 user,可以使用 forUser()

1
2
3
4
5
6
7
if (Gate::forUser($user)->allows('update-post', $post)) {
// The user can update the post...
}

if (Gate::forUser($user)->denies('update-post', $post)) {
// The user can't update the post...
}

可以用 any()none() 來同時 authorize 多個 action:

1
2
3
4
5
6
7
if (Gate::any(['update-post', 'delete-post'], $post)) {
// The user can update or delete the post...
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
// The user can't update or delete the post...
}

如果想要在 authorize 失敗的時候自動丟 Illuminate\Auth\Access\AuthorizationException

1
Gate::authorize('update-post', $post);

Illuminate\Auth\Access\AuthorizationException 會自動被轉成 403 response by Laravel’s exception handler。

可以 support 複雜的 context:https://laravel.com/docs/8.x/authorization#gates-supplying-additional-context

Gate define 的時候也可以 return Illuminate\Auth\Access\Response 來帶更多資訊。Ref

如果想要讓某個 user 可以 grant all ability,可以用 before() Ref

1
2
3
4
5
Gate::before(function ($user, $ability) {
if ($user->isAdministrator()) {
return true;
}
});

If the before closure returns a non-null result that result will be considered the result of the authorization check.

也可以用 after 來定義在所有 authorization check 執行後執行的 closure。

Policies

Policies are classes that organize authorization logic around a particular model or resource.

可以用 make:policy 來產生 policy class,產生的 policy 會放在 app/Policies/

如果想要有跟某個 model 關聯的 example policy:

1
$ php artisan make:policy PostPolicy --model=Post

Register policy

有 policy class 後,要 register 它。

Registering policies is how we can inform Laravel which policy to use when authorizing actions against a given model type.

App\Providers\AuthServiceProvider 裡有個 policies property,它的 key 是 model class,value 是對應的 policy class name。

Registering a policy will instruct Laravel which policy to utilize when authorizing actions against a given Eloquent model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];

/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();

//
}
}

Write Policy

大致長這樣,可以寫任意 function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* @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;
}
}

如果用 --model 生,會生出 CRUD 有關的 function。

使用 Policy

透過 User Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PostController extends Controller
{
/**
* Update the given post.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Post $post
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Post $post)
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}

// Update the post...
}
}

沒有 model instance 的 policy:

1
2
3
4
5
6
7
8
public function store(Request $request)
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}

// Create the post...
}

透過 Controller Helpers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PostController extends Controller
{
/**
* Update the given blog post.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Post $post
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);

// The current user can update the blog post...
}
}

如果是 resource controller,可以這樣:

1
2
3
4
5
6
7
8
9
10
11
12
class PostController extends Controller
{
/**
* Create the controller instance.
*
* @return void
*/
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
}

要注意 CRUD 的 function 的 signature 要符合 laravel 需要的 type hint 才能用

透過 middleware

1
2
3
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');