Настройка Yii2 RBAC

  • Tutorial

Задача


Настроить использование RBAC в Yii2.

Условия


Список возможных ролей:
  • guest — не авторизованный юзер;
  • BRAND — авторизованный юзер, наследует разрешения роли guest и имеет свои уникальные разрешения;
  • TALENT — авторизованный юзер, наследует разрешения роли guest и имеет свои уникальные разрешения;
  • admin — авторизованный юзер, наследует разрешения ролей guest, BRAND и TALENT и имеет свои уникальные разрешения.
  • Роль определяется полем group в модели UserExt;
  • Роли имеют вложенную структуру — одна роль может наследовать разрешения другой;
  • Используется yii\rbac\PhpManager;
  • Не использовать назначение роли юзеру по его ID — вместо этого использовать несколько предустановленных ролей (defaultRoles);
  • Генерирование конфига «роль-разрешения» будет делать консольная команда yii;
  • Будут использованы расширенные правила (Rules) для разрешений.


Предварительная настройка


app/config/console.php
'components' => [
    // ...
    'authManager' => [
        'class' => 'yii\rbac\PhpManager',
    ],
    // ...
],


app/config/web.php
'components' => [
    // ...
    'authManager' => [
        'class' => 'yii\rbac\PhpManager',
        'defaultRoles' => ['admin', 'BRAND', 'TALENT'], // Здесь нет роли "guest", т.к. эта роль виртуальная и не присутствует в модели UserExt
    ],
    // ...
],


Создать директорию @app/rbac — именно в ней будут находиться разрешения и правила.

Создание разрешений


В директории @app/commands создать контроллер, который будет генерировать массив разрешений:
app/commands/RbacController.php
<?php
namespace app\commands;
 
use Yii;
use yii\console\Controller;
use \app\rbac\UserGroupRule;
 
class RbacController extends Controller
{
    public function actionInit()
    {
        $authManager = \Yii::$app->authManager;
 
        // Create roles
        $guest  = $authManager->createRole('guest');
        $brand  = $authManager->createRole('BRAND');
        $talent = $authManager->createRole('TALENT');
        $admin  = $authManager->createRole('admin');
 
        // Create simple, based on action{$NAME} permissions
        $login  = $authManager->createPermission('login');
        $logout = $authManager->createPermission('logout');
        $error  = $authManager->createPermission('error');
        $signUp = $authManager->createPermission('sign-up');
        $index  = $authManager->createPermission('index');
        $view   = $authManager->createPermission('view');
        $update = $authManager->createPermission('update');
        $delete = $authManager->createPermission('delete');
 
        // Add permissions in Yii::$app->authManager
        $authManager->add($login);
        $authManager->add($logout);
        $authManager->add($error);
        $authManager->add($signUp);
        $authManager->add($index);
        $authManager->add($view);
        $authManager->add($update);
        $authManager->add($delete);
 
 
        // Add rule, based on UserExt->group === $user->group
        $userGroupRule = new UserGroupRule();
        $authManager->add($userGroupRule);
 
        // Add rule "UserGroupRule" in roles
        $guest->ruleName  = $userGroupRule->name;
        $brand->ruleName  = $userGroupRule->name;
        $talent->ruleName = $userGroupRule->name;
        $admin->ruleName  = $userGroupRule->name;
 
        // Add roles in Yii::$app->authManager
        $authManager->add($guest);
        $authManager->add($brand);
        $authManager->add($talent);
        $authManager->add($admin);
 
        // Add permission-per-role in Yii::$app->authManager
        // Guest
        $authManager->addChild($guest, $login);
        $authManager->addChild($guest, $logout);
        $authManager->addChild($guest, $error);
        $authManager->addChild($guest, $signUp);
        $authManager->addChild($guest, $index);
        $authManager->addChild($guest, $view);
 
        // BRAND
        $authManager->addChild($brand, $update);
        $authManager->addChild($brand, $guest);
 
        // TALENT
        $authManager->addChild($talent, $update);
        $authManager->addChild($talent, $guest);
 
        // Admin
        $authManager->addChild($admin, $delete);
        $authManager->addChild($admin, $talent);
        $authManager->addChild($admin, $brand);
    }
}


Класс UserGroupRule отвечает за проверку равенства роли текущего юзера, роли прописанной в массиве разрешений. Этим мы избегаем проблемы с назначением роли юзеру по его ID.
app/rbac/UserGroupRule.php
<?php
namespace app\rbac;
 
use Yii;
use yii\rbac\Rule;
 
class UserGroupRule extends Rule
{
    public $name = 'userGroup';
 
    public function execute($user, $item, $params)
    {
        if (!\Yii::$app->user->isGuest) {
            $group = \Yii::$app->user->identity->group;
            if ($item->name === 'admin') {
                return $group == 'admin';
            } elseif ($item->name === 'BRAND') {
                return $group == 'admin' || $group == 'BRAND';
            } elseif ($item->name === 'TALENT') {
                return $group == 'admin' || $group == 'TALENT';
            }
        }
        return true;
    }
}


Теперь в контроллере из метода behaviors можно убрать правило access:
app/controllers/SiteController.php
public function behaviors()
{
    return [
        
        // ...
        'access' => [
            'class' => AccessControl::className(),
            'only' => ['logout'],
            'rules' => [
                [
                    'actions' => ['logout'],
                    'allow' => true,
                    'roles' => ['@'],
                ],
            ],
        ],
        // ...
         
    ];
}


Проверка доступа


Способ 1 — в методе контроллера:
app/controllers/SiteController.php
public function actionAbout()
{
    if (!\Yii::$app->user->can('about')) {
        throw new ForbiddenHttpException('Access denied');
    }
    return $this->render('about');
}


Способ 2 — прописать beforeAction, чтобы не писать "if !\Yii::$app->user->can" в каждом методе:
app/controllers/SiteController.php
public function beforeAction($action)
{
    if (parent::beforeAction($action)) {
        if (!\Yii::$app->user->can($action->id)) {
            throw new ForbiddenHttpException('Access denied');
        }
        return true;
    } else {
        return false;
    }
}


Генерирование файлов разрешений


Чтобы сгенерировать файл с массивом разрешений нужно в корне проекта выполнить команду:
Внимание!
Перед выполнением этой команды нужно удалить файлы @app/rbac/items.php и @app/rbac/rules.php чтобы избежать конфликтов слияния

./yii rbac/init


В директории @app/rbac должны появиться два файла:
app/rbac/items.php
<?php
return [
    'login' => [
        'type' => 2,
    ],
    'logout' => [
        'type' => 2,
    ],
    'error' => [
        'type' => 2,
    ],
    'sign-up' => [
        'type' => 2,
    ],
    'index' => [
        'type' => 2,
    ],
    'view' => [
        'type' => 2,
    ],
    'update' => [
        'type' => 2,
    ],
    'delete' => [
        'type' => 2,
    ],
    'guest' => [
        'type' => 1,
        'ruleName' => 'userGroup',
        'children' => [
            'login',
            'logout',
            'error',
            'sign-up',
            'index',
            'view',
        ],
    ],
    'BRAND' => [
        'type' => 1,
        'ruleName' => 'userGroup',
        'children' => [
            'update',
            'guest',
        ],
    ],
    'TALENT' => [
        'type' => 1,
        'ruleName' => 'userGroup',
        'children' => [
            'update',
            'guest',
        ],
    ],
    'admin' => [
        'type' => 1,
        'children' => [
            'delete',
            'TALENT',
            'BRAND',
        ],
    ],
];


app/rbac/rules.php
<?php
return [
    'userGroup' => 'O:22:"app\\rbac\\UserGroupRule":3:{s:4:"name";s:9:"userGroup";s:9:"createdAt";N;s:9:"updatedAt";N;}',
];


Расширенное правило (Rule) для разрешений


Например, нужно запретить юзерам редактировать (update) не свой профиль. Для этого нужно расширенное правило:
app/rbac/UserProfileOwnerRule.php
<?php
namespace app\rbac;
 
use yii\rbac\Rule;
use yii\rbac\Item;
 
class UserProfileOwnerRule extends Rule
{
    public $name = 'isProfileOwner';
 
    /**
     * @param string|integer $user   the user ID.
     * @param Item           $item   the role or permission that this rule is associated with
     * @param array          $params parameters passed to 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)
    {
        if (\Yii::$app->user->identity->group == 'admin') {
            return true;
        }
        return isset($params['profileId']) ? \Yii::$app->user->id == $params['profileId'] : false;
    }
}


В файл @app/rbac/RbacController.php добавить:
app/rbac/RbacController.php
use \app\rbac\UserProfileOwnerRule;
 
// add the rule
$userProfileOwnerRule = new UserProfileOwnerRule();
$authManager->add($userProfileOwnerRule);
 
$updateOwnProfile = $authManager->createPermission('updateOwnProfile');
$updateOwnProfile->ruleName = $userProfileOwnerRule->name;
$authManager->add($updateOwnProfile);
 
$authManager->addChild($brand, $updateOwnProfile);
$authManager->addChild($talent, $updateOwnProfile);


Проверка доступа в методе контроллера:
app/controllers/UsersController.php
public function actionUpdate($id)
{
    if (!\Yii::$app->user->can('updateOwnProfile', ['profileId' => \Yii::$app->user->id])) {
        throw new ForbiddenHttpException('Access denied');
    }
    // ...
} 
Поделиться публикацией

Комментарии 18

    0
    Способ 2 — прописать beforeAction, чтобы не писать «if !\Yii::$app->user->can» в каждом методе:

    Удобнее всего реализовать это в фильтре по аналогии с AccessControl.
      0
      Хранение в базе поля group излишне если вот это не критично:

      Не использовать назначение роли юзеру по его ID — вместо этого использовать несколько предустановленных ролей (defaultRoles);


      Роли можно построить так, чтобы роли верхнего уровня и были по сути группами.
        0
        Но ведь в этом случае придется добавлять роль каждому пользователю. Это удобно, если пользователь должен иметь несколько ролей из иерархии ролей, но в случае с определенным первичным набором ролей удобнее хранить единственную роль пользователя в таблице пользователей.
          +1
          Вы делаете ровно то же, но при помощи поля в базе данных. Прописываете нечто в `group`. Можно в `defaultRoles` оставить `user` с базовым набором прав, а повышение привилегий делать через API RBAC.
            0
            Согласен с Александром. Нет ничего страшного в добавлении роли каждому пользователю (помимо использования defaultRoles можно просто добавлять роль guest автоматически при регистрации каждому пользователю).
            +1
            Александр, а RBAC можно использовать внутри модулей?

            Опишу подробнее.
            Есть ядро сайта со своим набором RBAC. Пусть там будут какие-то стандартные роли guest, user, admin. При помощи их он видит (например) только главную страницу.

            Теперь пишем модуль news.
            И внутри него предполагается свои роли (readerNews, writerNews, moderatorNews). Т.е. они должны «налету» через бизнес-правила присвоиться юзеру, если он попал в раздел новостей.
            Если есть модуль blog — то там другие роли. Тоже «налету». Они не зависят друг от друга и никогда не пересекаются.
            Так вот, можно ли «дополнять» роли? Можно ли их хранить в разных местах (внутри модуля). Или нужно все хранить в одном месте?

            Просто судя по коду PhpManager там предполагается хранение только в одном месте.
              +2
              Хранить в одном месте, но иерархию можно построить так, чтобы она не пересекалась с основной. Например, префиксируя всё именем модуля.
                0
                Ок, спасибо!
            +1
            Теперь в контроллере из метода behaviors можно убрать правило access

            А зачем его убирать? Попробуйте воспользоваться matchCallback, внутри которого проверять permission. Либо сделайте по одному правилу на каждый action, указывая в roles название соответствующего permission (фишка в том, что yii\filters\AccessRule проверяет roles посредством того же can).
              0
              Тоже верно.
                0
                Да, достаточно удобно использовать Rbac в поведениях. Только в выше описанный метод нужно внести поправку:
                class UserGroupRule extends Rule
                {
                    public function execute($user, $item, $params)
                    {
                        if (!\Yii::$app->user->isGuest) {
                
                        }
                        return false;
                    }
                }
                

                И после использовать в behaviors -> AccessControl:
                'roles' => ['admin']
                

                Не боясь что в этот же раздел попадет виртуальный Guest ;)
                +1
                Внимание!
                Перед выполнением этой команды нужно удалить файлы app/rbac/items.php и app/rbac/rules.php чтобы избежать конфликтов слияния

                Можно в методе
                RbacController::actionInit
                после строчки
                $authManager = \Yii::$app->authManager;
                добавить
                $authManager->removeAll(); 
                Это удалит все права, тогда никаких конфликтов не возникнет.
                  0
                  Спасибо тебе, добрый человек!
                  0
                  Если я хочу настраивать привилегии юзергруппы через интерфейс, как быть тогда? Идея в том, чтобы через веб интерфейс можно было создавать юзергруппы и для каждой группы через тот же веб-интефейс настраивать привилегии.
                  0
                  Каким образом ограничить доступ к некоторым методам? Допустим, у контроллера CommentsController к actionDelete() доступ дать только администратору?
                  $deleteComments= $authManager->createPermission('deleteComments');
                  $authManager->add($deleteComments);
                  $authManager->addChild($admin, $deleteComments);
                  А дальше в методе выполнять проверку:
                  if (!\Yii::$app->user->can('deleteComments')) {
                  throw new ForbiddenHttpException('Access denied');
                  }
                  Или есть более лаконичное/верное решение?
                    0
                    Посмотрите вот эту статью, здесь сильно проще всё описывается.
                      0
                      Спасибо! Очень помогло это решение.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое