RBAC у Yii2 будується на ієрархії ролей і дозволів з правилами: ієрархія зберігається в БД або файлі, перевірка йде через Yii::$app->user->can(), а правила додають динамічні умови на кшталт авторства.
Як питають
Чим правило (rule) відрізняється від permission?
Як зберігати RBAC-дерево: в БД чи у файлах?
Як зробити, щоб автор міг редагувати лише свої пости?
Пояснення
RBAC у Yii2 складається з трьох видів елементів. Permission — атомарна дія на кшталт updatePost. Role — група дозволів або інших ролей: author, admin. Rule — клас із методом execute(), який під час перевірки отримує користувача, елемент і параметри й повертає bool. Разом вони утворюють орієнтований граф, і Yii::$app->user->can('updatePost', ['post' => $post]) шукає шлях від призначень користувача до потрібного дозволу, виконуючи правила на кожному вузлі.
Правила роблять RBAC динамічним. Дозвіл updateOwnPost з правилом AuthorRule є дочірнім для updatePost: автор проходить до updatePost лише тоді, коли правило підтверджує, що пост його. Адміністратор отримує updatePost напряму без умов. Так авторство описується один раз у графі, а не через if у кожному екшні.
Ієрархія зберігається в DbManager або PhpManager, які реалізують один інтерфейс. Для продакшену з динамічними призначеннями потрібен DbManager з увімкненим кешем, інакше кожна перевірка робить кілька запитів до auth-таблиць. PhpManager підходить для невеликих застосунків із фіксованими ролями і дозволяє тримати права в git.
У коді перевіряють дозволи, а не ролі: can('updatePost'), а не can('admin'). Тоді зміна ролей не вимагає правки коду, а при міграції на Laravel чи Symfony дозволи з правилами майже один в один стають Policies або Voters.
КодPHP
// Правило: дозвіл спрацьовує лише для власного поста
final class AuthorRule extends yii\rbac\Rule
{
public $name = 'isAuthor';
public function execute($user, $item, $params): bool
{
return isset($params['post']) && $params['post']->author_id == $user;
}
}
// Консольна команда: будуємо ієрархію один раз
$auth = Yii::$app->authManager;
$updatePost = $auth->createPermission('updatePost');
$auth->add($updatePost);
$rule = new AuthorRule;
$auth->add($rule);
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);
$auth->addChild($updateOwnPost, $updatePost); // updateOwnPost => updatePost за умови правила
$author = $auth->createRole('author');
$auth->add($author);
$auth->addChild($author, $updateOwnPost);
$admin = $auth->createRole('admin');
$auth->add($admin);
$auth->addChild($admin, $updatePost); // admin редагує будь-який пост
$auth->addChild($admin, $author);
$auth->assign($author, 15);
// У контролері: перевіряємо дозвіл, а не роль
if (! Yii::$app->user->can('updatePost', ['post' => $post])) {
throw new ForbiddenHttpException('Not your post.');
}
Що хоче почути інтервʼюер
Структуру: permission це атомарна дія, role групує permissions або інші ролі, і разом вони утворюють орієнтований граф, де can() шукає шлях від призначень користувача до потрібного дозволу.
Що rule це клас із execute(), який отримує користувача, елемент і параметри й повертає bool, тому дозвіл updateOwnPost з правилом AuthorRule перевіряє, що пост належить користувачу.
Різницю сховищ: DbManager для продакшену з кешем, PhpManager для невеликих застосунків без динамічних призначень, і що обидва реалізують один інтерфейс.
Що перевірка викликається в контролерах через AccessControl-фільтр або явний can(), а призначення ролей робиться в консольній команді або адмінці.
Уміння порівняти з Voters у Symfony, Gates і Policies у Laravel, і розуміння, як перенести правила при міграції.
Типові помилки
Плутати RBAC з AccessControl-фільтром по ролях '@' і '?': фільтр лише вирішує, кого пускати в екшн, а RBAC описує ієрархію дозволів.
Перевіряти ролі замість дозволів: can('admin') замість can('updatePost'), через що зміна ролей вимагає правки коду.
Забувати про кеш DbManager і робити десятки запитів до auth-таблиць на кожен запит.
Реалізовувати авторство через if ($post->author_id === Yii::$app->user->id) у кожному екшні замість правила.
Не розуміти, що rule виконується для кожного елемента на шляху, і важкі запити в execute() виконуються багато разів.
ПОРАДА
Порівняйте з Voters у Symfony або Policies у Laravel: питання про перенесення логіки прав при міграції задають часто. І скажіть, що в коді перевіряєте дозволи, а не ролі.
Додаткові питанняЗ ВІДПОВІДЯМИ
Permission це статичний вузол графа: назва дії, наприклад updatePost. Rule це код, привʼязаний до permission або role, який виконується під час can() і додає умову з контексту: чи належить пост користувачу, чи не минув дедлайн. Rule не є дозволом сам по собі, він лише дозволяє або забороняє проходження через вузол, до якого прикріплений.
DbManager зберігає елементи, ієрархію й призначення в чотирьох таблицях і потрібен, коли ролі призначаються динамічно з адмінки або їх багато. PhpManager тримає все у файлах, що зручно для малих застосунків з фіксованими ролями і дозволяє версіонувати права в git, але призначення теж у файлі, тому для тисяч користувачів не підходить. У продакшені DbManager обовʼязково з увімкненим cache.
Дозволи з правилами стають Policies у Laravel або Voters у Symfony: updateOwnPost з AuthorRule перетворюється на PostPolicy::update($user, $post). Статичну ієрархію ролей і дозволів або залишають у таблицях і перевіряють через Gate::define, або беруть spatie/laravel-permission. Перевірки can('updatePost', $post) у коді переносяться майже один в один, саме тому важливо в Yii перевіряти дозволи, а не ролі.
У контролері через behaviors() з AccessControl і правилом roles => ['updatePost'], яке під капотом викликає can(), або явно Yii::$app->user->can('updatePost', ['post' => $post]) з викиданням ForbiddenHttpException. У шаблоні той самий can() ховає кнопки, але це лише зручність: справжня перевірка завжди в контролері або сервісі.