Авторизація — це не “щоб не пускало чужих”. Це система правил, яка захищає бізнес-процеси від помилок, зловживань і випадкових дій. У CMS/адмінці найнебезпечніші проблеми виникають не через хакерів, а через неправильну конфігурацію доступів: редактор може змінити те, що не повинен, стажер випадково видалив дані, або хтось отримав доступ до “контролю публікацій” без права. Якщо у вас вже є permissions-middleware, це означає, що доступи — критична частина архітектури, і їх треба будувати по стандарту: чіткі зони, чіткі дії, мінімум дублювання, максимум прозорості.
У Laravel авторизація складається з трьох рівнів: middleware permissions (вхід у зону), Policies/Gates (право на конкретну дію), і бізнес-обмеження всередині сервісів (правила станів і полів). Ідеальний порядок такий: middleware не пускає в “розділ”, policy не дає виконати “дію”, а сервіс не дає порушити “правила домену”, навіть якщо десь помилились із middleware/policy. Це три лінії оборони, і саме так робиться на живих проєктах.
Почнемо з middleware permissions, бо це ваша база. Middleware працює на вході до маршруту. Він відповідає на питання: “чи має користувач доступ до цього endpoint взагалі?”. Це зручно для зон: адмінка, модуль “Ресурси”, модуль “Счета”, “Контроль публікацій”, “Довідники”. Тут найкраща практика — групувати маршрути і вішати middleware на групу, а не на кожен маршрут по одному. Так ви не забудете захист на новому endpoint, бо він автоматично успадкує правила групи.
Але middleware — це грубий фільтр. Він не знає контексту об’єкта. Наприклад, middleware може сказати “ти можеш редагувати новини”, але він не розуміє, чи це новина цього автора, чи вона вже опублікована, чи її статус “на затвердженні”, чи це платний матеріал, чи є обмеження по ресурсу. Оце вже зона Policies (або Gates). Тому стандарт: middleware вирішує доступ до розділу/дії в загальному, а policy вирішує доступ до конкретного ресурсу з урахуванням даних.
Policies — це авторизація “на ресурс”. Тобто політика описує, що користувач може робити з конкретною моделлю: view, create, update, delete, publish, approve і т.д. У Laravel це виглядає як клас NewsPolicy, TagPolicy, InvoicePolicy тощо. Ключова цінність policy: ти концентруєш правила в одному місці, а не розмазуєш по контролерах. І це сильно знижує ризик “у цьому контролері перевірили, а в іншому забули”.
Типовий розподіл такий. viewAny — чи можна бачити список. view — чи можна дивитися конкретний запис. create — чи можна створювати. update — чи можна редагувати конкретний запис. delete — чи можна видаляти. Далі додаються кастомні дії: approve, publish, toggleSubscription, setStatus. Саме кастомні дії критичні для вашої адмінки, бо там багато станів і обмежень. І якщо ви тримаєте ці правила в policy, вони стають видимими й керованими.
Gates — це авторизація “на дію без моделі” або на загальні правила. Gate — це умовна функція типу “чи може користувач робити X”. Вона корисна, коли немає конкретної моделі, або коли правило не прив’язане до однієї сутності. Наприклад: “доступ до адмінки”, “доступ до розділу аналітики”, “можна бачити фінансові дані”, “можна управляти користувачами”. Gates також корисні як “склейка” для ваших permission-рядків, якщо у вас є власна система permissions і ви хочете мапити її на Laravel authorization механізми.
Практично: якщо у вас є конкретний запис (новина, рахунок, ресурс) і рішення залежить від цього запису — це policy. Якщо рішення не залежить від конкретного запису, а лише від ролі/прав користувача — це gate або middleware. Це правило відразу прибирає половину плутанини.
Тепер критична частина: middleware permissions і policy не мають дублювати одне й те саме правило. Дублювання призводить до розсинхрону. Наприклад, ви додали новий permission, оновили middleware, але забули policy — і частина дій працює, частина дає 403. Правильна архітектура така: middleware забезпечує “вхід” (базовий допуск), policy забезпечує “деталі” (включно з контекстом), а сервіс забезпечує “цілісність сценарію”.
Як це виглядає для CRUD модулів. На index ви часто ставите middleware permission:tags.view і плюс policy viewAny. Для create/store — permission:tags.create + policy create. Для edit/update — permission:tags.update + policy update($tag). Для delete — permission:tags.delete + policy delete($tag). Це здається надмірним, але на критичних модулях це нормально: middleware швидко відсікає без прав, policy захищає від помилок у логіці (наприклад, не можна видалити, якщо є зв’язки).
Далі — найцінніше для вашого проєкту: permissions на рівні полів і станів. Це те, що майже завжди недоробляють новачки. Є різні рівні доступу: “може редагувати контент”, “може змінювати статус”, “може виставляти subscription_required”, “може публікувати в соцмережі”. Це не просто “update”. Це набір capability, і їх треба розділяти. У policy ви робите окремі методи на дії (approve/publish/toggleSubscription), а у сервісі — додаткову перевірку, які саме поля дозволено змінювати в цьому сценарії.
Чому перевірка полів повинна бути ще й у сервісі. Бо policy відповідає “можеш/не можеш виконати дію”, але update часто приходить одним запитом з багатьма полями. Навіть якщо користувач має право редагувати текст, він може підсунити в запит поле approval_status чи created_by, якщо ви не обмежили whitelist. Тому у сервісі ви робите “ефективні дані”: дозволені ключі в залежності від прав користувача і стану запису. Це практичний захист від підміни полів і випадкових змін.
Окрема тема — фільтрація списків за доступами. Дуже часто проблема не в тому, що “можна натиснути”, а в тому, що користувач бачить те, що не повинен бачити. Для цього у вас має бути єдиний спосіб будувати запити з урахуванням доступів: або scope на моделі, або окремий query builder/servіс. Наприклад, автор бачить тільки свої новини, редактор — всі новини свого ресурсу, адміністратор — все. Якщо це правило розкидане по контролерах, рано чи пізно ви забудете його на новій сторінці і відкриєте витік даних.
Найбільш стабільний підхід — “policy + scope/фільтр запиту”. Policy відповідає за дії, а для списків ви робите “доступний набір” записів на рівні query. Тоді навіть якщо хтось підставить ID в URL, він не знайде запис, або policy заборонить доступ. Це двошаровий захист: “не показуємо” і “не дозволяємо”.
Тепер про те, як це правильно “підключається” в коді, не ламаючи структуру. Для маршрутів: групи з middleware permissions на модулі. Для контролерів: виклики authorize() або can() на потрібні методи перед дією. Для моделей: опис policy в AuthServiceProvider. Для сервісів: whitelist полів і перевірки бізнес-станів. Ви отримуєте систему, де помилка в одному шарі не відкриє дірку повністю.
Типові помилки, які потрібно уникати одразу. Перша — робити авторизацію тільки middleware і вважати, що цього достатньо. Це небезпечно для дій з конкретними записами. Друга — робити авторизацію тільки в контролері через if, а не через policy: ви отримаєте розмазану логіку і забудете перевірку в одному місці. Третя — змішувати авторизацію з валідацією: “якщо немає права — повернемо 422”. Це неправильні статуси і плутанина. Четверта — не враховувати стани: “можна update завжди”, хоча після публікації або після оплати правила змінюються.
Висновок: для критичних систем стандарт такий. Middleware permissions тримає вхід у модулі й розділи, Policies описують доступи до конкретних ресурсів і дій, Gates — для загальних правил без моделі, а сервісний шар гарантує, що навіть валідний запит не змінить заборонені поля і не порушить правила станів. Якщо ви зробите цю структуру зараз, то в майбутньому — коли з’являться нові ролі, нові статуси, нові модулі — ви не будете “підкручувати доступи по всьому коду”, а просто додасте правило в одному місці і будете впевнені, що система не розсиплеться.