Ми}{@лbI4

Блог хеллоуворлдщика

RBAC или роли доступа в Yii2

23.10.2015 yii2, советы, rbac

Много разных вариантов реализации ролей доступа мной было испробовано, пока я не нашел идеально подходящий для меня.

Первое, что мне не нравится в RBAC реализованный в Yii2, это возможность использовать несколько ролей. На самом деле, при правильно реализованной иерархической структуре достаточно одной роли.

Второе, что мне не нравится, это хранение назначений отдельно, т.е. связывание роли с пользователем. И это в коробке вообще никак не отключается. Если нужно действительно это отключить, чтобы случайно другой программист не заюзал - нужно переопределять менаджер авторизации и вешать всякие throw и прочее. Роль, имхо, должна указываться в таблице с пользователем, потому что: если нужно делать дамп, то если назначения хранятся в ФС - придется делать дамп и связей; а если назначения хранятся в БД (а хранить их в БД нет никакого смысла вообще), то такое тяжело поддерживать, если вдруг структура ролей будет изменена; и потом, если хранить назначения с ролями отдельно, то у людей получается жуткий говнокод, если нужно иметь возможность "видеть" роль пользователя и менять её динамически. Поэтому, делать нужно правильно изначально: все роли, правила, иерархию наследования храним в ФС, а связываем все это дело, через поле role в таблице с пользователями.

С реализацией все очень просто.

Добавление поля

Во-первых, нужно добавить поле role в таблицу с пользователями. Лучше всего, чтобы role имела тип VARCHAR(64) NOT NULL, но дело ваше. Это не принципиально по сути.

Объявляем роли в модели пользователей

const ROLE_SUPERUSER = 'superuser';
const ROLE_REGISTERED = 'registered';
const ROLE_GUEST = 'guest';
 
/**
 * Возвращает массив всех доступных ролей.
 * @return array
 */
static public function roleArray()
{
    return [
        self::ROLE_SUPERUSER,
        self::ROLE_REGISTERED,
        self::ROLE_GUEST,
    ];
}

Настраиваем AuthManager в конфигурации приложения

use app\models\User;
 
'authManager' => [
    'class' => 'yii\rbac\PhpManager',
    'itemFile' => '@app/rbac/items.php',
    'ruleFile' => '@app/rbac/rules.php',
    'assignmentFile' => '@app/rbac/assignments.php', // назначения придется указать, потому что того требуют каноны церкви
    'defaultRoles' => User::roleArray(),
],

В defaultRoles мы указали список всех доступных ролей. Это нужно для того, чтобы менеджер авторизации знал о их существовании. А чтобы он не присваивал их кому попало, нужно реализовать правило на проверку принадлежности к роли у пользователя, который такое поведение будет исключать.

Добавляем правило на проверку принадлежности к роли у пользователя

Теперь нужно создать класс UserRoleRule в app/rbac, правило которого будет проверять принадлежность пользователя к роли.

use Yii;
use yii\rbac\Rule;
use app\models\User
 
class UserRoleRule extends Rule
{
    /**
     * @inheritdoc
     */
    public $name = 'userRole';
 
    private $_assignments = [];
 
    /**
     * @inheritdoc
     */
    public function execute($user, $item, $params)
    {
        if ($role = $this->userRole($user)) {
            switch ($item->name) {
                case User::ROLE_SUPERUSER:
                    return $role == User::ROLE_SUPERUSER;
 
                case User::ROLE_REGISTERED:
                    return $role == User::ROLE_SUPERUSER || $role == User::ROLE_REGISTERED;

                case User::ROLE_GUEST:
                    return in_array($role, [User::ROLE_SUPERUSER, User::ROLE_REGISTERED, User::ROLE_GUEST]);
            }
        }
        return false;
    }
 
    /**
     * @param integer|null $userId ID of user.
     * @return string|false
     */
    protected function userRole($userId)
    {
        $user = Yii::$app->user;      
        if ($userId === null) {
            if ($user->isGuest) {
                return Users::ROLE_GUEST;
            }
            return false;
        }
        if (!isset($this->_assignments[$userId])) {
            $role = false;
            if (!$user->isGuest && $user->id == $userId) {
                $role = $user->role;
            } elseif ($user->isGuest || $user->id != $userId) {
                $role = User::getRoleOfUser($userId);
            }     
            $this->_assignments[$userId] = $role;
        }     
        return $this->_assignments[$userId];
    }
}

Метод userRole реализует получение роли пользователя даже в случае, если вы проверяете не только текущего пользователя, но и какого-то другого. Этот метод обращается к User::getRoleOfUser() для получения роли пользователя. Реализация данного метода примерно такая:

/**
 * Возвращает роль пользователя по его ID в случае успеха и `false`  в случае неудачи.
 * @param integer $id ID пользователя.
 * @return string|false
 */
static public function getRoleOfUser($id)
{
    return (new Query)
        ->select('role')
        ->from(self::tableName())
        ->where(['id' => $id])
        ->scalar();
}

Также, компонент user в методе userRole обращается к свойству role, которое реализовано через геттер в компоненте yii\web\User. Вам нужно либо добавить данный метод в уже унаследованный у вас класс yii\web\User, либо унаследоваться и потом добавить, либо добавить в UserRoleRule, чего я не рекомендую. Реализация метода примерно такая:

/**
 * Возвращает роль пользователя или `null`.
 * @return string|null
 */
public function getRole()
{
    $identity = $this->getIdentity();
    return $identity !== null ? $identity->role : null;
}

Инициализируем RBAC

Т.к. у нас все уже подготовлено, то можно приступить к добавлению ролей, правил и т.д.

В консольном контроллере создаем действие actionInitRbac:

use Yii;
use app\rbac\UserRoleRule;
use app\models\User;
 
public function actionInitRbac()
{
    $auth = Yii::$app->getAuthManager();
    $auth->removeAll();
 
    $userRoleRule = new UserRoleRule;
    $auth->add($userRoleRule);
 
    $superuser = $auth->createRole(User::ROLE_SUPERUSER);
    $superuser->ruleName = $userRoleRule->name;
    $auth->add($superuser);
    $registered = $auth->createRole(User::ROLE_REGISTERED);
    $registered->ruleName = $userRoleRule->name;
    $auth->add($registered);
    $guest = $auth->createRole(User::ROLE_GUEST);
    $guest->ruleName = $userRoleRule->name;
    $auth->add($guest);
 
    $auth->addChild($registered, $guest);
    $auth->addChild($superuser, $registered);  
}

Иерархия ролей получилась следующей:

- superuser
- | - registered
- - | - guest

Инициализируем

./yii controller-name/init-rbac

Вот и все. Теперь, достаточно указывать в поле role таблицы пользователя роль пользователя. Проверка прав доступа не изменилась.

Данный метод работает прекрасно без нареканий. Я им полностью доволен.


UPD 26.11.2015 16:26: Добавлена роль гость для общей картины.