Как разобраться в иерархической системе RBAC / Yii framework


Оригинал: Getting to Understand Hierarchical RBAC Scheme

Authentication and Authorization — это хороший учебник, в котором, помимо прочих тем, описываются основные аспекты реализации иерархической системы RBAC в Yii. Но сколько я ни читал этот толковый учебник, я не мог понять, как именно работает иерархия. Я нашел, как определить авторизацию иерархии, как оцениваются правила управления, как настраивается authManager, но почти ничего не обнаружил о том, как строить свою иерархию, в какой последовательности проверяются ее узлы, когда проверка останавливается, и каков будет результат проверки.

У меня ничего не оставалось, кроме как начать самому копаться в коде Yii. В этой статье я хочу представить вашему вниманию свои выводы по данному вопросу. Отмечу, что разобраться в коде Yii не представляет особых сложностей, так как он хорошо структурирован. Каждый может это сделать, но следующая информация поможет вам сэкономить время, особенно если вы новичок в работе с Yii-фреймворком.

Я должен сказать, что понять смысл статьи было бы намного проще, если бы вы ознакомились с вышеупомянутым учебником. Особенно начиная с темы Role-Based Access Control.
Давайте рассмотрим пример иерархии, который приведен в учебнике (здесь показано, как система безопасности может быть построена для некоторых блогов):
$auth=Yii::app()->authManager;
 
$auth->createOperation('createPost','create a post');
$auth->createOperation('readPost','read a post');
$auth->createOperation('updatePost','update a post');
$auth->createOperation('deletePost','delete a post');
 
$bizRule='return Yii::app()->user->id==$params["post"]->authID;';
$task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule);
$task->addChild('updatePost');
 
$role=$auth->createRole('reader');
$role->addChild('readPost');
 
$role=$auth->createRole('author');
$role->addChild('reader');
$role->addChild('createPost');
$role->addChild('updateOwnPost');
 
$role=$auth->createRole('editor');
$role->addChild('reader');
$role->addChild('updatePost');
 
$role=$auth->createRole('admin');
$role->addChild('editor');
$role->addChild('author');
$role->addChild('deletePost');

Прежде всего, я хочу преобразовать код в более удобную для человеческого восприятия форму:

Бирюзовые прямоугольники — это роли, желтый прямоугольник — задачи, а самый низкий уровень авторизации занимают операции в прямоугольниках персикового цвета. Вместе роли, задачи и операции, называются элементами авторизации. Вы должны иметь в виду, что функционально все эти элементы авторизации равноправны. Все полностью зависит от вас — сделать некоторые элементы авторизации ролями или задачами. Они будут выполнять одни и те же функции. Различные типы элементов авторизации вводятся исключительно с целью сделать процесс более удобным. Вы не ограничены в трех уровнях авторизации: может быть нескольких уровней ролей, задач и операций. (Вернувшись к схеме, вы можете увидеть, что этот момент проиллюстрирован несколькими уровнями ролей.) Можно пропустить любой из этих уровней (роль автор обращается непосредственно к дочерней операции создать, игнорируя задачу). Единственным ограничением является то, что в иерархии авторизации роли должны оставаться выше, чем задачи, а задачи должны оставаться выше, чем операции.

Теперь давайте прикинем — о чем думает человек, который хочет создать блог. Все вроде бы вполне логично. Самая пассивная роль отводится читателю: единственное, на что он имеет право — читать. Автор имеет немного больше власти: он может создавать посты и обновлять свои собственные записи. Редактор может читать посты и обновлять (править) все посты, а не только собственные (на самом деле, в соответствии с иерархией, редакторы не могут создавать свои записи). И конечно, самая мощная роль достается администратору, который может сделать все, что угодно.

Если вы знакомы с принципами объектно-ориентированной иерархии, уже имеющиеся знания могут запутать вас. В каждом последующем уровне дерева объектов объект получает (наследует) все (или часть) особенностей родительских (базовых) объектов. Это приводит к тому, что нижележащие объекты становятся наиболее «загруженными» функциями, в то время как корневые объекты имеют только основные возможности. Совершенно противоположное происходит с системой иерархии RBAC в Yii-фреймворке. Нижние элементы в иерархии авторизации представляют собой основные операции, в то время как верхние элементы авторизации (как правило, роли) являются наиболее мощными и служат объединяющим звеном во всей системе авторизации.

Теперь, когда с идеей иерархии стало все ясно, давайте разберемся, как работает проверка доступа. Чтобы проверить, разрешается ли текущему пользователю выполнять определенное действие, вы должны вызвать метод CheckAccess, например:
if(Yii::app()->user->checkAccess('createPost'))
{
    // create post
}

Как наша иерархия используется Yii-фреймворком для проверки доступа? Я приведу здесь в качестве примера кусок кода Yii-фреймворка, ответственный за проверку доступа (реализация CAuthManager для баз данных CDbAutManager) для справки:
if(($item=$this->getAuthItem($itemName))===null)
 return false;
Yii::trace('Checking permission "'.$item->getName().'"','system.web.auth.CDbAuthManager');
if($this->executeBizRule($item->getBizRule(),$params,$item->getData()))
{
 if(in_array($itemName,$this->defaultRoles))
  return true;
 if(isset($assignments[$itemName]))
 {
  $assignment=$assignments[$itemName];
  if($this->executeBizRule($assignment->getBizRule(),$params,$assignment->getData()))
   return true;
 }
 $sql="SELECT parent FROM {$this->itemChildTable} WHERE child=:name";
 foreach($this->db->createCommand($sql)->bindValue(':name',$itemName)->queryColumn() as $parent)
 {
  if($this->checkAccessRecursive($parent,$userId,$params,$assignments))
   return true;
 }
}
return false;

Когда вы вызываете CheckAccess, Yii начинает рекурсивно подниматься по иерархической системе авторизации и проверять правило управления для каждого элемента. Например, когда вы совершаете вызов, как здесь, происходит следующее:
Yii::app()->user->checkAccess('readPost')

Сначала Yii проверяет правило управления readPost (напомним, что пустое правило управления приравнивается к правилу управления, всегда возвращающему истину). Затем он ищет все родительские объекты у readPost, — вот автор и редактор — и также проверяет их правила управления. Этот процесс не остановить, пока правила управления оцениваются как истина. Он останавливается только тогда, когда некоторые правила возвращают ложь или мы достигли вершины иерархии, и больше нет родительских объектов для проверки.

Так какие пути для метода CheckAccess возвращают истину? Их два. Во-первых, итерация может остановится с положительным результатом, когда Yii встречает в иерархии так называемую роль по умолчанию. Это роль, назначенная по умолчанию для всех авторизованных пользователей. Для нашего блога это, например, роль читателя. Роли по умолчанию могут быть установлены в файле конфигурации веб-приложения. Как это делается, подробно описано в разделе учебника Using Default Roles.

Второй способ возвращения истины для метода CheckAccess состоит в создании списка назначения разрешений, которые в основном определяют пару -. В коде это можно сделать так:
$auth->assign('reader','Pete');
$auth->assign('author','Bob');
$auth->assign('editor','Alice');
$auth->assign('admin','John');

Это семантически эквивалентно назначению ролей пользователям. Вы не ограничены назначением ролей. Пользователям могут быть назначены отдельные задачи и операции. В реальной жизни было бы более практичным не усложнять код авторизации всеми назначениями, а хранить их в базе данных. Вы можете реализовать эту возможность, используя компонент CDbAuthManager, который описан в учебнике по Yii-фреймворку.

Давайте вернемся к обсуждению CheckAccess. Перед тем как начнется итерация иерархии, Yii собирает все элементы авторизации, присвоенные текущему пользователю, и на каждом шаге итерации сверяет текущие элементы аутентификации со списком назначенных. Если все сходится, то итерация останавливается и возвращает положительный результат.

Предположим, что мы хотим обеспечить безопасность действия пользователя «Обновление поста». Тот, кто входит в блог, должен пройти проверку авторизации, прежде чем он сможет перейти к редактированию. Поэтому наиболее подходящим местом для проверки доступа будет размещение в начале соответствующего контроллера действий:
public function actionUpdatePost()
{
 if(!Yii::app()->user->checkAccess('updatePost'))
  Yii::app()->end();
 
 // ... more code
}

Предположим, что текущего пользователя зовут Alice. Давайте посмотрим, как Yii обрабатывает авторизацию иерархии. Хотя updateOwnPost является непосредственным родителем updatePost и возвращает ложь, Yii быстро находит другой родительский элемент авторизации, который возвращает истину — роль редактора. В результате, Alice получает разрешение на обновление поста. Что произойдет, если Bob входит в систему? В этом случае ветвь иерархии, проходящая через элемент редактор, также обрабатывается, но нет элементов, возвращающих истину. Единственный возможный путь для успешной проверки доступа лежит через элемент updateOwnPost.

Но updateOwnPost более сложен, чем пустое «всегда истинное» правило управления (см. первый фрагмент кода в начале статьи) и для проверки требуется ID поста автора. Как мы можем интегрировать его в правила управления? В виде параметра CheckAccess. Для достижения этой цели мы должны настроить наш обработчик действий следующим образом:
public function actionUpdatePost()
{
 // here we obtain $post, probably via active record ...
 
 if(!Yii::app()->user->checkAccess('updatePost', array('post'=>$post)))
  Yii::app()->end();
 
 // ... more code
}

Обратите внимание, несмотря на то, что updateOwnPost возвращает истину для Bob, итерация авторизации иерархии все еще продолжается. Действие прекратится и успешно завершится в том случае, если найдется элемент автор.

Я думаю, теперь вы без труда сможете понять, как Yii проверяет доступ, учитывая, что Pete и John залогинились.

Вернемся к вышеприведенному фрагменту кода. Вам может показаться, что мы получаем параметры поста для операции updatePost, чьи правила управления пусты и не требуют никаких параметров. Это по большому счету правда, но есть свои нюансы. На самом деле, Yii проверяет один и тот же набор параметров (их может быть несколько, в этом случае они передаются в виде массива) для каждого элемента иерархии на каждом этапе итерации. Если правила управления элементом не требуют параметров, они просто будут проигнорированы. Обрабатываются только те параметры, которые необходимы для работы.

Это ведет к двум возможным стратегиям передачи параметров. В первом случае для каждого элемент аутентификации с его иерархией обеспечивается вызов CheckAccess с точным числом параметров. Преимущество этого варианта заключается в краткости кода и высокой эффективности. Другая стратегия состоит в том, чтобы всегда передавать все параметры каждому элементу аутентификации, независимо от того, будут ли они на самом деле использованы для оценки правил управления. Этот метод называется «выстрелил-и-забыл». Он может помочь избежать ненужных проб и ошибок при организации безопасности приложения. Его недостаток — некоторая захламленность кода и вероятное падение производительности.

Заключение

Это основные сведения о моделировании иерархической RBAC-авторизации в Yii. С использованием фреймворка могут быть построены и более продвинутые модели безопасности. Вы можете обратиться к специалистам The Definitive Guide to Yii и Class Reference, чтоб получить более подробную информацию. Также есть целый ряд веб-интерфейсов, реализованных в виде расширений, которые могут помочь вам в Yii RBAC-администрировании.