Laravel 5. Ієрархічний RBAC для самих маленьких

Як вам мабуть відомо, RBAC — це управління доступом на основі ролей. Всі, хто створювали системи трохи більші ніж домашня сторінка і трохи менші ніж Держпослуги, замислювалися про те, як розмежувати права користувачів.
У цій статті я не буду розповідати про те, що таке RBAC і чому це добре (хоча небагато, звичайно, розповім), а познайомлю вас зі своєї скромної розробкою (h-rbac) і спробую пояснити, чому вона з деяким аспектам краще, ніж відомі "монстри".
Вступ
Два стовпи, на яких тримається RBAC — це ролі і операції (role and permission). Слово "операція" мені подобається більше ніж "дозвіл", так і сенс відображає більш вірно. Виглядає це так:
Код -> Операція -> Роль -> Користувач
Таким чином в коді ми перевіряємо можливість виконання операції:
if ($request->user()->can('add to favorites')) {
// робимо щось пов'язане з додаванням статті в обране
}

У свою чергу модуль RBAC з'ясовує, чи дозволена для даного користувача зазначена операція (тобто міститься вона в його ролі) і, якщо так, то вміст блоку виконується.
Ні в якому разі не варто перевіряти в коді наявність у користувача ролі, т. к. сьогодні "менеджер" може додавати в обране, а завтра вже не може. Шукати по тексту і виключати з перевірок окремі ролі — справа не вдячна, власне для цього і існують "операції".
Операція завжди безпосередньо пов'язана з блоком програми (див. приклад вище) і при зміні ролей тут точно нічого міняти не доведеться. Натомість до складу ролі операції включаються і виключаються легко, просто і централізовано.
Зазвичай список ролей і операцій зберігається в базі даних. Користувачі і ролі найчастіше пов'язані відношенням "багато до багатьох", таким же ставленням пов'язані і ролі операціями.
Мастодонти
Всі, хто створювали системи трохи більші... щось я повторююсь. Коротше, всі ви знаєте основних гравців ринку обмеження доступу для Laravel. Це:
Це дійсно серйозні продукти мають багато різних "булочок" зразок перевизначення операцій для конкретного користувача і т. п., але у мене до них одне просте запитання (прощай карма, нам було добре разом,):
Як дозволити користувачеві редагувати тільки статті?
Хм… а ніяких вбудованих механізмів для цього немає! І це дуже дивно, оскільки в більшості систем користувачі генерують якийсь контент (наприклад, статті) і повинні мати можливість його редагування, при цьому не маючи доступу до редагування чужого.
І ще: що якщо мій проект не такий вже великий? Якщо весь цей геморой з зберіганням ролей і операцій в БД, сполучними таблицями для "багато до багатьох" і створенням цілого UI для управління всім цим господарством лишній?
— Та-дам!
Великий секрет для маленької...
Отже, прийшов час поговорити про власні проделках. Назва статті не випадково звучить "...RBAC для самих маленьких". Мова, звичайно, не йде про ТТХ програмістів, а скоріше про розмірі проектів (і, звичайно ж, ця оцінка досить умовна).
Якщо у вашому проекті не дуже багато операцій і не дуже багато ролей і ви не проти мріяли зберігати їх у вигляді масиву, радий представити вам модуль h-rbac (hierarchical RBAC with callbacks).
Немає ніяких протиріч з тим, щоб додати до модулю різні провайдери і зберігати ролі та операції хоч в БД, хоч в жп... будь-якому іншому цікавому для вас місці.
Відразу хочу зізнатися, що принцип, використаний у даному модулі, підглянули мною в Yii. Він був позбавлений від непотрібної (на мій погляд) сутності Task і реального збочення у вигляді bizRule, вычисляющейся з допомогою
eval()
(природно, я говорю про Yii 1.1, 2.0 стало по-іншому, але, на мій погляд, теж досить заплутано).

Вимоги

Для роботи з модулем потрібно Laravel 5.1 або вище.
Почати хочеться з того, що стандартна система дозволів (операцій) з'явилася Laravel починаючи з версії 5.1. Вона має хорошу інфраструктуру, багато корисних методів, підтримується в blade і навіть дозволяє дозволити користувачеві редагувати свої статті (тобто передавати на перевірку аргументи), але(!) там геть відсутні ролі…
… всі користувачі рівні — прямо демократія якась. Але ми-то з вами знаємо, що обов'язково повинен бути хтось "рівніші" інших. Інакше ніяк!
Так от, модуль h-rbac, по суті, є надбудовою над стандартним механізмом авторизації (не плутати з аутентифікацією!), додає туди ролі, а також ієрархію операцій. Тому ви продовжите користуватися абсолютно всіма стандартними "плюшками", але в контексті наявності ролей.

Установка

З допомогою Composer
$ composer require dlnsk/h-rbac

Зареєструємо провайдер в config/app.php
Dlnsk\HierarchicalRBAC\HRBACServiceProvider::class,

Опублікуємо потрібні нам елементи
$ php artisan vendor:publish --provider="Dlnsk\HierarchicalRBAC\HRBACServiceProvider"

а саме:
  • конфігураційний файл (config/h-rbac.php)
  • міграцію (додає текстове поле
    role
    до таблиці
    users
    )
  • клас конфігурації ролей, операцій і колбэков (app/Classes/Authorization/AuthorizationClass.php)
так, Так, дорогі друзі, як сказано в заголовку, це модуль "для самих маленьких", тому у користувача може бути тільки одна роль! Але з іншого боку, адже ролі — це всього лише масив, додати ще одну простіше простого.
Блекджек і дівчинки
Візьмемо наступні ролі:
  • admin
  • manager
  • user
і набір операцій:
  • update-post
  • add to favorites
Припустимо, що ми бажаємо розділити користувачів на тих, хто може редагувати всі статті, тих, хто буде редагувати статті лише в певних категоріях і тих, хто зможе редагувати лише власні статті.
Для цього ми насправді повинні створити три операції і об'єднати їх у ланцюг починаючи з самою відкритою і до самої обмеженою:
update-post -> update-post-in-category -> update-own-post
class AuthorizationClass extends Authorization
{
public function getPermissions() {
return [
'update-post' => [
// Необов'язкове властивість "опис"
'description' => 'Редагування будь-яких статей',
// Використовується для створення ланцюга (ієрархії) операцій
'next' => 'update-post-in-category',
],
'update-post-in-category' => [
'description' => 'Редагування статей певної категорії',
'next' => 'update-own-post',
],
'update-own-post' => [
'description' => 'Редагування власних статей',
// Тут ланцюг закінчується
],
// Вибране
'add to favorites' => [
'description' => 'Додавання статті в список обраних',
],
];
}
}

Призначення операцій ролями дуже просте. Хто що може робити очевидно:
class AuthorizationClass extends Authorization
{
public function getRoles(){
return [
'admin' => [
'update-post',
],
'manager' => [
'update-post-in-category',
],
'user' => [
'update-own-post',
'add to favorites',
],
];
}
}

Зверніть увагу, що по суті у нас тільки дві операції
update-post
та
add to favorites
саме можливість їх виконання ми і повинні перевіряти. Допоміжні операції
update-post-in-category
та
update-own-post
будуть перевірені автоматично, оскільки вони є вмістом однієї з
update-post
ланцюга.
// PostController.php

class PostController extends Controller
{
public function update(Post $post)
{
$this->authorize('update-post', $post);
// продовжуємо, якщо операція дозволена
}
}

<!-- post-view.php -->

<h1>{{ $post- > title }}</h1>
<ul class="post-tools">
<li class="post author">{{ $post->author->username }}</li>
@can('update-post', $post)
<li class="post update"><button></button></li>
@endcan
@can('add to favorites')
<li class="post add-favorite"><button></button></li>
@endcan
</ul>

Операція
add to favorites
означає можливість додати в обране будь-яку статтю, тобто ніяких додаткових перевірок робити не потрібно, достатньо, щоб ця операція містилася в ролі користувача. З цієї причини не можна передавати на перевірку об'єкт
$post
(але я би передавав його в будь-якому випадку, тому що сьогодні додаткових умов немає, а завтра можуть з'явитися).
Залишилося з'ясувати, як робити перевірки тих самих додаткових умов для
update-post-in-category
та
update-own-post
. Для цього досить додати два методу (назви методів отримуються шляхом камелкейсизации(о-па!) назви операції):
class AuthorizationClass extends Authorization
{
public function updatePostInCategory($user, $post, $permission) {
// Даний метод повертає модель у разі, якщо $post містить id моделі
$post = $this->getModel(\App\Post::class, $post);

return $user->category_id === $post->category_id;
}

public function updateOwnPost($user, $post, $permission) {
$post = $this->getModel(\App\Post::class, $post);

return $user->id === $post->user_id;
}
}

Параметр
$permission
в обох цих методах буде містити назву початкової операції
update-post
.

Логіка перевірки

Всі операції знаходяться в ланцюзі перевіряються одна за одною у вибраному користувачем порядку і операція:
  • дозволяється, якщо вона міститься в ролі і функція додаткової перевірки відсутня;
  • дозволяється, якщо вона міститься в ролі і функція додаткової перевірки повертає true (перевірка інших операцій ланцюга припиняється);
  • забороняється, якщо жодна з операцій ланцюга не міститься в ролі;
  • забороняється, якщо для всіх операцій ланцюга, що містяться в ролі, функції додаткової перевірки повернули false.

Фішка для ледачих

Давайте додамо операцію
delete-post
. Логіка підказує, що видаляти користувач може ті статті, які він може редагувати. Подібна ситуація зустрічається досить часто і щоб не створювати додаткову ланцюг операцій і абсолютно ідентичні функції перевірки аргументу скористаємося однією хитрістю — параметром
equal
:
class AuthorizationClass extends Authorization
{
public function getPermissions() {
return [
'update-post' => [
// Необов'язкове властивість "опис"
'description' => 'Редагування будь-яких статей',
// Використовується для створення ланцюга (ієрархії) операцій
'next' => 'update-post-in-category',
],
'update-post-in-category' => [
'description' => 'Редагування статей певної категорії',
'next' => 'update-own-post',
],
'update-own-post' => [
'description' => 'Редагування власних статей',
// Тут ланцюг закінчується
],
// Вибране
'add to favorites' => [
'description' => 'Додавання статті в список обраних',
],
// Видалення
'delete-post' => [
'description' => 'Видалення статей',
'знак' => 'update-post', // Застосовуємо правила аналогічні редагування
],
];
}

public function getRoles() {
return [
'admin' => [
'update-post',
'delete-post',
],
'manager' => [
'update-post-in-category',
],
'user' => [
'update-own-post',
'add to favorites',
'delete-post',
],
];
}
}

Виходячи з цього прикладу admin зможе видаляти будь-які посади, user — тільки свої власні, а от manager не зможе видалити взагалі нічого, т. к. в його ролі немає операції
delete-post
, а значить і перевіряти нічого не потрібно.
Пишіть, Шура, пишіть...
Як вже було сказано раніше, модуль є надбудовою над стандартною для Laravel 5.1 і вище системою авторизації, тому використовувати його потрібно так, як написано в документації, а якщо коротенько, то ось вам примерчики:
if (\Gate::allows('update-post', $post)) {
// робимо що-небудь, якщо це дозволено поточному користувачу
}
...
if (\Gate::denies('update-post', $post)) {
abort(403);
}
...
if (\Gate::forUser($user)->allows('update-post', $post)) {
// робимо що-небудь, якщо це дозволено іншому користувачеві
}

З моделі User:
if ($request->user()->can('update-post', $post)) {
// робимо що-небудь
}
...
if ($request->user()->cannot('update-post', $post)) {
abort(403);
}

В контролері:
$this->authorize('update-post', $post);

З допомогою Blade
@can('update-post', $post)
<!-- Поточний користувач може оновити статтю -->
@else
<!-- Поточний користувач не може оновити статтю -->
@endcan

@cannot('update-post', $post)
<!-- Поточний користувач не може оновити статтю -->
@endcannot

Крім того, спеціально для поганих хлопчиків і дівчаток, додана додаткова директива
@role
яку можна використовувати разом з
@else

@role('user|manager)
<!-- Поточний користувач має будь-яку з ролей -->
@endrole

Ось такий вийшов модуль, що поєднує потужність стандартних можливостей, дійсно необхідний функціонал і легкість конфігурації. Спасибі за увагу!
Вся конфігурація операцій і ролей в одному місці
// app/Classes/Authorization/AuthorizationClass.php
<?php
namespace App\Classes\Authorization;
use Dlnsk\HierarchicalRBAC\Authorization;

class AuthorizationClass extends Authorization
{
public function getPermissions() {
return [
'update-post' => [
// Необов'язкове властивість "опис"
'description' => 'Редагування будь-яких статей',
// Використовується для створення ланцюга (ієрархії) операцій
'next' => 'update-post-in-category',
],
'update-post-in-category' => [
'description' => 'Редагування статей певної категорії',
'next' => 'update-own-post',
],
'update-own-post' => [
'description' => 'Редагування власних статей',
// Тут ланцюг закінчується
],
// Вибране
'add to favorites' => [
'description' => 'Додавання статті в список обраних',
],
// Видалення
'delete-post' => [
'description' => 'Видалення статей',
'знак' => 'update-post', // Застосовуємо правила аналогічні редагування
],
];
}

public function getRoles() {
return [
'admin' => [
'update-post',
'delete-post',
],
'manager' => [
'update-post-in-category',
],
'user' => [
'update-own-post',
'add to favorites',
'delete-post',
],
];
}

////////////// Callbacks ///////////////
public function updatePostInCategory($user, $post) {
// Даний метод повертає модель у разі, якщо $post містить id моделі
$post = $this->getModel(\App\Post::class, $post);

return $user->category_id === $post->category_id;
}

public function updateOwnPost($user, $post) {
$post = $this->getModel(\App\Post::class, $post);

return $user->id === $post->user_id;
}
}

P. S.:
Мало не забув… Назви ролей "admin", "manager" і "user" взяті в цій статті просто як приклад. Насправді роль "admin" вбудована в модуль і не потребує визначення. Вона робить користувача ROOT. До нього не застосовуються ніякі перевірки, йому дозволено абсолютно все. Ура, товариші!
Посилання:
  1. RBAC Авторизація в YII і LDAP
  2. Модуль h-rbac на GitHub
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.