- PVSM.RU - https://www.pvsm.ru -
TheRole [1] — гем для организации ролевого распределения на RoR сайте (с панелью управления [2])
Еще один (1001-ый) способ обеспечить разграничение прав в web-приложении. Концепт данного решения был довольно давно реализован на PHP, и позже был переписан на ruby. Ввиду простоты реализации описанный подход может быть применим в любом MVC фреймворке вроде Rails, Laravel и.т.д.
В тексте я попытался подробно раскрыть, не только техническую интеграцию решения в приложение, но и причины предлагаемой реализации.
В 2015 году, пожалуй, только сумасшедший может осмелиться написать очередной гем для распределения прав пользователей для Ruby on Rails. Ведь все давно используют CanCan, Pundit или в конце концов Acl9. Однако, не все так однозначно. Во-первых у меня есть несколько причин себя оправдать:
Во-вторых, ниже я не только подробно расскажу как организовать ролевое распределение в вашем RoR проекте, но и коснусь причин и истории возникновения данного решения. Я расскажу о задачах, которые я пытался решить, об особенностях гема, границах применимости, вариантах использования и (не без гордости) представлю вам одного из моих трудолюбивых и талантливых online-студентов напарников, без помощи которого релиз 3-ей версии гема не состоялся бы еще очень долго.
Перед тем, как начать растекаться мыслью по древу, в попытке подробно объяснить то, как вы можете использовать TheRole в своем проекте, я попробую сформулировать свое виденье данного инструмента.
TheRole — это система ролевого разграничения для RoR приложений, которая:
Несомненно, данное решение не является панацеей, имеет как ряд положительных, так и отрицательных сторон. Я попробую рассказать о том, что, на мой взгляд, является важным и мне будет интересно узнать ваше мнение на этот счет.
В 2006-2007 годах, когда я еще был студентом, мне впервые пришлось встретится с использованием PHP в коммерческих проектах. Увиденное, прямо скажем, так шокировало меня, что я не мог не начать искать путь приведения PHP кода в порядок. Недолгие поиски привели меня к MVC архитектуре. Поскольку приемлемого для себя решения в PHP я в тот момент не нашел, то, страсть к программированию и наличие свободного времени подтолкнули меня к самозабвенному написанию своего MVC фреймворка на PHP.
Новоиспеченный MVC велосипед я с успехом начал обкатывать на сайте школы, в которой работал учителем. Совсем скоро я задался вопросом ролевого распределения пользователей на сайте. Если коротко, то я не нашел такого решения, которое бы мне понравилось. А моя самопальная реализация MVC с controllers/actions в купе с желанием cделать админку для управления политиками ролевого доступа, подтолкнули к написанию еще одного PHP велосипеда, который сейчас превратился в TheRole.
В 2008 году я попал в капкан Ruby on Rails и все мои PHP эксперименты остались в прошлом. Однако, желание повторить одно из моих решений в ruby коде преследовало меня. Когда в 2011 я принялся реализовать в ruby мой почивший PHP концепт, мои коллеги не без причины улыбались, т.к. CanCan в тот момент был production-решением де-факто. И необходимости в пере-изобретении колеса просто не было. Но отказываться от своих мечт я не привык. Так, я начал неспешную работу по написанию этого гема.
Главная цель, которую я преследовал — создать решение по разграничению прав доступа с графическим интерфейсом.
Меня не устраивает «программистский подход» решения задачи, когда для разграничения доступа вам сначала нужно что-то запрограммировать (вроде класса Ability), а потом для изменения работы политик доступа (уже существующей и функционирующей системы) необходимо пригласить программиста, который что-то перепрограммирует (читай совершит ошибку) и передеплоит код на сервер.
Меня устраивает «пользовательский подход» решения задачи, когда администратор сайта, при минимальной подготовке, может самостоятельно изменить политику доступа к некоторым возможностям сайта через предоставленный интерфейс. (Хотя я не отрицаю, что вероятность совершения ошибки тут не меньше, но, как минимум, последствия возникшей ошибки не лежат на плечах программиста)
Исследуя принципы разграничения доступа в различных системах вы так или иначе придете приблизительно к следующему списку критериев доступа к исполнению операции:
Тут я остановлюсь, но, обозначу, что этот список можно смело умножить на 2, если брать не персональные критерии, а групповые. Но это уж совсем глубоко. Можно утонуть.
А теперь внимание! TheRole обеспечивает только первые 2 критерия доступности: Владение и Доступность операции (ACL). Остальное смело выкидываем.
TheRole обеспечивает работу только с 2-я критериями доступа: Владение объектом и Доступность операции. Однако, нужно признаться, что критерий Владение объектом, строго говоря — это не задача TheRole или любого другого гема для Авторизации. Никто не знает, у кого и как организованы взаимосвязи между объектами, и какие у вас признаки владения объектом. Универсального решения здесь создать просто невозможно.
Это значит, что метод проверки владения объектом owner?, доступный в TheRole из коробки, основывается на самом простом кейсе взаимосвязи между объектами и, вероятно, не для всех кейсов вашего приложения подойдет.
Все что делает метод owner?, так это пытается сопоставить ID данного пользователя, с полем USER_ID заданного объекта. Т.е. в следующем вызове:
@user.owner?(@page)
фактически будет проверено
@user.id == @page.user_id
Вот, собственно и всё.
На входе, в большинстве проектов и в большинстве кейсов такой метод сработает. Однако будьте готовы предпринять дополнительные меры, что бы owner? возвращал необходимый результат. Как это сделать — рассказано в документации к гему.
Если вы считаете, что было бы здорово создать систему распределения прав такого уровня, что бы она закрывала хотя бы те 5 кейсов, которые я обозначил в разделе «Какие критерии доступа существуют?», то я боюсь вас огорчить. Попытка создания таких систем для широкого применения такой же героический, насколько и бессмысленный поступок.
Именно поэтому TheRole решает только задачу обеспечения критерия Доступности операции и пытается дать первую зарисовку для проверки критерия Владения объектом. Наверняка в 99% случаев этого будет достаточно.
Нет, сухого объяснения не будет. Вы его найдете в Википедии [6]. Будет еще более сухое. ACL это просто хранилище правил доступа, над которым применима булева функция acl_check вида:
acl_check(@user, @action_name)
которая все что умеет, это возвращать true или false, красное или синее, добро или зло, ноль или единицу.
Как и прочие ACL системы, TheRole просто обеспечивает acl_check над хранилищем правил (я храню правила доступа в виде JSON строки в БД). Ничего особенного. Но, возможно вам будет интересно узнать, как TheRole организует хранение правил и почему именно так.
С самого начала я пришел к мысли, что если я хочу хранить в БД список контроля доступа и хочу обеспечить гибкое средство управление над этим списком, то хранить данные в виде сток таблицы не совсем удобно. Получение списка доступа, его построчное обновление и все прочие операции — будет весьма затратным делом (хотя бы даже с точки зрения графического интерфейса).
В свое время на PHP я обратил внимание на ассоциативные массивы, которые можно было легко превращать из объекта в строку и обратно. Формировать на клиенте такие массивы было легко, а на сервере, после отправки формы, массив правил был уже фактически готовым. Все что мне оставалось просто превратить его в строку и сохранить в базе. Рисовать на клиенте массивы правил и работать с ними на сервере оказалось крайне просто.
В PHP в свое время я использовал serialize/unserialize для работы с ассоциативными массивами. В ruby сейчас я использую JSON и хеши.
Началось все с очень простых списков доступа. Например, пользователь может создать пост, но не имеет доступа к панели управления комментариями (явно определено), и не может редактировать фотоальбомы (определено не явно, правила не существует, значит false).
{
post_create: true,
post_delete: true,
comments_panel: false
}
а вот модератор может создавать и удалять посты, получить доступ к панели управления комментариями, но редактировать фотоальбомы тоже не может
{
post_create: true,
post_delete: true,
comments_panel: true
}
Используя MVC реализацию ROR, и ежедневно видя двухуровневую структуру controller/action (хотя и до ROR код моих контроллеров был устроен аналогично), очень трудно не переложить 2х уровневую структуру controller/action на список контроля доступа. Соблазн настолько велик, что я не смог перед ним устоять. Так ACL в первых реализациях TheRole, кроме гибкого формата данных для хранения получила еще и 2х уровневую структуру.
Вот так может выглядеть роль пользователя, который может создать странницу, но почему-то доступ к редактированию страниц ему ограничили.
pages: {
index: true,
show: true,
new: true,
create: true,
edit: false,
update: false,
destroy: false
}
Сразу как только TheRole получила 2х уровневую структуру ACL стало крайне легко контролировать доступ к действиям контроллера в приложении. А это, как правило, одна из самых полезных и эффективных проверок в приложении. Теперь для такой проверки достаточно вызвать в before_filter метод проверки доступа, которому передать имя контроллера и имя действия.
return page_404 if not @user.has_role?(controller_name, action_name)
Итак. TheRole позволяет проверять право доступа пользователя к конкретному действию. Но если мы рассмотрим вопрос более внимательно, то мы заметим, что это всего лишь одна из проверок доступа, которыми должен быть наделен контроллер.
Первая проверка на доступ к действию контроллера выполняется вашим гемом Аутентификации. Например, Devise или Sorcery. Гем Devise это делает вот так:
before_action :authenticate_user!, except: [:index, :show]
Вторая проверка на доступ к действию контроллера должна выполняться только если мы уверены, что пользователь для проверки прав существует. Так, например, при попытке доступа к действию update первым сработает before_action :authenticate_user! и если этот фильтр пройден успешно (т.е пользователь существует), то вот тут можно уже передать полномочия гему TheRole:
before_action :role_required, except: [:index, :show]
role_required это метод, который внутри себя вызывает проверку вида current_user.has_role?(controller_name, action_name) и показывает страницу с ошибкой доступа, если пользователь не имеет должных прав.
Третья проверка на владение объектом. Не владея объектом, мы не можем получить доступ к действию контроллера, которое может этот объект изменить (удалить или отредактировать). Однако, мы не можем выполнить эту проверку, пока у нас нет объекта. Это значит, что сперва мы должны объект найти.
before_action :set_page, only: [:edit, :update, :destroy]
Мы видим, что поиск объекта выполняется на ограниченном кол-ве действий контроллера. Только для этих действий и имеет смысл проводить проверку владения через гем TheRole
before_action :owner_required, only: [:edit, :update, :destroy]
Нужно заметить, что из метода set_page нужно передать найденный объект в метод проверки владения owner_required. Это делается посредством метода for_ownership_check
В итоге, мы получаем следующий шаблон контроллера с довольно надежной системой ограничения доступа:
class PagesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :role_required, except: [:index, :show]
before_action :set_page, only: [:edit, :update, :destroy]
before_action :owner_required, only: [:edit, :update, :destroy]
# ... code ...
private
def set_page
@page = Page.find params[:id]
for_ownership_check(@page)
end
end
Представив ACL в виде 2х уровневого массива, где первый уровень обозначает секции (группы) правил, а второй — правила с соответствующими булевыми значениями, мне удалось довольно аккуратно интегрировать ролевую систему в контроллеры приложения. Но только контролем доступа к действиям контроллеров можно не ограничиваться.
Несмотря на то, что устройство ACL может очень точно отражать реальное устройство контроллера приложения, это не значит, что все секции и правила в ACL должны четко совпадать с устройством нашего приложения. Мы можем создать совершенно любые удобные нам группы правил в ACL и использовать их на свое усмотрение. Я называю такие группы правил виртуальными, исходя из того, что они не отражают реального устройства кода, а наделены лишь логическим смыслом.
Вот пример роли пользователя, которую можно использовать для контроля доступа к действиям контроллера и для контроля отображения социальных кнопок на странице.
pages: {
index: true,
show: true,
new: true,
create: true,
edit: true,
update: true,
destroy: true
},
social_buttons: {
vk: false,
twitter: true,
facebook: true
},
Прочитать такой список доступа довольно легко, если вы позаботитесь дать секциям и правилам внятные названия. Здесь я вижу, что пользователь обладающий этой ролью может выполнять любые базовые действия в контроллере Pages и кроме того, ему доступна работа с социальными кнопками Твиттера и Фэйсбука. А вот работать с кнопкой Вконтакте, пользователь почему-то не может.
Если с интеграцией TheRole в контроллер мы разобрались, то с интеграцией во View все еще проще:
- if current_user
- if current_user.has_role?(:social_buttons, :vk)
= link_to "Like with VK", "#"
- if current_user.has_role?(:social_buttons, :twitter)
= link_to "Like with TW", "#"
- if current_user.has_role?(:social_buttons, :facebook)
= link_to "Like with Fb", "#"
Я не мог не озадачится вопросом, как быстро и легко ввести в ролевую систему суперпользователя и модераторов. Я ввел в TheRole 2 виртуальные секции, имеющие особенное значение. В некотором роде это решение можно воспринимать как костыль, но я не вижу в нем ничего плохого. Оно не нарушает единства общей идеи.
Пользователь, обладающий в своем списке правил доступа секцией system и правилом administrator: true считается владельцем любых объектов и на запрос доступа всегда получает true.
system: {
administrator: true
}
Пользователь с секцией moderator будет получать true в ответ на все запросы к правилам секций, которые указаны в его наборе правил и равны true.
moderator: {
pages: true,
blogs: false,
twitter: true
}
т.е. на любые запросы вида user.has_role?(:pages, :blabla) и user.has_role?(:twitter, :blabla) данный пользователь будет всегда получать true. А вот запросы вида user.has_role?(:blogs, :blabla) будут давать такие разрешения, которые у данного пользователя прописаны в секции blogs. Т.е. при работе с блогами у данного пользователя нет никаких привилегий.
Теперь, суть функционирования гема в целом раскрыта, можно посмотреть на панель управления.
Панель управления реализована отдельным гемом и при большом желании может не устанавливаться в ваше приложение. Но в общем случае я думаю имеет смысл ее установить.
Панель управления обеспечивает:
Import/Export ролей может быть полезен если необходимо сделать backup ACL. Или, например, что бы перемещать настроенные роли между несколькими проектами использующими TheRole.
С одной стороны TheRole позволяет вам создавать какие угодно правила для использования в ваших проверках доступа и, если вы будете последовательными, то данные правила будут довольно семантично отражать происходящее в вашем приложении. С другой стороны у TheRole все еще много ограничений, о которых вы должны знать:
Ну, ок. Но хотя бы сделать так, что бы пользователь обладал несколькими ролями одновременно можно? Простите, но нет. Это порочный подход. Он связан с большим количеством логических ожиданий, которые могут быть совершенно разные у разных людей.
Если вам нужна система, которая обеспечивает множество ролей для одного пользователя, то это значит то, что или вы серьезно заблуждаетесь, или то, что TheRole вам катастрофически не подходит.
Прошлый 2014 год был крайне удачным для меня — под предлогом online обучения я познакомился и подружился с несколькими талантливыми людьми, страстно увлеченными программированием. География обширна — от Владивостока до Киева. И мне искренне радостно от того, что люди осознанно выбирают ruby технологии для решения своих задач и воплощения идей. Особенно приятно, что нам удается сделать что-то вместе в рамках open source проектов.
1) Xочу поблагодарить одного из этих людей, который приложил больше количество сил и старания для того что бы 3-й релиз гема состоялся. Этого человека зовут Илья Бондаренко [7], он живет в Перми и, насколько я знаю, работает тестировщиком. Я, прямо скажем, был приятно удивлен, как Илье удавалось быстро решать поставленные задачи и той степенью увлеченности, которую он проявил. В рамках нашей совместной работы Илья помог полностью переработать тесты, выполнить ряд важных преобразований в структуре кода, исправить пару важных багов, улучшить документаицю и даже внести ряд предложений в roadmap гема. Илья, не уверен, что этот отзыв будет тебе полезен с точки зрения карьеры, но все же, — я смело могу отрекомендовать тебя публике, как человека умеющего качественно выполнять поставленные задачи и добивающегося отличных результатов. Еще раз спасибо!
2) Кроме того, хочу поблагодарить Сергея Фуксмана [8]. По-видимому Сергей стал одним из первых пользователей, кто мигрировал с TheRole 2 на TheRole 3 и столкнулся с небольшими проблемами. Сергей, спасибо за ценный фидбэк и доверие к TheRole.
На этом я, кажется, рассказал обо всем, что хотел. Выводы о целесообразности и полезности решения делать вам. Но, как минимум вы теперь знаете еще один (1001-ый) вариант решения задачи Авторизации в проектах с MVC структурой.
Успехов в разработке!
Автор: zayko
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/83881
Ссылки в тексте:
[1] TheRole: https://github.com/the-teacher/the_role
[2] с панелью управления: https://github.com/TheRole/the_role_management_panel
[3] Image: http://badge.fury.io/rb/the_role
[4] Image: https://travis-ci.org/TheRole/DummyApp
[5] Image: https://codeclimate.com/github/TheRole/TheRoleApi
[6] в Википедии: https://ru.wikipedia.org/wiki/ACL
[7] Илья Бондаренко: https://github.com/sedx
[8] Сергея Фуксмана: https://github.com/fuksman
[9] Источник: http://habrahabr.ru/post/249951/
Нажмите здесь для печати.