TheRole 3. Авторизация для Ruby on Rails
TheRole — гем для организации ролевого распределения на RoR сайте (с панелью управления)
tl; drЕще один (1001-ый) способ обеспечить разграничение прав в web-приложении. Концепт данного решения был довольно давно реализован на PHP, и позже был переписан на ruby. Ввиду простоты реализации описанный подход может быть применим в любом MVC фреймворке вроде Rails, Laravel и.т.д. В тексте я попытался подробно раскрыть, не только техническую интеграцию решения в приложение, но и причины предлагаемой реализации.В 2015 году, пожалуй, только сумасшедший может осмелиться написать очередной гем для распределения прав пользователей для Ruby on Rails. Ведь все давно используют CanCan, Pundit или в конце концов Acl9. Однако, не все так однозначно. Во-первых у меня есть несколько причин себя оправдать:
То решение, о котором я сегодня вам расскажу появилось задолго до того, как я стал рубистом. Первый рабочий концепт был написан в 2006 году на PHP для школьного сайта К счастью для меня, тогда я не знал других подходов, а потому был полностью свободен в полете фантазии. Вряд ли меня обвинят в повторении чужой идеи, а свои выдумки всегда интересно притворять в жизнь Такие проекты как этот, для меня являются площадкой для работы с людьми, которых я беру на online обучение. И хотя бы уже этим TheRole привносит в рельсовый мир немного бобра Разработка любого гема дает обратную связь, что позволяет становиться чуть более лучше (я слишком долго жил в Иваново) Подход реализованный в TheRole может быть легко, просто и быстро повторен на PHP, JS, Python. Вдруг, и вам понравится этот вариант решения задачи авторизации для MVC, и вы повторите его в каком-нибудь Laravel. Как знать?! Во-вторых, ниже я не только подробно расскажу как организовать ролевое распределение в вашем RoR проекте, но и коснусь причин и истории возникновения данного решения. Я расскажу о задачах, которые я пытался решить, об особенностях гема, границах применимости, вариантах использования и (не без гордости) представлю вам одного из моих трудолюбивых и талантливых online-студентов напарников, без помощи которого релиз 3-ей версии гема не состоялся бы еще очень долго.Что такое TheRole? Перед тем, как начать растекаться мыслью по древу, в попытке подробно объяснить то, как вы можете использовать TheRole в своем проекте, я попробую сформулировать свое виденье данного инструмента.TheRole — это система ролевого разграничения для RoR приложений, которая:
предоставляет предопределенный путь разграничения доступа в проекте не требует дополнительного программирования в большинстве случаев имеет лаконичный способ интеграции в контроллеры для разграничения доступа к действиям Активно использует принцип «Соглашения vs. Конфигурации» Обладает GUI для управления списком доступа Достаточно надежна и хорошо поддерживаема, ввиду простоты реализации, небольшого кол-ва кода и приличного покрытия тестами под все основные сочетания ruby/rails/database Несомненно, данное решение не является панацеей, имеет как ряд положительных, так и отрицательных сторон. Я попробую рассказать о том, что, на мой взгляд, является важным и мне будет интересно узнать ваше мнение на этот счет.Немного истории В 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-решением де-факто. И необходимости в пере-изобретении колеса просто не было. Но отказываться от своих мечт я не привык. Так, я начал неспешную работу по написанию этого гема.
Зачем создан TheRole? Главная цель, которую я преследовал — создать решение по разграничению прав доступа с графическим интерфейсом.Меня не устраивает «программистский подход» решения задачи, когда для разграничения доступа вам сначала нужно что-то запрограммировать (вроде класса Ability), а потом для изменения работы политик доступа (уже существующей и функционирующей системы) необходимо пригласить программиста, который что-то перепрограммирует (читай совершит ошибку) и передеплоит код на сервер.
Меня устраивает «пользовательский подход» решения задачи, когда администратор сайта, при минимальной подготовке, может самостоятельно изменить политику доступа к некоторым возможностям сайта через предоставленный интерфейс. (Хотя я не отрицаю, что вероятность совершения ошибки тут не меньше, но, как минимум, последствия возникшей ошибки не лежат на плечах программиста)
Какие критерии доступа существуют? Исследуя принципы разграничения доступа в различных системах вы так или иначе придете приблизительно к следующему списку критериев доступа к исполнению операции: Владение — обычно мы хотим, что бы пользователь имел возможность исполнения важных операций только над теми объектами, которые ему принадлежат. Т.е. между пользователем и объектом есть некоторый признак принадлежности (владения). Что бы попасть в номер отеля вам нужно иметь ключ Доступность операции — как правило определяется через ACL.Т. е. в неком хранилище есть информация о том, может ли данный пользователь в принципе исполнять некоторое действие. В деканате есть список студентов, которым можно сдавать экзамен Время доступности операции — Само действие может быть доступно, но не сейчас. Студент может сдавать экзамены только во время сессии, хотя в принципе операция сдачи экзамена ему доступна. Доступность операции над объектом — аналог ACL, но здесь каждая операция привязана к конкретному объекту, и критерий владения тут уже не играет большой роли. В деканате есть список студентов, которым можно сдавать конкретные предметы Доступность операции над объектом с ограничением по времени — Просто пример, в деканате есть список студентов, которым можно пересдавать некоторые предметы (каждому свой), но только в конкретные дни (для каждого свой) Тут я остановлюсь, но, обозначу, что этот список можно смело умножить на 2, если брать не персональные критерии, а групповые. Но это уж совсем глубоко. Можно утонуть.А теперь внимание! TheRole обеспечивает только первые 2 критерия доступности: Владение и Доступность операции (ACL). Остальное смело выкидываем.
1,5 критерия доступа. Определение владения объектом TheRole обеспечивает работу только с 2-я критериями доступа: Владение объектом и Доступность операции. Однако, нужно признаться, что критерий Владение объектом, строго говоря — это не задача TheRole или любого другого гема для Авторизации. Никто не знает, у кого и как организованы взаимосвязи между объектами, и какие у вас признаки владения объектом. Универсального решения здесь создать просто невозможно.Это значит, что метод проверки владения объектом owner?, доступный в TheRole из коробки, основывается на самом простом кейсе взаимосвязи между объектами и, вероятно, не для всех кейсов вашего приложения подойдет.
Все что делает метод owner?, так это пытается сопоставить ID данного пользователя, с полем USER_ID заданного объекта. Т.е. в следующем вызове:
@user.owner?(@page) фактически будет проверено @user.id == @page.user_id Вот, собственно и всё.На входе, в большинстве проектов и в большинстве кейсов такой метод сработает. Однако будьте готовы предпринять дополнительные меры, что бы owner? возвращал необходимый результат. Как это сделать — рассказано в документации к гему.
Почему только 1,5 критерия проверки доступа? Если вы считаете, что было бы здорово создать систему распределения прав такого уровня, что бы она закрывала хотя бы те 5 кейсов, которые я обозначил в разделе «Какие критерии доступа существуют?», то я боюсь вас огорчить. Попытка создания таких систем для широкого применения такой же героический, насколько и бессмысленный поступок.Во-первых, такие вещи практически никому не нужны и реального выхода в свет такой комбайн вряд ли когда-либо получит. Во-вторых, такие системы распределения прав связаны с огромным количеством кейсов, которые необходимо покрыть тестами. И это довольно трудоемко. В-третьих, Для такой системы крайне трудно придумать доступный интерфейс, который бы позволил конечному пользователю осознанно контролировать систему. В-четвертых, я убежден, что конечный пользователь не станет использовать все те возможности, которые предоставит данное решение, даже если оно будет завершено. Именно поэтому TheRole решает только задачу обеспечения критерия Доступности операции и пытается дать первую зарисовку для проверки критерия Владения объектом. Наверняка в 99% случаев этого будет достаточно.Что такое ACL? Нет, сухого объяснения не будет. Вы его найдете в Википедии. Будет еще более сухое. ACL это просто хранилище правил доступа, над которым применима булева функция acl_check вида: acl_check (@ user, @action_name) которая все что умеет, это возвращать true или false, красное или синее, добро или зло, ноль или единицу.Как и прочие ACL системы, TheRole просто обеспечивает acl_check над хранилищем правил (я храню правила доступа в виде JSON строки в БД). Ничего особенного. Но, возможно вам будет интересно узнать, как TheRole организует хранение правил и почему именно так.
Гибкая структура данных для хранения ACL С самого начала я пришел к мысли, что если я хочу хранить в БД список контроля доступа и хочу обеспечить гибкое средство управление над этим списком, то хранить данные в виде сток таблицы не совсем удобно. Получение списка доступа, его построчное обновление и все прочие операции — будет весьма затратным делом (хотя бы даже с точки зрения графического интерфейса).В свое время на 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 & ACL. Каждый видит то, что хочет видеть Используя 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»,»#» Специальные виртуальные секции: system и moderator Я не мог не озадачится вопросом, как быстро и легко ввести в ролевую систему суперпользователя и модераторов. Я ввел в 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. Т.е. при работе с блогами у данного пользователя нет никаких привилегий.Панель управления ACL Теперь, суть функционирования гема в целом раскрыта, можно посмотреть на панель управления.TheRole Management Panel Панель управления реализована отдельным гемом и при большом желании может не устанавливаться в ваше приложение. Но в общем случае я думаю имеет смысл ее установить.Панель управления обеспечивает:
Создание новых пустых ролей Создание новых ролей на основе уже существующих Редактирование информации о данной роли Создание и удаление новых секций внутри заданной роли Создание и удаление новых правил внутри заданной секции роли Выгрузка одной или всех ролей системы в JSON файл Загрузка JSON файла с ролями Import/Export ролей может быть полезен если необходимо сделать backup ACL. Или, например, что бы перемещать настроенные роли между несколькими проектами использующими TheRole.Гибкость с ограничениями С одной стороны TheRole позволяет вам создавать какие угодно правила для использования в ваших проверках доступа и, если вы будете последовательными, то данные правила будут довольно семантично отражать происходящее в вашем приложении. С другой стороны у TheRole все еще много ограничений, о которых вы должны знать: Вы можете использовать только предопределенный путь проверки доступа, через ограниченный API (и я верю в то, что это хорошо) Пользователь обладает только одной ролью (это моя принципиальная позиция и неизменяемая техническая реализация) TheRole работает только с моделью User. (Вопрос времени и активного контрибьютора) TheRole работает только с хелпером current_user. (Вопрос времени и активного контрибьютора) Не поддерживает mongo. (Вопрос времени и активного контрибьютора) Поддерживает только 3 SQL-like БД (sqlite, mysql, psql) Не предполагает использование native json столбцов БД и хранит JSON в базе исключительно как текст. (хотя да, есть патч для PSQL, но в стандартную поставку гема я включать его не буду, ввиду стремления не засорять код specific поведением) Ну, ок. Но хотя бы сделать так, что бы пользователь обладал несколькими ролями одновременно можно? Простите, но нет. Это порочный подход. Он связан с большим количеством логических ожиданий, которые могут быть совершенно разные у разных людей.Если вам нужна система, которая обеспечивает множество ролей для одного пользователя, то это значит то, что или вы серьезно заблуждаетесь, или то, что TheRole вам катастрофически не подходит.
Благодарности Прошлый 2014 год был крайне удачным для меня — под предлогом online обучения я познакомился и подружился с несколькими талантливыми людьми, страстно увлеченными программированием. География обширна — от Владивостока до Киева. И мне искренне радостно от того, что люди осознанно выбирают ruby технологии для решения своих задач и воплощения идей. Особенно приятно, что нам удается сделать что-то вместе в рамках open source проектов. 1) Xочу поблагодарить одного из этих людей, который приложил больше количество сил и старания для того что бы 3-й релиз гема состоялся. Этого человека зовут Илья Бондаренко, он живет в Перми и, насколько я знаю, работает тестировщиком. Я, прямо скажем, был приятно удивлен, как Илье удавалось быстро решать поставленные задачи и той степенью увлеченности, которую он проявил. В рамках нашей совместной работы Илья помог полностью переработать тесты, выполнить ряд важных преобразований в структуре кода, исправить пару важных багов, улучшить документаицю и даже внести ряд предложений в roadmap гема. Илья, не уверен, что этот отзыв будет тебе полезен с точки зрения карьеры, но все же, — я смело могу отрекомендовать тебя публике, как человека умеющего качественно выполнять поставленные задачи и добивающегося отличных результатов. Еще раз спасибо!
2) Кроме того, хочу поблагодарить Сергея Фуксмана. По-видимому Сергей стал одним из первых пользователей, кто мигрировал с TheRole 2 на TheRole 3 и столкнулся с небольшими проблемами. Сергей, спасибо за ценный фидбэк и доверие к TheRole.
Завершение На этом я, кажется, рассказал обо всем, что хотел. Выводы о целесообразности и полезности решения делать вам. Но, как минимум вы теперь знаете еще один (1001-ый) вариант решения задачи Авторизации в проектах с MVC структурой.Успехов в разработке!