Команда Mastering Laravel дуже любить пакет laravel-permission і використовує його на більшості проєктів. Автор матеріалу Joel розповідає про неочевидну поведінку, яка виникає, коли авторизація на основі прав поєднується з прив’язкою моделей до маршрутів.
Перевірка прав через middleware can
Зручна особливість пакета в тому, що він дозволяє використовувати штатний middleware Laravel can, щоб перевірити, чи має користувач певний дозвіл. У файлі маршрутів можна обгорнути цілу групу маршрутів у перевірку на основі права:
// routes/web.php
Route::middleware('can:manage-articles')->group(function () {
Route::get('/articles', 'ArticleController@index');
Route::get('/articles/{article}', 'ArticleController@show');
// more article routes...
});
У реальних проєктах замість рядка зазвичай використовують enum Permission, але в прикладі для простоти та читабельності вжито рядок.
Якщо в користувача немає права manage-articles, він не повинен мати доступу до жодного з цих маршрутів. Додаткові, більш деталізовані перевірки на кшталт «чи може він редагувати саме цю модель Article» тут не потрібні - доступ просто відхиляється на ранньому етапі.
Несподівана поведінка: 404 замість 403
Проте трапляється неочікуване. Якщо користувач без права доступу до цієї групи звертається до маршруту на кшталт /articles/123, де 123 - неіснуюча стаття, він отримає помилку 404, а не очікувану 403. Причина - route model binding.
За замовчуванням Laravel надає middleware SubstituteBindings вищий пріоритет, ніж middleware Authorize. Тому, якщо модель не знайдено, відповідь 404 повертається ще до того, як Authorize матиме змогу віддати 403.
Чому порядок саме такий
Перш ніж шукати рішення, варто зрозуміти, навіщо так зроблено. Деякі методи класичних політик (policies) потребують уже прив’язаної моделі. Наприклад, типовий метод edit у політиці виглядає так:
public function edit(User $user, Article $article)
{
return $user->id === $article->user_id;
}
Якщо модель не прив’язана, метод політики не можна виконати, а отже middleware Authorize не зможе виконати свою роботу. Тому логічно, що SubstituteBindings спрацьовує першим.
У розглянутому прикладі прив’язана модель не потрібна, але проста зміна порядку middleware місцями зламає ту частину логіки авторизації в політиках, яка залежить від прив’язаної моделі.
Рішення
У такому випадку можна скористатися middleware самого пакета - PermissionMiddleware - замість can, а також переконатися, що він має вищий пріоритет, ніж SubstituteBindings. Так перевірка прав виконається раніше за прив’язку моделей, а політики, що потребують моделі, продовжать працювати як раніше.