Контроль доступа с использованием ролей (RBAC)
Я думаю, все, кто хоть немного работал с фреймворком Yii знают, что он поддерживает возможность разграничения прав доступа на основе ролей.
Принцип работы этой системы достаточно прост. Вы создаёте наборы правил и пользователей, связываете их между собой. После этого, вы в любой момент можете проверить, имеет ли пользователь право на выполнение какой-то операции или нет.
Одно из основных преимуществ использования данной библиотеки заключается в том, что вам нужно написать минимум кода для проверки прав доступа. Обычно этот код выглядит следующим образом.
if (!Yii::app()->user->checkAccess('createUser')) {
throw new CHttpException(403, 'Forbidden');
}
//остальной код…
В теории всё просто. Но на практике, документации и примеров по этой теме практически нет (надеюсь, это скоро изменится).
Основные источники информации (на русском):
Аутентификация и авторизация и
RBAC и описание ролей в файле. На английском хороших и подробных примеров, к сожалению, я не нашел.
Примечание. Очень советую прочитать эти статьи, прежде чем переходить к моему примеру.
Когда я первый раз решил использовать RBAC, то выяснилось, что есть множество нюансов, которые приходится учитывать при работе с этой библиотекой. Ничего запредельно сложного и недоступного для понимания, но «ковырялся» я довольно долго ;)
В этой статье мы рассмотрим пример создания несложной системы управления пользователями (идея взята из одного web приложения).
Постановка задачи.
Есть три типа пользователей.
Обычный (user) – может создавать и редактировать свои данные (например, список контактов) и изменять свои логин/пароль.
Администратор (admin) – может создавать новых пользователей, но не может изменять их данные. Он может изменить свои собственные логин и пароль, но не может изменить роль.
Суперпользователь (root) – может выполнять любые операции с пользователями.
Ни admin, ни root не имеют доступа к персональным данным пользователя (которые он создаёт при работе с приложением).
Примечание. Чтобы сократить объем кода, я урезал набор правил. Но, думаю, вы без особого труда сможете его дополнить. Главное понять идею и принцип работы.
Шаг первый. Выбираем тип хранилища для ролей и операций.
Для этих целей Yii позволяет использовать PHP файл (CPhpAuthManager) или базу данных (CDbAuthManager).
Если вы предполагаете, что количество пользователей будет большим, то лучше использовать БД. Но для знакомства с библиотекой лучше использовать PHP файл, т.к. читать его легче. К тому же перейти от одного типа хранилища к другому совсем несложно.
Для того, чтобы Yii «узнал» о вашем выборе, в массив с настройками (config/main.php) нужно добавить следующий элемент.
'components'=>array(
…
'authManager'=>array(
'class' => 'CPhpAuthManager',
),
),
Шаг второй. Создадим таблицу в БД для хранения пользователей.
Эта таблица будет состоять из шести полей.
u_id – первичный ключ;
u_name – имя пользователя;
u_email – адрес почты (используется как логин);
u_pass – пароль;
u_state – статус (активен, заблокирован);
u_role – роль пользователя (root, admin, user).
В данном примере для нас играет роль последнее поле (u_role).
Шаг третий. Создаём операции, роли и задачи.
Я очень надеюсь, что вы прочли статью из
Полного руководства и знаете, что представляют собой операции, роли и задачи и зачем они нужны.
Сразу перейдём к созданию файла с этими настройками.
Т.к. в качестве хранилища мы выбрали PHP файл, то можно создать его вручную (этот способ подробно рассмотрен в статье
RBAC и описание ролей в файле).
Но, на мой взгляд, гораздо удобнее использовать API. Поэтому создадим небольшой инсталляционный скрипт (контроллер SiteController метод actionInstall).
Примечание. Данные будут сохранены в файле protected/data/auth.php. Файл будет создан автоматически, поэтому запись в эту папку должна быть разрешена.
public function actionInstall() {
$auth=Yii::app()->authManager;
//сбрасываем все существующие правила
$auth->clearAll();
//Операции управления пользователями.
$auth->createOperation('createUser', 'создание пользователя');
$auth->createOperation('viewUsers', 'просмотр списка пользователей');
$auth->createOperation('readUser', 'просмотр данных пользователя');
$auth->createOperation('updateUser', 'изменение данных пользователя');
$auth->createOperation('deleteUser', 'удаление пользователя');
$auth->createOperation('changeRole', 'изменение роли пользователя');
$bizRule='return Yii::app()->user->id==$params["user"]->u_id;';
$task = $auth->createTask('updateOwnData', 'изменение своих данных', $bizRule);
$task->addChild('updateUser');
//создаем роль для пользователя admin и указываем, какие операции он может выполнять
$role = $auth->createRole('admin');
$role->addChild('createUser');
$role->addChild('viewUsers');
$role->addChild('readUser');
$role->addChild('updateOwnData');
//все пользователи будут создаваться по-умолчанию с ролью user,
//только root может менять роль другого пользователя
//создаем роль для пользователя root
$role = $auth->createRole('root');
//наследуем операции, определённые для admin'а и добавляем новые
$role->addChild('admin');
$role->addChild('updateUser');
$role->addChild('deleteUser');
$role->addChild('changeRole');
//создаем операции для user'а
$bizRule='return Yii::app()->user->id==$params["contact"]->c_user_id;';
$auth->createOperation('createContact','создание контакта');
$auth->createOperation('viewContacts','просмотр списка контактов');
$auth->createOperation('readContact','просмотр контакта', $bizRule);
$auth->createOperation('updateContact','редактирование контакта',$bizRule);
$auth->createTask('deleteContact','удаление контакта',$bizRule);
//создаем роль user и добавляем операции для неё
$user = $auth->createRole('user');
$user->addChild('createContact');
$user->addChild('viewContacts');
$user->addChild('readContact');
$user->addChild('updateContact');
$user->addChild('deleteContact');
$user->addChild('updateOwnData');
//создаем пользователя root (запись в БД в таблице users)
//тут используем DAO, т.к. AR автоматически назначит пользователю роль user
$sql = 'INSERT INTO users(u_name, u_email, u_pass, u_state, u_role)'
.' VALUES ("root", "test@test.ru", "'.md5('11111')
.'", '.Users::STATE_ACTIVE.', "'.Users::ROLE_ROOT.'")';
$conn = Yii::app()->db;
$conn->createCommand($sql)->execute();
//связываем пользователя с ролью
$auth->assign('root', $conn->getLastInsertID());
//сохраняем роли и операции
$auth->save();
$this->render('install');
}
Метод получился довольно объемный, но в нём большую часть занимаю вызовы createOperation и addChild, которые создают операции и связывают их с ролями.
Большинство операций в этом примере соответствуют методам контроллера (CRUD), но они могут быть любыми. Например, такими как changeRole, позволяющими изменять одно единственное поле записи.
Обратите внимание. После создания операций и ролей доступ ограничен не будет. Вы должны будете сами проверить у пользователя наличие прав с помощью метода checkAccess.
Отдельного внимания заслуживает использование бизнес правил (bizRule) в операциях.
Бизнес правило представляет собой обычный PHP код, который должен возвращать true или false. Этот код может получить массив с данными, который будет доступен через переменную $params.
Рассмотрим правило
$bizRule='return Yii::app()->user->id==$params["user"]->u_id;';
Здесь мы проверяем, соответствуют ли id текущего пользователя (который выполнил вход) и id записи в таблице пользователей, которую он хочет изменить. Таким образом, это правило позволит пользователю редактировать только свои данные.
В конце метода мы создаём пользователя root. Дело в том, что все пользователи будут создаваться с ролью user и только root может изменять роль. Поэтому мы создаём его сразу при инсталляции.
После создания пользователя назначаем ему роль (метод assign) и сохраняем изменения
$auth->save();
Шаг четвертый. Создание модели для работы с пользователями.
Как обычно, создаём модель с помощью генератора (gii) и затем вносим свои изменения.
Весь код модели я приводить здесь не буду (в конце статьи есть ссылка на архив с примером). Рассмотрим только измененные методы.
public function beforeSave() {
parent::beforeSave();
$this->u_pass = md5($this->u_pass);
/*
* Если пользователь не имеет права изменять роль, то мы должны
* установить роль по-умолчанию (user)
*/
if (!Yii::app()->user->checkAccess('changeRole')) {
if ($this->isNewRecord) {
//ставим роль по-умолчанию user
$this->u_role = Users::ROLE_USER;
}
}
return true;
}
Перед созданием новой записи мы проверяем, имеет ли текущей пользователь право изменять роли, если нет, то ставим роль по-умолчанию (user).
После сохранения (или создания) записи, нужно назначить пользователю роль. Права пользователя мы уже проверили и роль установили, поэтому сейчас просто назначаем пользователю роль (метод assign).
Предварительно, с помощью метода revoke удаляем связь между пользователем и ролью (если такая существовала). Если связь не удалить, то когда root будет изменять роли, у нас появятся пользователи с несколькими ролями.
public function afterSave() {
parent::afterSave();
//связываем нового пользователя с ролью
$auth=Yii::app()->authManager;
//предварительно удаляем старую связь
$auth->revoke($this->prevRole, $this->u_id);
$auth->assign($this->u_role, $this->u_id);
$auth->save();
return true;
}
При удалении пользователя не забываем удалить связь между ним и ролью.
public function beforeDelete() {
parent::beforeDelete();
//убираем связь удаленного пользователя с ролью
$auth=Yii::app()->authManager;
$auth->revoke($this->u_role, $this->u_id);
$auth->save();
return true;
}
Как видите, принцип работы достаточно простой. Главное, не забывайте вызывать $auth->save(); чтобы сохранить изменения.
Шаг пятый. Контроллер и представления.
Как и в случае с моделью, создаем контроллер и представления с помощью генератора (gii).
Методы filters и accessRules можно убрать, т.к. их мы не используем. В остальные методы добавляем проверку прав пользователя.
Опять же, весь контроллер целиком рассматривать нет смысла, он есть в архиве с исходниками. В качестве примера рассмотрим метод обновления данных пользователя.
Это самый сложный случай. У нас есть пользователь, которому можно изменять любые записи, есть пользователи, которым можно изменять только свою запись, но при этом нельзя изменять свою роль. Поэтому проверок будет две.
В первый раз проверяем, пытается ли изменить роль пользователь, у которого на это нет прав.
С помощью второй проверки убеждаемся, что у пользователя есть права на изменение данной записи.
public function actionUpdate()
{
$model=$this->loadModel();
//проверяем, можно ли пользователю изменять роль
if (isset($_POST['Users']['u_role']) && !Yii::app()->user->checkAccess('changeRole')) {
throw new CHttpException(403,'Forbidden');
}
//проверяем, может ли пользователь изменять данную запись
if (!Yii::app()->user->checkAccess('updateUser')
&& !Yii::app()->user->checkAccess('updateOwnData', array('user'=>$model))) {
throw new CHttpException(403,'Forbidden');
}
if(isset($_POST['Users']))
{
$model->prevRole = $model->u_role;
$model->attributes=$_POST['Users'];
if($model->save())
$this->redirect(array('view','id'=>$model->u_id));
}
$this->render('update',array(
'model'=>$model,
));
}
В представлении (views/users/_form.php) убираем из формы поле «Роль» для пользователя у которого нет прав её изменять.
…
<?php if (Yii::app()->user->checkAccess('changeRole')) { ?>
<div class="row">
<?php echo $form->labelEx($model,'u_role'); ?>
<?php echo $form->dropDownList($model,'u_role',array(
Users::ROLE_USER=>Users::ROLE_USER,
Users::ROLE_ADMIN=>Users::ROLE_ADMIN,
Users::ROLE_ROOT=>Users::ROLE_ROOT,
));
?>
<?php echo $form->error($model,'u_role'); ?>
</div>
<?php } ?>
…
На этом мы остановимся.