RBAC - это просто! В этом посте я постараюсь понятно объяснить как настроить RBAC в Yii2. Напомню, RBAC - Role Based Access Control, что означает - Контроль доступа на основе ролоей. Программисты, которые не разобрались с RBAC, «впихивают» в модель юзера константы, типа ROLE_ADMIN, ROLE_USER, ROLE_MANAGER. В таблице юзера хранят роль… Потом в коде сравнивают эту самую роль… Я и сам писал подобный шлак, пока в один прекрасный момент не разобрался с примитивныи использованием RBAC.

Без моих пояснений понятно, что каждому пользователю назначается роль (role)/много ролей/разрешения (permissions)/правила (rules)… Роль может включать какие то разрешения. Она может наследовать разрешения от другой роли или нескольких ролей и т.д. Уверен, вы это уже миллион раз перечитали, пережевали… И всё равно вам непонятно, как же это работает, и как всё таки заставить в своем проекте работать этот «непонятный» RBAC.

Почему так всё запутано?

Да потому что, «умники» пишут ствои статьи, полагая, что вы уже и без них всё знаете! Авторы кидают копипасты, воруя тексты друг у друга. И никто из них не пытается объяснить сути работы RBAC. Одни пишут о каких то файлах, которых хранят все данные. Другие пишут, что можно хранить конфигурацию в БД. При этом, всё равно, нужны какие то файлы, которые нужно запускать в консоли. У одних файлы лежат в одном месте, у других - в другом. Ну да ладно, поехали дальше…

Так как работает RBAC в Yii2?

Я глубоко не изучал, как работает RBAC в Yii1, потому что использовал модули yii-user и rights. Но уверен, что в Yii2 изменилось немного.

Суть работы RBAC следующая: вы создаете экшены. Каждый экшн по сути - это какая то операция. Нужно проверить, имеет ли право текущий пользователь выполнять эту операцию. Пользователь имеет право её выполнять, если ему присвоено разрешение непосредственно для этой операции, либо роли пользователя присвоено такое разрешение. Так же возможно, что роль пользователя наследует другую роль, а той, в свою очередь, присвоено разрешение на проверяемую операцию. Глубина наследований практически не имеет границ.

Определиться с тем, какие роли будут в вашем проекте вы должны еще на стадии проектирования. В простом исполнении для каждого экшена нужно будет создавать именованное разрешение. Имя разрешения должно раскрывать его суть. По имени разрешения вы должны понять, в каком экшене оно используется. В коде экшенов вы вызываете проверку разрешений с помощью Yii::$app->user->can('имя_разрешения_или_роли').

Разрешения, роли, назначения можно хранить в базе данных, а можно и в файлах. Ниже я буду описывать вариант хранения данных в БД.

Ближе к коду…

Базовые шаги, которые необходимо пройти:

  • Конфигурируем RBAC так, что бы данные хранились в БД. Приводим часть конфига (например, main), к следующему виду:
<?php
...
return [
    // ...
    'components' => [
        'authManager' => [
            'class' => 'yii\rbac\DbManager',
        ],
        // ...
    ],
];
  • Теперь в БД нужно создать необходимые таблицы, в которых будут храниться разрешения, роли, назначения… Для этого можно выполнить миграции, которые лежат в «коробке». В консоли выполняем:

php yii migrate --migrationPath=@yii/rbac/migrations

Это создаст 4 таблицы в БД:

auth_item - эта таблица хранит роли и разрешения.

auth_item_child - здесь задаются наследования.

auth_assignment - тут пользователям назначаются позиции из auth_item. Т.е. пользователю (по user ID) можем назначить роль/разрешение (по названиям).

auth_rule - здесь хранятся имена классов правил, которые хранятся в php файлах.

Сейчас эти таблицы пустые.

  • Создаем пользователя, если до сих пор его не было. Например, у нас будет пользователь с ID 1 - это админ. С ID 2 - редактор новостей.

  • Теперь необходмо создать роли. Думаю будет достаточно двух ролей, например роль админа и роль редактора новостей. Так же необходимо создать разрешения, а затем назначить их ролям, а роли - пользователям. Можно это сделать руками в БД, а можно написать консольный скрипт, который всё добавит и назначит. Скрипт нужно выполнить один раз всего лишь для инициализации нашей задумки.

Сделаем первоначальную инициализацию с помощью консольного скрипта: в корне проекта по следущей вложенности создаем файл console/controllers/RbacController.php

<?php

namespace console\controllers;

use Yii;
use yii\console\Controller;
/**
 * Инициализатор RBAC выполняется в консоли php yii rbac/init
 */
class RbacController extends Controller {

    public function actionInit() {
        $auth = Yii::$app->authManager;
        
        $auth->removeAll(); //На всякий случай удаляем старые данные из БД...
        
        // Создадим роли админа и редактора новостей
        $admin = $auth->createRole('admin');
        $editor = $auth->createRole('editor');
        
        // запишем их в БД
        $auth->add($admin);
        $auth->add($editor);
        
        // Создаем разрешения. Например, просмотр админки viewAdminPage и редактирование новости updateNews
        $viewAdminPage = $auth->createPermission('viewAdminPage');
        $viewAdminPage->description = 'Просмотр админки';
        
        $updateNews = $auth->createPermission('updateNews');
        $updateNews->description = 'Редактирование новости';
        
        // Запишем эти разрешения в БД
        $auth->add($viewAdminPage);
        $auth->add($updateNews);
        
        // Теперь добавим наследования. Для роли editor мы добавим разрешение updateNews,
        // а для админа добавим наследование от роли editor и еще добавим собственное разрешение viewAdminPage
        
        // Роли «Редактор новостей» присваиваем разрешение «Редактирование новости»
        $auth->addChild($editor,$updateNews);

        // админ наследует роль редактора новостей. Он же админ, должен уметь всё! :D
        $auth->addChild($admin, $editor);
        
        // Еще админ имеет собственное разрешение - «Просмотр админки»
        $auth->addChild($admin, $viewAdminPage);

        // Назначаем роль admin пользователю с ID 1
        $auth->assign($admin, 1); 
        
        // Назначаем роль editor пользователю с ID 2
        $auth->assign($editor, 2);
    }
}

Теперь выполним этот скрипт

php yii rbac/init

Если выполнилось без ошибок, то в таблицах БД вы увидите результат работы. Он просто добавил туда записи. По мере добавления новых экшенов, возможно вам придется создавать для них новые разрешения и назначать ролям или пользователям.

Если не критично потерять назначения имеющихся пользователей, то можно обновлять код RbacController и запускать его, что бы он пересоздал записи. Возможно, если убрать $auth->removeAll() из RbacController, то новые роли/разрешения просто добавятся без потери старых данных. Лично я не проверял.

Вы можете вручную вносить новые записи ролей/разрешений через ваш менеджер баз данных. Так же можно установить расширение, которое для этих четырех таблиц реализует визуальный интерфейс. Оно называется yii2-rbac-plus. Правда в composer файле этого расширения не указано, что оно требует виджеты от kartik-v и их придется установить заранее.

  • Теперь, для того что бы проверить, имеет ли пользователь какое то разрешение, мы в нужном месте кода (хоть в экшене, хоть в фильтре доступа в поведениях) можем запросить проверку так:
<?php
...
if (!\Yii::$app->user->can('updateNews')) {
    throw new ForbiddenHttpException('Access denied');
}
...

Дословно это значит, что если пользователь НЕ может updateNews, то выдается ошибка. При этом система проверит, какие роли или разрешения назначены в таблице auth_assignment текущему пользователю. Сначала проверит присвоенные разрешения (вы же помните, что пользователю можно присвоить не только роль но и разрешение). Если разрешение updateNews не будет найдено, то RBAC пройдет по каждой назначенной роли и проверит разрешения для ролей. Если хоть одной роли будет назначено разрешение updateNews, то Yii::$app->user->can('updateNews') вернет true;

Как быть с новыми пользователями, которые добавляются в систему динамически?

Если у вас в проекте присутствует автоматическая регистрация пользователей или админ вносит юзеров через админку, то в модели юзера, после сохранения новой записи можно выполнить назначение новому юзера роли:

<?php
...

// Назначаем роль в методе afterSave модели User
$auth = Yii::$app->authManager;
$editor = $auth->getRole('editor'); // Получаем роль editor
$auth->assign($editor, $this->id); // Назначаем пользователю, которому принадлежит модель User

...        

Вот в принципе и вся суть. Ниже я распишу, как создать правила с параметрами, например для того, что бы проверить, является ли пользователь автором новости или нет.

Правила / RBAC Rules

RBAC дает возможность очень гибко работать с разрешениями и ролями с помощью правил. Правила добавляют ролям и разрешениям дополнительные ограничения. В RBAC Yii1 эти правила хранились непосредственно в разрешениях и назывались Business rules.

В Yii2 правила являются классами (php файлами), которые наследуются от yii\rbac\Rule и должны содержать в себе единственный метод execute().

Создадим правило, которое позволяет проверять, является ли пользователь автором новости. Файл common\rbac\AuthorRule.php. Путь может быть любой. Мне удобно хранить правила в common\rbac.

<?php
namespace common\rbac;

class AuthorRule extends yii\rbac\Rule
{
    public $name = 'isAuthor';

    /**
     * @param string|integer $user ID пользователя.
     * @param Item $item роль или разрешение с которым это правило ассоциировано
     * @param array $params параметры, переданные в ManagerInterface::checkAccess(), например при вызове проверки
     * @return boolean a value indicating whether the rule permits the role or permission it is associated with.
     */
    public function execute($user, $item, $params)
    {
        return isset($params['news']) ? $params['news']->createdBy == $user : false;
    }
}

Таким образом, мы проверяем, что поле createdBy у новости совпадает или нет с user id. Файл правила мы создали. Теперь его нужно добавть в RBAC. Для этого мы модернизируем наш инициализатор RbacController и выполним его еще раз. Старые данные сотрем:

<?php

namespace console\controllers;

use Yii;
use yii\console\Controller;
/**
 * Инициализатор RBAC выполняется в консоли php yii rbac/init
 */
class RbacController extends Controller {

    public function actionInit() {
        $auth = Yii::$app->authManager;
        
        $auth->removeAll(); //На всякий случай удаляем старые данные из БД...
        
        // Создадим роли админа и редактора новостей
        $admin = $auth->createRole('admin');
        $editor = $auth->createRole('editor');
        
        // запишем их в БД
        $auth->add($admin);
        $auth->add($editor);
        
        // Создаем наше правило, которое позволит проверить автора новости
        $authorRule = new \app\rbac\AuthorRule;
        
        // Запишем его в БД
        $auth->add($authorRule);
        
        // Создаем разрешения. Например, просмотр админки viewAdminPage и редактирование новости updateNews
        $viewAdminPage = $auth->createPermission('viewAdminPage');
        $viewAdminPage->description = 'Просмотр админки';
        
        $updateNews = $auth->createPermission('updateNews');
        $updateNews->description = 'Редактирование новости';
        
        // Создадим еще новое разрешение «Редактирование собственной новости» и ассоциируем его с правилом AuthorRule
        $updateOwnNews = $auth->createPermission('updateOwnNews');
        $updateOwnNews->description = 'Редактирование собственной новости';
        
        // Указываем правило AuthorRule для разрешения updateOwnNews.
        $updateOwnNews->ruleName = $authorRule->name;
        
        // Запишем все разрешения в БД
        $auth->add($viewAdminPage);
        $auth->add($updateNews);
        $auth->add($updateOwnNews);
        
        // Теперь добавим наследования. Для роли editor мы добавим разрешение updateOwnNews (редактировать собственную новость),
        // а для админа добавим собственные разрешения viewAdminPage и updateNews (может смотреть админку и редактировать любую новость)
        
        // Роли «Редактор новостей» присваиваем разрешение «Редактирование собственной новости»
        $auth->addChild($editor,$updateOwnNews);

        // админ имеет собственное разрешение - «Редактирование новости»
        $auth->addChild($admin, $updateNews);
        
        // Еще админ имеет собственное разрешение - «Просмотр админки»
        $auth->addChild($admin, $viewAdminPage);

        // Назначаем роль admin пользователю с ID 1
        $auth->assign($admin, 1); 
        
        // Назначаем роль editor пользователю с ID 2
        $auth->assign($editor, 2);
    }
}

Теперь, что бы вызвать проверку прав на редактирование собственной новости, в экшене редактирования производим проверку:

<?php
...
if (!\Yii::$app->user->can('updateOwnNews', ['news' => $newsModel])) {
    throw new ForbiddenHttpException('Access denied');
}
...

Здесь мы вызываем проверку updateOwnNews и передаем в правило этого разрешения параметр news (модель новости) в виде ассоциативного массива.

Использование проверки в фильтре доступа AccessControl

Использовать проверку в фильтре доступа AccessControl выгодно по причине того, что, если пользователь не авторизован и не имеет разрешения, то yii перекинет его на страницу авторизации. А если пользователь был авторизован и не имеет разрешения, то получает страницу ошибки. Пример ниже проверяет разрешение viewAdminModule для всех экшенов контроллера:

<?php
public function behaviors() {
        return [
            'access' => [
                'class' => \yii\filters\AccessControl::className(),
                'rules' => [
                    [
                        'allow' => true,
                        'roles' => ['viewAdminModule']
                    ],
                ],
            ],
        ];
    }

Здесь параметру roles передается массив ролей или разрешений, что в свою очередь в недрах системы вызывает проверку Yii::$app->user->can(‘viewAdminModule’)).

Кстати, с помощью Yii::$app->user->can() мы можем проверять не только наличие разрешения у роли, но и наличие роли. Yii::$app->user->can('editor')) вернет true, если текущему пользователю назначена роль editor. Получить массив ролей мы можем так: Yii::$app->authManager->getRolesByUser(Yii::$app->user->getId()).

Имейте в виду, что если роль админа наследует роль редактора новостей, то для админа Yii::$app->user->can('editor')) вернет true.