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

Владимир | | PHP, Web разработка, Yii.

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 } ?>
…

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

Source

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

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