Правильное использование RBAC

Всё про контроль доступа пользователей: фильтры, RBAC, проверки
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Правильное использование RBAC

Сообщение Brainfuck »

Я долго мучился с RBAC, писал кучу велосипедов, но все это выглядит "не очень". Вот смотрите по конкретному проекту: есть статья которую может менять только юзер ее создавший и редактор и возможно вебмастер, с кучей дополнительных подусловий. Кроме того одно и тоже правило я использую для ограничение доступа к компонентам статьи, таким как модели авторов, файлов, платежей и т.п.

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

class ArticleRule extends Rule
{
    public $name = 'updateArticle';

    /**
     * Executes the rule.
     *
     * @param string|int $user the user ID. This should be either an integer or a string representing
     * the unique identifier of a user. See [[\yii\web\User::id]].
     * @param Item $item the role or permission that this rule is associated with
     * @param array $params parameters passed to [[CheckAccessInterface::checkAccess()]].
     * @return bool a value indicating whether the rule permits the auth item it is associated with.
     * @throws NotFoundHttpException
     */
    public function execute($user, $item, $params)
    {
        $article_id = filter_var(ArrayHelper::getValue($params, 'article_id'), FILTER_VALIDATE_INT);

        if ($article_id === false)
            throw new \InvalidArgumentException('article_id required and must be int or can be converted to int');

        /** @var Article $article */
        $article = Article::find()->where(['id' => $article_id])->select('user_id, status_id')->oneOrFail();

        $userOnly = ArrayHelper::getValue($params, 'userOnly', false);
        $allowedStatuses = ArrayHelper::getValue($params, 'allowedStatuses', [Status::DRAFT, Status::RETURNED]);
        $adminRoles = ArrayHelper::getValue($params, 'adminRoles', ['editor', 'webmaster']);
        $additionalCondition = ArrayHelper::getValue($params, 'additionalCondition', true);

        $canByOwn = $article->user_id == $user;
        $canByStatus = in_array($article->status_id, $allowedStatuses);
        $canByRole = AccessHelper::can($adminRoles);

        return (($canByOwn && $canByStatus) || ($canByRole && !$userOnly)) && $additionalCondition;
    }
}

class AccessHelper
{
    /**
     * @param array|string $roles
     * @return bool
     */
    public static function can($roles)
    {
        if (is_string($roles))
            $roles = explode(',', preg_replace('/\s/', '', $roles));

        return (bool)array_filter(array_map([Yii::$app->user, 'can'], $roles));
    }

    // кстати это сейчас перестало работать потому что я роль редактора унаследовал от юзера, но по другому не знаю как...
    public static function currentUserRoleIs($name)
    {
        $usrRoles = ArrayHelper::getColumn(Yii::$app->authManager->getRolesByUser(Yii::$app->user->id), 'name');
        return in_array($name, $usrRoles);
    }
}
И вот такой жуткий вызов:

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

public function behaviors() {
	return [
		'access' => [
			'class' => AccessControl::class,
			'rules' => [
				[
					'actions' => ['edit-step1', 'edit-step2', 'edit-step3', 'edit-step4', 'check'],
					'allow' => true,
					'roles' => ['updateOwnArticle'],
					'roleParams' => function() {
						return [
							'article_id' => Yii::$app->request->get('id')
						];
					}
				],
				[
					'actions' => ['delete'],
					'allow' => true,
					'roles' => ['updateOwnArticle'],
					'roleParams' => function() {
						$article = Article::findOneOrFail(Yii::$app->request->get('id'));

						return [
							'article_id' => $article->id,
							'allowedStatuses' => [Status::DRAFT],
							'adminRoles' => ['editor'],
							'additionalCondition' => !$article->getPayments()->exists(),
						];
					}
				]
			],
		],
	];
}
Или например применяю то же правило для файлов статьи:

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

public function behaviors() {
	return [
		'access' => [
			'class' => AccessControl::class,
			'rules' => [
				[
					'actions' => ['create'],
					'allow' => true,
					'roles' => ['updateOwnArticle'],
					'roleParams' => function() {
						return [
							'article_id' => Yii::$app->request->post('ArticleFile')['article_id']
						];
					}
				],
				[
					'actions' => ['delete'],
					'allow' => true,
					'roles' => ['updateOwnArticle'],
					'roleParams' => function() {
						return [
							'article_id' => ArticleFile::find()
								->where(['id' => Yii::$app->request->get('id')])
								->select('article_id')
								->scalar()
						];
					}
				],
			],
		],
	];
}
Подскажите как это все может сделать оптимальнее? Под "оптимальнее" я предполагаю отсутствие необходимости лишних запросов (например статья по id ищется в правиле, а потом еще раз в action) и в принципе какое-то разбиение кода на более мелкие составляющие.
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

Как-то странно разрешение называется updateOwnArticle - модератор может менять свою статью. Лучше бы было updateArticle.

Что касается "лишних запросов", что мешает сразу Yii::$app->request->get('id') передать в ArticleRule?

Вместо

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

'roleParams' => function() {
						return [
							'article_id' => ArticleFile::find()
								->where(['id' => Yii::$app->request->get('id')])
								->select('article_id')
								->scalar()
						];
					}
Требовать

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

'roleParams' => function() {return ['article_id' => Yii::$app->request->get('article')];}
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

yiijeka писал(а): 2018.08.06, 08:22 Как-то странно разрешение называется updateOwnArticle - модератор может менять свою статью. Лучше бы было updateArticle.
Так почему-то не работает. Кстати в примере по которому я разбирался с RBAC тоже было сделано на двух правилах.
yiijeka писал(а): 2018.08.06, 08:22 Что касается "лишних запросов", что мешает сразу Yii::$app->request->get('id') передать в ArticleRule?

Вместо

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

'roleParams' => function() {
						return [
							'article_id' => ArticleFile::find()
								->where(['id' => Yii::$app->request->get('id')])
								->select('article_id')
								->scalar()
						];
					}
Требовать

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

'roleParams' => function() {return ['article_id' => Yii::$app->request->get('article')];}
Потому что в запросе нет никакой переменной article. В запросе есть только id файла. А чтобы получить id статьи надо выбрать из базы модель файла и в ней уже будет id статьи (т.к. они связаны). Но я скорее говорю даже не об этом, а например вон в действии удаления статьи я сначала нахожу статью в параметрах правила, а потом мне еще в действии самом надо ее найти чтобы вызвать $article->delete(). Т.е. выходит два запроса вместо одного. Даже три - т.к. я еще в правиле самом ищу статью, точнее ее поля.
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

В статье, по которой разбирались updateOwnNews и updateNews никак между собой не связаны, это два разные разрешения, хоть и называются подобно... Вполне внятно и корректно написано в офф. доке https://www.yiiframework.com/doc/guide/ ... user-notes

По поводу file: Т.к. он имеет связь, то можно требовать, чтобы запрос содержал id статьи. Во view требуется то только отправить $file->article_id ..

По поводу двойного запроса: Есть кеширование.... Ну а можно отказаться от AccessControl и прямо в методе, когда получите $article передавать её в метод Rules, т.е.

вместо

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

 public function execute($user, $item, $params)
    {
        $article_id = filter_var(ArrayHelper::getValue($params, 'article_id'), FILTER_VALIDATE_INT);
        if ($article_id === false)
            throw new \InvalidArgumentException('article_id required and must be int or can be converted to int');

        /** @var Article $article */
        $article = Article::find()->where(['id' => $article_id])->select('user_id, status_id')->oneOrFail();
        
Будет:

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

 public function execute($user, $item, $params)
    {
        $article = \yii\di\Instance::ensure(ArrayHelper::getValue($params, 'article'), Article::class);
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

yiijeka писал(а): 2018.08.06, 09:52 В статье, по которой разбирались updateOwnNews и updateNews никак между собой не связаны, это два разные разрешения, хоть и называются подобно... Вполне внятно и корректно написано в офф. доке https://www.yiiframework.com/doc/guide/ ... user-notes
Увы. Я неспроста читаю переведенные доки - я почти не знаю английский (и не надо меня убеждать - у меня нет ни времени, ни денег, ни желания на его изучение). По поводу "двух разных разрешений" я все равно не понял... Смотрите как они у меня добавлялись в базу:

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

$updateArticlePermission = new ArticleRule();
$auth->add($updateArticlePermission);
$updateOwnArticlePermission = $auth->createPermission('updateOwnArticle');
$updateOwnArticlePermission->description = 'Может обновлять свои статьи';
$updateOwnArticlePermission->ruleName = $updateArticlePermission->name;
$auth->add($updateOwnArticlePermission);
$auth->addChild($userRole, $updateOwnArticlePermission);
$auth->addChild($editorRole, $userRole);
Т.е. updateArticle - это правило, а updateOwnArticle это пермишн. Хз что это значит, но так оно работает. А с одним только правилом кажется нет...
yiijeka писал(а): 2018.08.06, 09:52 По поводу file: Т.к. он имеет связь, то можно требовать, чтобы запрос содержал id статьи. Во view требуется то только отправить $file->article_id ..
Как вариант да. Можно. Это пожалуй избавит меня от запроса из базы id статьи, но данные статьи для валидации мне все равно понадобятся.
yiijeka писал(а): 2018.08.06, 09:52 По поводу двойного запроса: Есть кеширование....
Кэшировать не совсем ясно на какое время. Чтобы это кэширование работало только во время вызова правила и метода, а на следующий запрос к тому же урлу уже не влияло.
yiijeka писал(а): 2018.08.06, 09:52 По поводу двойного запроса: Есть кеширование.... Ну а можно отказаться от AccessControl и прямо в методе, когда получите $article передавать её в метод Rules, т.е.

вместо

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

 public function execute($user, $item, $params)
    {
        $article_id = filter_var(ArrayHelper::getValue($params, 'article_id'), FILTER_VALIDATE_INT);
        if ($article_id === false)
            throw new \InvalidArgumentException('article_id required and must be int or can be converted to int');

        /** @var Article $article */
        $article = Article::find()->where(['id' => $article_id])->select('user_id, status_id')->oneOrFail();
        
Будет:

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

 public function execute($user, $item, $params)
    {
        $article = \yii\di\Instance::ensure(ArrayHelper::getValue($params, 'article'), Article::class);
Ну ка с этого места поподробнее. Как эту штуку использовать? Это типа как статичный массив который я могу использовать и из правила и из метода?
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

Офф документация идёт с переводом: https://www.yiiframework.com/doc/guide/ ... horization

Существенное отличие кода офф. документации от статьи в строке

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

$auth->addChild($updateOwnPost, $updatePost);
а у вас

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

$auth->addChild($editorRole, $userRole);
updateArticle - это правило, а updateOwnArticle это пермишн. Хз что это значит, но так оно работает...
пермишн, Permission - по русски РАЗРЕШЕНИЕ - базовая вещь RBAC, вторая базовая вещь - это РОЛЬ. Стандарт RBAC описывает как должны согласовываться между собой РОЛИ И РАЗРЕШЕНИЯ, и как их соотносить с ПОЛЬЗОВАТЕЛЕМ.

Всё! Всё остальное - "правила", "маршруты" и прочие выдуманные вещи, не связаны с RBAC. Они лишь производные вещи, которые программисты притянули для решения конкретных локальных проблем. Правила это обычно куча условий (if-ов), которые помогают определить доступно ли конкретное РАЗРЕШЕНИЕ для конкретной РОЛИ.

Есть ли на русском правильное описание RBAC даже и не знаю. Английский стандарт стоит 60$ :) Бесплатно, только устаревший 2012 года. Все статьи, которые встречались в русском сегменте содержат ошибки в корне. Например, статья, вами приведённая:

Суть работы RBAC следующая: вы создаете экшены .... (facepalm, дальше и читать не стоит)

Конкретно вам я ничего не могу посоветовать, вы с английским не дружите, а рассказывать то, что и так уже написано в СТАНДАРТЕ по RBAC нет желания.
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

yiijeka писал(а): 2018.08.06, 10:53 пермишн, Permission - по русски РАЗРЕШЕНИЕ - базовая вещь RBAC, вторая базовая вещь - это РОЛЬ. Стандарт RBAC описывает как должны согласовываться между собой РОЛИ И РАЗРЕШЕНИЯ, и как их соотносить с ПОЛЬЗОВАТЕЛЕМ.
Просто объясните мне что происходит когда я делаю вот так?

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

$updateArticlePermission = new ArticleRule();
$auth->add($updateArticlePermission);
Это не роль, но кажется и не пермишн, потому что пермишн создается методом $auth->createPermission. Вот что меня путает пожалуй больше всего. Поэтому и понадобилось делать этот updateOwnArticle.
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

$auth - это authManager, компонент для работы с разрешениями и ролями - сущностям RBAC. Позволяет производить поиск по этим сущностям, сохранять их в хранилище( может быть SQL, может быть файлы) и прочие операции... смотрите API AuthManager

Как я выше говорил - Rule, это правило, обычно это куча условий (if-ов), которые помогают определить доступно ли конкретное РАЗРЕШЕНИЕ для конкретной РОЛИ.

Т.е. ArticleRule просто код с кучей IF IF IF IF ...
вы создали объект этого класса $updateArticlePermission = new ArticleRule(); и назвали его зачем-то неправильно - updateArticlePermission (разрешение обновить статью)

Дальше просите $auth сохранить этот объект в хранилище, через метод add
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

yiijeka писал(а): 2018.08.06, 12:18 Как я выше говорил - Rule, это правило, обычно это куча условий (if-ов), которые помогают определить доступно ли конкретное РАЗРЕШЕНИЕ для конкретной РОЛИ.
Это звучало бы логично если бы у одного разрешения было несколько правил. Но правило всегда одно - оно указывается в поле $permission->ruleName. Таким образом я не понимаю зачем нужно дублирование в базе, ведь правило и пермишн это по сути одно и тоже.
Аватара пользователя
maleks
Сообщения: 1985
Зарегистрирован: 2012.12.26, 12:56

Re: Правильное использование RBAC

Сообщение maleks »

Brainfuck писал(а): 2018.08.06, 12:37 Это звучало бы логично если бы у одного разрешения было несколько правил. Но правило всегда одно - оно указывается в поле $permission->ruleName.
Бывает так что пермишены разные, а правило для них одно и тоже.
Brainfuck писал(а): 2018.08.06, 12:37 Таким образом я не понимаю зачем нужно дублирование в базе, ведь правило и пермишн это по сути одно и тоже.
Это вообще разные вещи. И хранятся отдельно, для правил своя таблица существует. Это дополнительные алгоритмы проверок.
Yii2 universal module sceleton - for basic and advanced templates
Аватара пользователя
maleks
Сообщения: 1985
Зарегистрирован: 2012.12.26, 12:56

Re: Правильное использование RBAC

Сообщение maleks »

Brainfuck писал(а): 2018.08.06, 10:18 Увы. Я неспроста читаю переведенные доки - я почти не знаю английский (и не надо меня убеждать - у меня нет ни времени, ни денег, ни желания на его изучение).
Вас это и подводит, например тут:

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

$updateArticlePermission = new ArticleRule();
именуете как разрешение, а храните вроде как правило(rule), что создает жуткую путаницу
Yii2 universal module sceleton - for basic and advanced templates
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

maleks писал(а): 2018.08.06, 13:35 Бывает так что пермишены разные, а правило для них одно и тоже.
Чисто из интереса: зачем???! :shock:
maleks писал(а): 2018.08.06, 13:35 Это вообще разные вещи. И хранятся отдельно, для правил своя таблица существует. Это дополнительные алгоритмы проверок.
Я знаю что они хранятся в разных таблицах. Я просто не могу придумать ситуации когда понадобилось бы делать несколько разрешений с одинаковым правилом.
Последний раз редактировалось Brainfuck 2018.08.06, 14:51, всего редактировалось 1 раз.
Аватара пользователя
maleks
Сообщения: 1985
Зарегистрирован: 2012.12.26, 12:56

Re: Правильное использование RBAC

Сообщение maleks »

Brainfuck писал(а): 2018.08.06, 14:50 Я просто не могу придумать ситуации когда понадобилось бы делать несколько разрешений с одинаковым правилом.
Например разрешения разные - "удалять свою страницу", "редактировать свою страницу". А правило им подойдет одно и то же - "моя ли страница"
Yii2 universal module sceleton - for basic and advanced templates
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

maleks писал(а): 2018.08.06, 14:58
Brainfuck писал(а): 2018.08.06, 14:50 Я просто не могу придумать ситуации когда понадобилось бы делать несколько разрешений с одинаковым правилом.
Например разрешения разные - "удалять свою страницу", "редактировать свою страницу". А правило им подойдет одно и то же - "моя ли страница"
Ну и нафиг надо делать два разрешения если можно обойтись одним? Как собственно я и делаю. У меня одно разрешение используется в туевой хуче мест. Для обновления не только статьи, но и всех связанных с ней моделей.
Аватара пользователя
maleks
Сообщения: 1985
Зарегистрирован: 2012.12.26, 12:56

Re: Правильное использование RBAC

Сообщение maleks »

Brainfuck писал(а): 2018.08.06, 14:59 Ну и нафиг надо делать два разрешения если можно обойтись одним?
Это уже как бизнес правила скажут, программист тут не указ. Пример спрошен, пример дан, пример когда требуется разделять, а как там у вас и что, совсем не интересно.
Yii2 universal module sceleton - for basic and advanced templates
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

Еще появился вопрос по дефолтным ролям. Вот я прописал в конфиге:

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

'authManager' => [
	'class' => 'yii\rbac\DbManager',
	'defaultRoles' => ['user']
],
И все по умолчанию имеют роль юзера. Но помимо юзеров есть и редакторы например. Но получается такая фигня что редакторы они как-бы одновременно и юзеры... Да, логически рассуждая это так, но мне не нужно наследование редакторов от юзеров. Просто когда я пишу Yii::$app->user->can('user') для редактора возвращает true и ломает мне логику... Как мне избавиться от такого поведения не убирая дефолтную роль? Т.е. я не хочу для каждого юзера делать в базе пометку что он юзер - это как-то тупо. Для этого как раз и существует такая настройка.
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

> Ну и нафиг надо делать два разрешения если можно обойтись одним?
> просто не могу придумать ситуации когда понадобилось бы делать несколько разрешений с одинаковым правилом.

Это может быть одним разрешением, когда у вас всё просто.
1) Удалить страницу
2) Редактировать страницу
По сути выливается в два разрешения "Изменение страницы" и "Изменение своей страницы"(тут привязывается Rule)


Но когда требования становятся сложнее:
Компания 1
- роль Директор имеет разрешение "Просмотр данных"
- роль Менеджер имеет разрешение "Подтвержедение внесённых данных"
- роль Работник имеет разрешение "Просмотр внесённых данных"
- роль Работник имеет разрешение "Внесение данных"

Компания 2
- роль Директор имеет разрешение "Просмотр данных"
- роль Менеджер имеет разрешение "Подтвержедение внесённых данных"
- роль Работник имеет разрешение "Просмотр внесённых данных"
- роль Работник имеет разрешение "Внесение данных"

Будет одно Rule в котором проверяется: принадлежат ли данные компании, которой пренадлежит текущий пользователь

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

public function execute($user, $item, $params) {
    return $user->getCompany === $params['company'];
}
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

Brainfuck писал(а): 2018.08.06, 16:14 Просто когда я пишу Yii::$app->user->can('user') для редактора возвращает true и ломает мне логику...
Значит ваша логика неверна.
Значит у вас не должно быть, что все пользователи имеют роль user.
....
Аватара пользователя
yiijeka
Сообщения: 3103
Зарегистрирован: 2012.01.28, 09:14
Откуда: Беларусь
Контактная информация:

Re: Правильное использование RBAC

Сообщение yiijeka »

Т.е. я не хочу для каждого юзера делать в базе пометку что он юзер - это как-то тупо.
Для этого можно при регистрации пользователя назначать ему роль user. А когда пользователь получает роль редактора, то убирать ему роль user. В таком случае у вас получается, что у Пользователя может быть только одна РОЛЬ, имеет права на жизнь, но не соотвествует ожиданиям RBAC...
Brainfuck
Сообщения: 313
Зарегистрирован: 2018.02.19, 14:20

Re: Правильное использование RBAC

Сообщение Brainfuck »

yiijeka писал(а): 2018.08.06, 16:31 Для этого можно при регистрации пользователя назначать ему роль user. А когда пользователь получает роль редактора, то убирать ему роль user.
Раньше я так и делал, но реально это же странно - назначать всем одну и ту же роль и хранить это в базе... Логичнее хранить только данные о присвоении роли редактора и т.п.
yiijeka писал(а): 2018.08.06, 16:31В таком случае у вас получается, что у Пользователя может быть только одна РОЛЬ, имеет права на жизнь, но не соотвествует ожиданиям RBAC...
А почему это не соответствует ожиданиям RBAC? По моему как раз логично что пользователь одновременно имеет только одну роль. Вот разрешений у роли может быть много. Но роль все таки одна.

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

Кстати создание пермишна не спасет в данном случае т.к. если редактор одновременно и юзер, а я например дам этот пермишн только юзеру то для редактора раздел все равно будет отображаться.
Ответить