Skip to content

Instantly share code, notes, and snippets.

@ramainen
Created January 21, 2012 15:06
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ramainen/1653015 to your computer and use it in GitHub Desktop.
Save ramainen/1653015 to your computer and use it in GitHub Desktop.
Конспект по RBAC и ACL

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

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

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

Для того, чтобы не держать в закладках уйму текста, было решено составить этот конспект. Если Вы случайно наткнулись на этот текст, и у Вас есть вопросы, мысли или предложения - пишите их в комментариях. Если нет регистрации тут, пишите на мой ящик ainu.sky@gmail.com.

Код вида 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. Ничего особенного. К тому же имеет собственную админку и кучу других вещей, что плохо в плане того, что устройство системы должно быть крайне прозрачным.

@idler
Copy link

idler commented Dec 27, 2012

http://wiki.limb-project.com/2011.1/doku.php?id=limb3:ru:packages:acl вот отличная реализация Role Based. Умеет все что нужно.

@crusat
Copy link

crusat commented Aug 19, 2013

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

Можно вполне себе легко. Здесь почитать можно http://habrahabr.ru/post/177873/

Copy link

ghost commented Oct 11, 2015

Вы плохо раскурили Yii. Точнее, вы забыли его поджечь.

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