Разные RBAC в одной БД

Всё про контроль доступа пользователей: фильтры, RBAC, проверки
Ответить
crows
Сообщения: 289
Зарегистрирован: 2012.03.03, 10:37

Разные RBAC в одной БД

Сообщение crows »

Здравствуйте.

Системой могут пользоваться разные фирмы (saas). Одна БД для всех.

Как сделать так, чтобы RBAC был отдельным для конкретной фирмы? Есть идея как-то добавить company_id в таблицы, создать какой-то RbacHelper. Но пока трудно представляю как переопределить стандартную логику...

:?: :idea:
crows
Сообщения: 289
Зарегистрирован: 2012.03.03, 10:37

Re: Разные RBAC в одной БД

Сообщение crows »

Простой пример:

В одной компании роль admin может удалять юзеров, а в другой - нет. Обе компании должны использовать одну БД.
Аватара пользователя
ElisDN
Сообщения: 5841
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Разные RBAC в одной БД

Сообщение ElisDN »

Добавьте company_id. Переопределите логику наследованием/пепеписыванием DbManager.
crows
Сообщения: 289
Зарегистрирован: 2012.03.03, 10:37

Re: Разные RBAC в одной БД

Сообщение crows »

ElisDN писал(а): 2017.08.03, 10:17 Добавьте company_id. Переопределите логику наследованием/пепеписыванием DbManager.
Спасибо.
crows
Сообщения: 289
Зарегистрирован: 2012.03.03, 10:37

Re: Разные RBAC в одной БД

Сообщение crows »

В общем что мне пришлось сделать:

1) Нaписать миграцию под новую структуру RBAC:

- добавить поле id;
- добавить поле company_id;
- убрать все существующие ключи (было много FK, PK по полям типа "name" и т. п.).

Код: Выделить всё

public function safeUp()
    {
        $this->execute("
            CREATE TABLE IF NOT EXISTS `auth_assignment` (
                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
              `item_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
              `user_id` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
              `created_at` int(11) DEFAULT NULL,
              `company_id` int(11) unsigned NOT NULL,
              PRIMARY KEY (`id`),
              KEY `company_id` (`company_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
        ");

        $this->execute("
            CREATE TABLE IF NOT EXISTS `auth_item` (
                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
              `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
              `type` smallint(6) NOT NULL,
              `description` text COLLATE utf8_unicode_ci,
              `rule_name` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
              `data` blob,
              `created_at` int(11) DEFAULT NULL,
              `updated_at` int(11) DEFAULT NULL,
              `company_id` int(11) unsigned NOT NULL,
              PRIMARY KEY (`id`),
              KEY `company_id` (`company_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
        ");

        $this->execute("
            CREATE TABLE IF NOT EXISTS `auth_item_child` (
                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
              `parent` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
              `child` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
              `company_id` int(11) unsigned NOT NULL,
              PRIMARY KEY (`id`),
              KEY `company_id` (`company_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
        ");

        $this->execute("
            CREATE TABLE IF NOT EXISTS `auth_rule` (
                `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
              `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
              `data` blob,
              `created_at` int(11) DEFAULT NULL,
              `updated_at` int(11) DEFAULT NULL,
              `company_id` int(11) unsigned NOT NULL,
              PRIMARY KEY (`id`),
              KEY `company_id` (`company_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
        ");

        return true;
    }
2) В /components добавить новый класс:

- переопределить в нём нужные нам методы;
- добавить поле companyId для инициализации ID компании, с которой работаем.

Код: Выделить всё

<?php

namespace app\components\rbac;

use Yii;
use yii\rbac\DbManager;
use yii\caching\Cache;
use yii\db\Connection;
use yii\db\Query;
use yii\db\Expression;
use yii\base\InvalidCallException;
use yii\base\InvalidParamException;
use yii\di\Instance;
use yii\rbac\Permission;
use yii\rbac\Role;
use yii\rbac\Item;
use yii\rbac\Rule;
use yii\rbac\Assignment;

/**
 * Class RbacComponent.
 * We need this class because each company can have own RBAC.
 * @package app\components\rbac
 */
class RbacComponent extends DbManager
{

    /**
     * @var integer
     */
    public $companyId;

    /**
     * @inheritdoc
     */
    public function addChild($parent, $child)
    {
        if (!$this->companyId) {
            throw new InvalidParamException("Please set the parameter 'companyId' for this class (RbacComponent).");
        }

        if ($parent->name === $child->name) {
            throw new InvalidParamException("Cannot add '{$parent->name}' as a child of itself.");
        }

        if ($parent instanceof Permission && $child instanceof Role) {
            throw new InvalidParamException('Cannot add a role as a child of a permission.');
        }

        if ($this->detectLoop($parent, $child)) {
            throw new InvalidCallException("Cannot add '{$child->name}' as a child of '{$parent->name}'. A loop has been detected.");
        }

        $this->db->createCommand()
            ->insert($this->itemChildTable, [
                'parent' => $parent->name,
                'child' => $child->name,
                'company_id' => $this->companyId,
            ])
            ->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * @inheritdoc
     */
    protected function addRule($rule)
    {
        if (!$this->companyId) {
            throw new InvalidParamException("Please set the parameter 'companyId' for this class (RbacComponent).");
        }

        $time = time();
        if ($rule->createdAt === null) {
            $rule->createdAt = $time;
        }
        if ($rule->updatedAt === null) {
            $rule->updatedAt = $time;
        }
        $this->db->createCommand()
            ->insert($this->ruleTable, [
                'name' => $rule->name,
                'data' => serialize($rule),
                'created_at' => $rule->createdAt,
                'updated_at' => $rule->updatedAt,
                'company_id' => $this->companyId,
            ])->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * @inheritdoc
     */
    public function assign($role, $userId)
    {
        if (!$this->companyId) {
            throw new InvalidParamException("Please set the parameter 'companyId' for this class (RbacComponent).");
        }

        $assignment = new Assignment([
            'userId' => $userId,
            'roleName' => $role->name,
            'createdAt' => time(),
        ]);

        $this->db->createCommand()
            ->insert($this->assignmentTable, [
                'user_id' => $assignment->userId,
                'item_name' => $assignment->roleName,
                'created_at' => $assignment->createdAt,
                'company_id' => $this->companyId,
            ])->execute();

        return $assignment;
    }

    /**
     * @inheritdoc
     */
    protected function addItem($item)
    {
        if (!$this->companyId) {
            throw new InvalidParamException("Please set the parameter 'companyId' for this class (RbacComponent).");
        }

        $time = time();
        if ($item->createdAt === null) {
            $item->createdAt = $time;
        }
        if ($item->updatedAt === null) {
            $item->updatedAt = $time;
        }
        $this->db->createCommand()
            ->insert($this->itemTable, [
                'name' => $item->name,
                'type' => $item->type,
                'description' => $item->description,
                'rule_name' => $item->ruleName,
                'data' => $item->data === null ? null : serialize($item->data),
                'created_at' => $item->createdAt,
                'updated_at' => $item->updatedAt,
                'company_id' => $this->companyId,
            ])->execute();

        $this->invalidateCache();

        return true;
    }

    /**
     * Init RBAC for a specific company.
     * @param integer $companyId
     */
    public static function initRbac($companyId)
    {
        $auth = Yii::$app->authManager;
        $auth->companyId = $companyId;

        ...

        $admin = $auth->createRole('admin');
        $auth->add($admin);
        
        $createUsers = $auth->createPermission('create_users');
        $viewUsers = $auth->createPermission('view_users');
        $editUsers = $auth->createPermission('edit_users');
        $deleteUsers = $auth->createPermission('delete_users');
        
        $auth->add($createUsers);
        $auth->add($viewUsers);
        $auth->add($editUsers);
        $auth->add($deleteUsers);
        
        $auth->addChild($admin, $createUsers);
        $auth->addChild($admin, $viewUsers);
        $auth->addChild($admin, $editUsers);
        $auth->addChild($admin, $deleteUsers);
    }

}
3) В модель, которая отвечает за создание новой компании, добавить вызов метода RbacComponent::initRbac, чтобы создать RBAC для нужной нам компании:

Код: Выделить всё

        // Init RBAC:
        RbacComponent::initRbac($id);
Теперь, чтобы работать с RBAC конкретной компании, нужно сделать следующее:

Код: Выделить всё

$auth = Yii::$app->authManager;
$auth->companyId = $companyId;

...

$admin = $auth->createRole('admin');
$auth->add($admin);
Аватара пользователя
maleks
Сообщения: 1985
Зарегистрирован: 2012.12.26, 12:56

Re: Разные RBAC в одной БД

Сообщение maleks »

Имхо вы перемудрили, так переделав дефолтную работу.
Я бы просто ввел для каждой компании свой набор прав вида
create_users_{company_id}
И в can при проверке просто в фоне этот суфикс динамически устанавливать
Yii2 universal module sceleton - for basic and advanced templates
crows
Сообщения: 289
Зарегистрирован: 2012.03.03, 10:37

Re: Разные RBAC в одной БД

Сообщение crows »

maleks писал(а): 2017.08.03, 16:43 Имхо вы перемудрили, так переделав дефолтную работу.
Я бы просто ввел для каждой компании свой набор прав вида
create_users_{company_id}
И в can при проверке просто в фоне этот суфикс динамически устанавливать
Тоже вариант, но уже поздно... :)
Ответить