Skip to content

Instantly share code, notes, and snippets.

@slavcodev
Forked from ramainen/alc.md
Created January 23, 2013 17:59
Show Gist options
  • Save slavcodev/4611070 to your computer and use it in GitHub Desktop.
Save slavcodev/4611070 to your computer and use it in GitHub Desktop.

Конспект по RBAC и ACL

Данный текст - мой набросок после филтрации огромного количества текстов.

По работе стоит задача реализации системы распределения прав. Дабы не строить велосипед, решил посмотреть в CI, Yii, Zend, и так далее. В итоге выяснилось, что даже именитые библиотеки вроде Zend_Acl не решают даже части проблем.

Для того, чтобы не держать в закладках уйму текста, было решено составить этот конспект. Если Вы случайно наткнулись на этот текст, и у Вас есть вопросы, мысли или предложения - пишите их в комментариях. Если нет регистрации тут, пишите на мой ящик [email protected].

Код вида d()->User->permissions относится к разрабатываемому мною фреймворку, и написан на PHP (не руби).

Общие принципы

Крайне важно понимать, что роли и права - разные вещи. В системе, которую мы создаём, должны быть реализованы либо первые, либо вторые, но не одновременно. Рассмотрим два примера:

В нашей системе администратор форума может дать права разделу четырём пользователям - модераторам.

Вот так:

                  Чтение    Запись    Удаление постов     Загрузка картинок
Пользователь 1      да        да            да                  да
Пользователь 2      да        да            нет                 нет
Пользователь 3      да        да            да                  да
Пользователь 4      да        да            нет                 нет

Таким образом пользователи 1 и 3 получили по 4 роли, а 2 и 4 по 3 роли (роль читателя постов и писателя постов).

Другими словами, мы можем сделать так:

                    Роль
Пользователь 1      супермодератор
Пользователь 2      младший модератор
Пользователь 3      супермодератор
Пользователь 4      младший модератор

В таком случае, роль пользователя (группы) по отношениюк объекту будет одна и только одна - квинтэссенция прав пользователя.

В последнем случае система более логична, потому что в первом случае нельзя дать пользователю права на запись и не дать на чтение. В Yii и Zend это решили в лоб - роль Запись наследуется от роли Чтение и получает все её привелегии.

В последнем случае есть ещё одно преимущество - дав права редактора, мы автоматически даём права на чтение. Проблема в том, что это заставит писать немного лишнего кода.

Есть способ обойти это:

function can_write()
{
	if(!can_read()){
		return false;
	}
	if(user_moderator()){
		return true;
	}

}

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

Codeigniter + Zend

Суть

// проверка доступа к ресурсу 'dummy' залогиненым пользователем
if ($this->zacl->check_acl('dummy')){
	...
}

Это означает, что второй параметр функции check_acl необязательный (он принимает роль, например, admin). по умолчанию, используется залогиненый пользователь. и это хорошо.

Второй плюс статьи: структура таблиц:

CREATE TABLE IF NOT EXISTS `tbl_acl` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`type` enum('role','user') NOT NULL,
	`type_id` int(11) NOT NULL,
	`resource_id` int(11) NOT NULL,
	`action` enum('allow','deny') NOT NULL,
	PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT;
 
CREATE TABLE IF NOT EXISTS `tbl_aclresources` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`resource` varchar(255) NOT NULL,
	`description` longtext NOT NULL,
	`aclgroup` varchar(255) NOT NULL,
	`aclgrouporder` int(11) NOT NULL,
	`default_value` enum('true','false') NOT NULL,
	PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT;
 
CREATE TABLE IF NOT EXISTS `tbl_aclroles` (
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`name` varchar(255) NOT NULL,
	`roleorder` int(11) NOT NULL,
	PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT;
 
INSERT INTO `tbl_aclroles` (`id`, `name`, `roleorder`) VALUES
(1, 'Admin', 1),
(2, 'User', 2),
(3, 'Guest', 3);
 
CREATE TABLE IF NOT EXISTS `tbl_users` (
	`userid` int(11) NOT NULL AUTO_INCREMENT,
	`user` longtext NOT NULL,
	`pass` longtext NOT NULL,
	`firstname` longtext NOT NULL,
	`prefix` longtext NOT NULL,
	`lastname` longtext NOT NULL,
	`gender` enum('m','f') NOT NULL,
	`roleid` int(11) NOT NULL,
	`mail` longtext NOT NULL,
	PRIMARY KEY  (`userid`)
) ENGINE=MyISAM DEFAULT;

Но сама структура ужасна,т.к. это группы а не роли. групп должно быть безлимитно, роли захардкожены.

Ruby on Rails

Интересный проект для ROR, добавляет в контроллер возможность написать role_requirement "admin", если для данного контроллера нужна роль админа.

Настраивается; можно написать require_role "admin", :for_all_except => :index

Можно написать несколько ролей.

require_role "admin", :for_all_except => :index require_role "registered_user"

Добавляет в модель User метод has_role?, простейший метод, проверяющий есть ли роль в массиве ролей User.roles.

Подходит для простейших админок, схож с методом if(iam('admin'))

Не является полноценной системой ролей.

Глубокий и сложный проект, однако один из самых полезных. Полезные фрагменты:

user.has_no_role 'moderator', group
Model.accepts_role 'class moderator', user

user.is_eligible_for_what   --> returns array of authorizable objects for which user has role "eligible"
user.is_moderator_of? group --> returns true/false
user.is_moderator_of group  --> sets user to have role "moderator" for object group.
user.is_administrator       --> sets user to have role "administrator" not really tied to any object.

Есть связи с объектами. Это хорошо.

Есть мысль раздавать роли по аналогии с Zend (если параметр опущен, то он для всего, чем уже и точнее правило, тем выше его приоритет).

Например,

User('заказчик')->add_role('administrator');
User($object->creator)->add_role('administrator', $object);

В первом сучае объект не указан, поэтому пользователь получит права применительно ко всем объектам. Пробмема в том, что $object и 'заказчик' - захардкоженные объекты, а пользователей - админов и объектов вообще в системе могут быть тысячи.

Много ответов. Один из них базируется на 4 китах

Resources <- require -> (one or many) Permissions.
Roles <- are collections of -> (one or many) Permissions.
Users <- can have -> (one or many) Roles.

Таблицы:

permission
role
user
role_permission
user_role	

То же самое что и ранее. Ресурс поста может требовать разрешение на запись. Оно есть у ролей: Админ, Модератор, Хозяин_поста. Вопрос: в базе данных так и писать - хозяин поста? Таким образом, роль Хозяин_поста динамическая. Однако, тогда зачем нужна таблица role?

Вторая проблема - отсуствие групп. Они замещены Ролями. Я не могу создать группу Редакторы Нижний Новгород и дать им право на запись в раздел с ID=381 (Новости в Нижнем Новгороде).

Вывод - очень хорошо для админок. Гибко. Но не решает всех проблем, в т.ч. самых банальных.

А вот это то, что надо. Академично, гибко, полно.

Основная идея - объект Permission - право, связан с пользователем и ресурсом (полиморфически).

Сам объект тоже является полиморфным, то есть объект права на создание CreatePermission наследуется от Permission.

Таким образом:

class Membership has_many :create_permissions, :as => :resource

Группа имеет несколько прав на запись (ресурсов), несколько прав на чтение (ресурсов). Т.е. user->read_permissins[3]->title заголовок третьего поста, который разрешено править поьзователю.

Стоит заметить, что по-правильному, необходимо делать следующее: user->write_post_permissions - массив постов с разрешением на запись.

Обращаю внимание на то, что результат - не функция вроде user->can_write?($post_12), а полноценный массив.

В комментариях приведён интересный фрагмент:

class User
  def creatable_by?(creator)
	!creator.guest? && creator == user
  end

  def updatable_by?(updater, new)
	!updater.guest? && updater == user and same_fields?(new, :user)
  end

  def deletable_by?(deleter)
	!deleter.guest? && deleter == user
  end

  def viewable_by?(viewer, field)
	viewer == user || conference_sessions.count > 0
  end
end

Таким образом правила записываются в моделях, а вот это уже плохо. С другой стороны, добустим есть post, и на странице редактирвоания

function post_edit($id)
{
	d()->post = d()->Post($id);
	if(!d()->post->editable(d()->Auth->id))	{
		return 'Запрещено';
	}
}

Если следовать идее из первого столбца, а также из моего черновика ACL, результат может быть таким:

function post_edit($id)
{
	d()->post = d()->Post($id);
	if(!d()->Acl->post_editable(d()->post)) {
		return 'Запрещено';
	}
}

Другим вариантом может быть:

if(!d()->Can->edit_post(d()->post)) {

В свою очередь, функция edit_post выглядит так (самый дикий вариант):

function edit_post($object) {

	//Самый крутой случай
	if($object->permissions->where('user_id = ?',d()->Auth->id)->is_creator_role){
		return true;
	}
	
	if(d()->Auth->user->is_root){
		return true;
	}
	
	//В данном случае моддератор отвественнен только за один раздел. Частный случай
	if(!$object->category->moderators->where('user_id = ?',d()->Auth->id)->is_empty){
		return true;
	}
	
}

Данная проверка всегда привязывается к объекту. Поэтому можно сделать так:

d()->Comment(482)->can_edit

d()->Comment->can_edit_list - вот это уже на порядок-два сложнее.

Тут можно вставлять объект Acl через Dependency Injection

Второй хорошей мыслью была запись в комментариях

def creatable_by?(user)
	# Style one. Permissions are attributes attached to model.
	user.director? or user.manager?

	# Style two. Permissions are abstracted away from model.
	user.permissions.find_by_name('Can create stores')
end

Нам интересна вторая часть - хранение ролей в виде разрешений. Это ещё одна альтернативная точка зрения, не универсальная, но удобочитаемая.

Знаменитый CanCan от ryanb.

Альтернативный подход, но, наряду с предыдущим решением, справляется со своими задачами.

Сложно портировать ввиду того, что активно использует фишки ruby

can :read, Project, :category => { :visible => true }
can :read, Project, :priority => 1..3

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

can('read','project',array('user_id' => d()->Auth->id));

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

class Can
{
	function read_project($object)
	{
		if($object->user_id == d()->Auth->id){
			return true;
		}
		
		if($object->premissions->where('group_id in ?', d()->Auth->user->groups)->role == 'write'){
			return true;
		}
		
	}
}

С другой стороны, d()->Project(21)->can_read

class Project
{
	$this->can = new CanProject();
	
	function can_read()
	{
		if($this->user_id == d()->Auth->id){
			return true;
		}
		
		if($this->premissions->where('group_id in ?', d()->Auth->user->groups)->role == 'write'){
			return true;
		}
	}
}

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

d()->Can->read_project($project);
//или:
$project->can_read;

и при этом легко управлять правами.

Yii

Любопытная статья, но не решает всех проблем. Предполагает, что пользователи бывают N-го количества типов с захардкоженными правами. Соответственно каждый пользователь в БД записан жёстко - пользователь, админ, рут. И стальные права харжкдятся в стветствии с разрешениями.

Подход достаточно простой и удобный, но не позволяет дать пользователю админские права на определённый раздел. Если пользователь - админ, то он и останется админом.

В качестве фишки подразуемевается наследование ролей. Например, админ наследуется от пользователя и получает все его привилегии.

Также используется дикая штука bizRule. Содержит строку с PHP кодом. Ужасно.

Также возникает проблема, если надо например, топику назначить 5 модераторов.

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

Тоже самое. Говорится по большей мере об авторизации и регистрации. Банально трудно запретить пользователю редактировать чужие посты.

Пример:

if(Yii::app()->user->checkAccess('createPost'))
{
	// создаём запись
}

Те же яйца тольо в профиль. Хорошее объяснение что в себя что включает (Роли, Таски, Действия, Объекты). Та же самая проблема - невозможность задать модераторов или банально разрешить править собственные объекты. Зато хорошо для админок.

Вывод: RBAC в Yii не универсален. Можно использовать внедрением дополнительных полей, как в linux - 777 - права для всех, есть owner, есть рут, есть гость.

Назначить владельцами объекта 10 пользователей нельзя.

Просто библиотеки

Тоже самое, что Zend_Acl. Ничего особенного. К тому же имеет собственную админку и кучу других вещей, что плохо в плане того, что устройство системы должно быть крайне прозрачным.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment