Yii PHP framework: контроль доступа с использованием ролей (RBAC)

6 августа, 2010
yii 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 } ?>
…

На этом мы остановимся. Общую идею, надеюсь, я объяснил. Если есть желание, можете скачать архив и поиграться с этим примером.

архив с исходным кодом

В архиве есть дамп базы и папка protected приложения. Само приложение вам нужно будет создать самостоятельно. Дальше, думаю, вы разберетесь ;)

Любые вопросы или замечания оставляйте в комментариях. Постараюсь ответить!

Понравилась статья? Подписывайтесь на продолжение rss link !

Или на мой твиттер twitter link

]]>

Добавьте эту страницу в google.com bobrdobr.ru del.icio.us technorati.com linkstore.ru news2.ru rumarkz.ru memori.ru moemesto.ru

]]>

Опубликовано в PHP, Web разработка, Yii Комментарии (28) »

]]>

Вы можете оставить комментарий. Трекбеки закрыты.

  • http://mecs.kz/ Shal

    Чем больше узнаю о Yii тем больше желание на него перейти. Относительно прост, относительно гибок. Единственно что не нравится, это документация

  • AntonYu

    Если знаешь английский на минимальном уровне, то всегда можно спросить на официальном форуме. Да и на yiiframework.ru/forum всегда помогают.

  • guest

    #
    //создаем пользователя 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();

    какой пздц

  • http://sergebezborodov.com Mail

    Если зашла тема об авторизации, очень хотелось бы почитать о использовании Zend_Acl в Yii framework

  • http://www.simplecoding.org Владимир

    Я знаю, но в данной ситуации это самый удобный вариант.
    При использовании AR библиотеки фреймворка создание пользователя выглядело бы так.
    $user = new Users;
    $user->u_name = 'root';
    $user->u_email = 'test@test.ru';

    $user->save();

    Но при этом был бы вызван обработчик события beforeSave в котором выполняется проверка прав текущего пользователя. При инсталляции никаких пользователей нет, поэтому добавить пользователя с ролью root не получится.

    В общем, удобнее один раз выполнить простой запрос.

    • Дамир Фахрутдинов

      Проблема Yii  и Ruby on Rails – валидаторы железно засунуты в модель.

  • http://www.simplecoding.org Владимир

    Почему именно Zend_Acl?

  • Elbek

    pochemuto Yii::app()->user->checkAccess('createUser') vsegda vozvrashaet true esli ya user

    smotrite function checkAccess v classe CPhpAuthManager, vnem on kak proveryayet u vas dosup est ili net tam nikakoy svyaz net s logined useram

    • Elbek

      checkAccess($operation,$this->getId(),$params); izvinite ya ne smotrel

  • Zel_mail

    Возможно, глупый вопрос, но не проясните ситуацию:
    почему createOperation ипользуется для 'createContact', 'viewContacts' и т.д., а для 'deleteContact' используется createTask?

    ( в документации на http://yiiframework.ru/doc/guide/ru/topics.auth тоже самое: createOperation для 'createPost' и т.п., а для 'updateOwnPost' используется createTask )

    Хотя в коде фреймвёка вроде отличия между ними только в типе дочерних элементов, которые можно добавлять. То есть, вроде бы, при наборе правил для пользователей вполне можно обойтись одними операциями, без задач.

    • http://www.simplecoding.org Владимир

      Обойтись можно, но такая структура позволяет группировать операции. В более сложных приложениях это имеет смысл использовать.

      >> Роль состоит из задач. Задача состоит из операций. Операция — разрешение на какое-либо действие (дальше не делится).

      • Zel_mail

        Попробую еще раз: вопрос был про почему для однотипных процессов в одном случае использовались операции, а в другом – задачи?

        То есть, логично было бы предположить, что роль User должна состоять из задач controlUserData ( состоит из операций читать и изменять свои данные ) и controlContacts ( состоит из операций с контактами ). Соответственно роли admin и root тоже должны состоять из задач.

  • Profi_net

    У меня таблица в БД в которой хранится:
    Группа пользователей->Модуль-> чтение, редактирование, удаление, добавление

    то есть грубо говоря есть админка в которой можно создавать группы пользователей, к каждой группе присвоено три модуля(которые могут быть потом добавлены) и к каждому модулю есть четыре права – чтение, редактирование, удаление, добавление

    Как сделать так чтобы не вручную вводить все правила, а что бы они выгружались из БД?

    • http://www.simplecoding.org Владимир

      Правила должны откуда-то взяться в БД. Т.е. один раз их нужно ввести.
      А для хранения использовать CDbAuthManager.
      'components'=>array(

      'authManager'=>array(
      'class' => 'CDbAuthManager',
      ),
      ),

      • Profi_net

        я просто не так немного написал…
        я так понимаю CDbAuthManager предполагает использование определенных таблиц в БД
        а у меня уже есть таблица, которая совсем по другому организована:
        столбцы: роль(группа пользователей), модуль, чтение,редактирование, изменение, удаление
        к примеру есть 2 модуля: блог, форум – для них в таблице есть следующие записи:
        админ,блог,1,1,1,1
        админ,форум,1,1,1,1
        модератор,блог,0,0,0,0
        модератор,форум,1,1,1,1
        То есть иными словами, идея такая что не на каждое действие идет разрешение/запрет некоторых правил, а на весь модуль.
        Понятно что в каждом модуле придется прописывать это все в ручную. Но не могу понять просто как сделать так что бы: Пользователь зашел, в системе зарегистрировалось то к какой группе пользователей он относится, и соответственно открылся доступ к соответствующим модулям. Но при этом администратор свободно мог добавить новые роли(группы пользователей), и при этом менять код только в модулях.
        Ну вот как то так примерно.

      • Profi_net

        я просто не так немного написал…
        я так понимаю CDbAuthManager предполагает использование определенных таблиц в БД
        а у меня уже есть таблица, которая совсем по другому организована:
        столбцы: роль(группа пользователей), модуль, чтение,редактирование, изменение, удаление
        к примеру есть 2 модуля: блог, форум – для них в таблице есть следующие записи:
        админ,блог,1,1,1,1
        админ,форум,1,1,1,1
        модератор,блог,0,0,0,0
        модератор,форум,1,1,1,1
        То есть иными словами, идея такая что не на каждое действие идет разрешение/запрет некоторых правил, а на весь модуль.
        Понятно что в каждом модуле придется прописывать это все в ручную. Но не могу понять просто как сделать так что бы: Пользователь зашел, в системе зарегистрировалось то к какой группе пользователей он относится, и соответственно открылся доступ к соответствующим модулям. Но при этом администратор свободно мог добавить новые роли(группы пользователей), и при этом менять код только в модулях.
        Ну вот как то так примерно.

        • Profi_net

          мне кажется можно обойтись и без RBAC, используя стандартный функционал:
          public function accessRules()
          {
          return array(
          array('deny',
          'actions'=>array('create', 'edit'),
          'users'=>array('?'),
          ),
          array('allow',
          'actions'=>array('delete'),
          'roles'=>array('admin'),
          ),
          array('deny',
          'actions'=>array('delete'),
          'users'=>array('*'),
          ),
          );
          }
          только функцию нужно изменить на выгрузку из базы:
          сделать чтобы следующий массив получал роли из БД:
          array('allow',
          'actions'=>array('delete'),
          'roles'=>array('admin'),
          иными словами получаем название модуля, затем подставляем название модуля в базу, получаем все строки связанные с данным модулем, и создаем на каждую роль массив с правами
          скажите правильно идет ход мыслей?

        • http://www.simplecoding.org Владимир

          Конечно, можно обойтись без RBAC и написать свою библиотеку. И получать массив с правами из базы тоже можно. Yii без разницы как вы формируете этот массив.

          Другой вопрос, что в вашем случае будет удобнее или быстрее, написать свою библиотеку или переделать систему под использование стандартной или воспользоваться сторонним решением вроде Zend_Auth + Zend_Acl?

          Но не могу понять просто как сделать так что бы: Пользователь зашел, в системе зарегистрировалось то к какой группе пользователей он относится, и соответственно открылся доступ к соответствующим модулям.

          Связать таблицу пользователей и таблицу с правами доступа (отношение "многие-ко-многим"). После этого можно получить все права для данного пользователя и сформировать массив.

        • Profi_net

          Спасибо за подробные комментарии!
          Скажите а как переопределить метод public function accessRules(), что бы во всех контроллерах использовался общий один на всех метод?

        • http://www.simplecoding.org Владимир

          Есть несколько вариантов.

          Первый.

          1) Создать еще один промежуточный контроллер.
          class MyController extends CController

          2) Определить в нём метод accessRules

          3) Все контроллеры приложения должны быть потомками MyController, а не
          CController.

          4) Не объявлять метод accessRules в контроллерах.

          Второй.

          1) Создаем свой класс с методом, который будет формировать массив с правилами.
          Назвать их можно как угодно, например, MyAccessManager::getRules()

          2) В контроллерах в методах accessRules пишем код
          return MyAccessManager::getRules();
          Этот метод должен правильно сформировать массив.

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

        • Profi_net

          в каждом контролере есть метод:
          public function accessRules()
          {
          return array(
          array('deny',
          'actions'=>array('create', 'edit'),
          'users'=>array('?'),
          ),
          array('allow',
          'actions'=>array('delete'),
          'roles'=>array('admin'),
          ),
          array('deny',
          'actions'=>array('delete'),
          'users'=>array('*'),
          ),
          );
          }
          у меня получается так что этот массив создается из базы данных, я хочу сделать так что бы при определении роли пользователю присваивались права на 'create', 'edit' к примеру, но получается так что при использовании в массиве 'roles' нужно использовать (RBAC) и (CPhpAuthManager) или (CDbAuthManager), но мне собственно это и ненужно так как вся система построена гораздо проще.

          То есть иными словами мы получаем значение переменной (к примеру роль 'admin') в зависимости от id пользователя, и потом в зависимости от роли в результирующем массиве который возвращает метод accessRules нужно дать те или иные права, как можно это сделать без создания ролей в базе или в файле?

          или еще можно объяснить это так:
          при регистрации загоняется $this->role значение роли пользователя
          потом в методе accessRules() мы проверяем если значение role 'admin' то даются права 'index','view', а если значение role 'user' то даются права 'admin','delete'

          Конечно как вариант можно использовать в массив 'users'=>array('admin'), с перечислением пользователей, но мне кажется что при большом количестве пользователей такой вариант не эффективен

          Как думаете можно что то здесь сделать, или все таки нужно использовать инструкции из описанного выше материала?

        • http://www.simplecoding.org Владимир

          Честно говоря, мне сложно ответить. Не совсем понял фразу "как вариант можно использовать в массив 'users'=>array('admin'), с перечислением пользователей".
          Зачем перечислять всех пользователей? Права ведь вы определяете не в зависимости от имени пользователя, а от его роли. Или у вас очень много ролей?

  • Roman

    Спасибо за статью! Но возник вопрос, каким образом можно вынести часть прав в контроллер (например в  accessRules) и проверять через user->checkAccess('deleteSomething')? Просто не хочется создавать большой массив с правами, зная что часть их необходима только в определённых контролерах.

    • http://www.simplecoding.org Владимир

      Посмотрите эту статью. Там есть пример создания собственного класса PhpAuthManager. В методе init этого класса загружается
      application.config.auth с правилами. Если вы хотите разнестиправила по разным файлам, то в методе init подключайте нужныефайлы. Выбор файла определяется названием контроллера и метода.

      • Roman

        Спасибо, эту статью читал. Но тем не менее не понял "Выбор файла определяется названием контроллера и метода.". Можно поподробнее? Или же пример

        • http://www.simplecoding.org Владимир

          Вы писали, что хотите поместить часть правил в контроллеры. Идея в том, чтобы создать по отдельному файлу с правилами для каждого из контроллеров и загружать нужный. Т.е. вместо
          $this->authFile=Yii::getPathOfAlias('application.config.auth').'.php';добавляете проверку контроллера и загружаете не auth.php, в котором находятся все правила, а файл с правилами именно для текущего контроллера.

  • Roms

    Что в статье ничего не сказано об применение данных из столбика u_state (статус (активен, заблокирован)), как его проверять в действиях?

    • http://www.simplecoding.org Владимир

      Действительно, пропустил. В файле components/UserIdentity.php есть строчка
      if(($user===null) or (md5($this->password)!==$user->u_pass)) {
      замените ее на
      if(($user===null) or (md5($this->password)!==$user->u_pass) or ($user->u_state !== Users::STATE_ACTIVE)) {

]]>
Tweet