О распределении прав в Web приложениях для Rails разработчиков. Часть 2.
Подготовленно спецаильно для питерского сообщества Saint P Ruby Community https://t.me/saintprug
В предыдущем посте мы узнали, что такое проверка на владение и о ее важности для системы распределения прав. Узнали об ACL и реализовали простейшую ролевую систему для нашего простейшего проекта.
В этой части вы узнаете о способах хранения ACL, особенностях их использования и некоторых лучших практиках.
Напомню, что наше текущее решение работает только для простейшей системы Users → Posts
В нашей системе только один контроллер Posts. Чтобы эффективно управлять доступом к действиям этого контроллера (index, show, create, edit, update, delete) мы можем использовать простейшие списки доступа приведенные ниже.
Вводим новый контроллер
Логично предположить, что в нашей системе будет не только контроллер Posts, но и, например, контроллер Users. Ведь мы хотим чтобы наши пользователи смогли загружать аватарки, менять пароль, менять email и выполнять много других действий со стоим профилем.
Как только мы задумываемся об этом — мы сразу понимаем, что наша система распределения прав перестает работать. Ведь при проверке того, может ли пользователь выполнять некоторое действие в контроллере мы учитываем только имя действия, но не учитываем имя контроллера в котором это действие выполняется.
Например, мы хотим, чтобы пользователь мог удалять свои посты, но не мог удалить свой аккаунт.
Из следующего набора правил совершенно не понятно действие в каком контроллере мы собираемся проверять. Если мы будем проверять только имя действия, то из текущего массива правил мы сделаем вывод, что пользователь может удалять и посты и свой аккаунт. А, как мы договорились, это не так.
Логично предположить, что наши списки контроля доступа должны иметь возможность более точно определять имя желаемого действия. Например, попробуем сделать следующее.
Сможете ли вы, глядя на данный массив разрешений сказать, может ли продвинутый пользователь удалить свой пост или аккаунт? Уверен, что да.
Пользователи с данной ролью (читай массивом разрешений) могут удалять посты, но не могут удалять пользователей, в частности свой аккаунт.
Изменим наши методы проверки таким образом, чтобы они соответствовали структуре данного массива разрешений.
Обратите внимание на стороку
action = [where, what].join(‘_’)
Здесь мы конструируем имя проверяемого действия из названия контроллера (where — где) и проверяемого действия (what — что).
Так, если пользователь пытается выполнить действие edit в контроллере users, то в качестве полного имени действия мы получим users_edit.
Ну, и в контроллере мы должны немного исправить наш метод проверки. Теперь он будет выглядеть вот так.
Только что мы с вами узнали, что для обеспечения качественной проверки на исполнение операции нам нужно знать следующее.
- КТО? Кто пытается выполнить операцию и является ли он владельцем объекта
- ГДЕ? Где производится попытка выполнить действие
- ЧТО? Какое действие он пытается выполнить
Ассоциативные массивы и распределение прав
Вероятно, вы уже заметили, что для простоты изложения материала я использовал в качестве ACL хеши или как их еще называют ассоциативные массивы.
Ассоциативный массив представляет собой классическую структуру данных вида ключ → значение.
- К качестве ключа я использую имя проверяемого действия.
- В качестве значения булево значение true (доступ есть) и false (доступа нет).
- Любое не определенное в хеше правило должно обозначать false.
Такие структуры, как ассоциативные массивы присутствуют в абсолютном большинстве современных языков программирования.
Хеши НАГЛЯДНЫ, ПРОСТЫ, СИМАНТИЧНЫ, ЛЕГКО ЧИТАЮТСЯ, ЛЕГКО ПОДДЕРЖИВАЮТСЯ И ИЗМЕНЯЮТСЯ. В некотором смысле, это идеальная структура для хранения списков доступа.
Хеши очень удобно использовать для организации простых, легко читаемых и, главное, легко поддерживаемых систем распределения прав для небольших и средних проектов.
Однако, как правило, вы не встретите в реальном проекте подобных решений. Почему?
Бизнес и Ролевые системы. Ожидание и реальность
Обычно, начиная разговор о реализации системы распределения прав в веб проекте, руководители компаний (или старший менеджмент) мечтают получить некоторую гибкость в настройке правовых политик. В их представлении ролевая система должна обязательно иметь некоторый графический интерфейс с чекбоксами или выпадающими списками, которые будут использоваться для назначения пользователям необходимых прав.
Для реализации подобного рода задач все ACL переносятся в базу данных, где может существовать таблица с правилами доступа (Permissions). Права крепятся к категоризирующей сущности Role, которая уже в свою очередь прикрепляется к пользователю.
В нашей схеме:
- Пользователь может иметь одну роль.
- Каждая роль содержит в себе много правил доступа (Permissions).
- Каждое правило отвечает за отдельно взятое действие в системе. Правило выражается булевым значением.
В данном случае, как и прежде, да и в остальных случаях, роль это всего лишь эфемерная сущность, которая существует только для того, чтобы категоризировать или группировать правила доступа.
В желании бизнеса иметь графически настраиваемую систему распределения прав нет ничего необычного. Для конечного пользователя с системой распределения прав такой способ работы вполне естественен и логичен. Ниже вы увидите первый попавшийся мне веб интерфейс подобной системы. Кажется, WordPress.
Как и в моих оригинальных примерах с хешами, мы видим, что правила доступа каким-то образом группируются. Например, секция Posts имеет правила edit, create, edit others, publish и прочее. Каждое правило имеет булево значение true или false.
Как можно заметить — при совершенно различных реализациях все системы работают практически идентично.
Не смотря на кажущуюся удобность использования ролевых систем с графическим интерфейсом я вынужден обозначить их фатальные недостатки. Особенно с точки зрения руководителя разработки или владельца бизнеса.
На основе моего опыта работы в нескольких довольно крупных компаниях, которые использовали подобные системы я могу поделиться с вами следующими наблюдениями.
- Сложность разработки и внедрения UI для системы распределения прав, как правило, несопоставимо выше по сравнению с пользой от существования такой системы.
- UI, как правило, перегружен, нелогичен и уникален для каждого отдельного проекта. Обучение нового сотрудника выполнению осознанных и корректных действий в этом UI приносит компании только убытки. Ошибка настройщика прав может стоить компании намного больше, чем сомнительное удовольствие обладания таким UI.
- Настройка прав выполняется только в момент запуска проекта и практически не используется в дальнейшем. Что вновь обесценивает затраты на создание подобных интерфейсов. Временные и финансовые затраты никогда не окупаются, а поддержка ролевой системы становится болью и для бизнеса и для разработчиков.
Как ответственный и квалифицированный инженер, перед тем как приступить к реализации UI для любой системы распределения прав, вы обязаны подробно и аргументированно донести до своего клиента, что создание подобного рода интерфейсов это крайне затратное и практически не окупаемое с точки зрения бизнеса мероприятие.
Хранение ACL в базах данных. Обязательный кеш
Напомню вид нашей схемы для хранения ролей и правил доступа.
С точки зрения программиста одна единственная проверка на доступ к действию будет выглядеть примерно так.
- Выбрать Роль для данного пользователя
- Выбрать для данной Роли Правило доступа по заданному имени Правила.
Для проверки единственного правила доступа к базе данных будет произведено 2 запроса.
Если для формирования страницы потребуется проверить 20–30 правил доступа, то к базе данных мы обратимся 40–60 раз. Что, конечно неприемлемо.
Именно поэтому для Роли обычно выбирают сразу все доступные правила и кешируют их на всем протяжении текущего процесса построения страницы. К счастью в Rails это делается без дополнительных усилий.
Выборка и Кеширование всех правил доступа для данной Роли в момент первой проверки любого из правил является обязательным требованием построения ролевой системы, при хранения ACL в классической базе данных.
Вычисляемые правила доступа. Типа “Rails Way”
Когда разработчики не вынуждены идти на поводу у бизнеса и не обязаны хранить правила доступа в базе данных, то в большинстве случаев (не только в Rails приложениях) вместо крайне удобного и дешевого способа хранения ACL в Hash-образной структуре, разработчики используют вычисляемые правила доступа.
Что такое вычисляемые правила доступа?
Вычисляемые правила доступа — это функционально-ориентированный способ формирования правил владения и доступа к объектам.
Чо? Чо? Чо ты сказал? А ну повтори!
Грубо говоря. Вместо пары permission_name → permission_value, которая хранится в хеше, rails программисты используют функцию, которая возвращает булево значение для нужного случая.
Этот способ ни хорош, ни плох. Он просто один из многих. Посмотрим как это работает.
Классическими примерами такого способа организации ACL является супер популярный гем CanCan и не менее известный Pundit.
Вот так выглядит описание политик доступа в геме CanCan.
А вот так определяются политики доступа в геме Pundit.
Давайте совсем обнаглеем и посмотрим на еще один пример — гем kanrb от Davydov Anton
Думаю, некоторые читатели спросят, а где же здесь список действий и ассоциированных с ними разрешений, как это вы видели на примере хешей?
Внимательно изучив код вы можете найти там и упоминание действий вида edit, show, update, delete и правила вычисления владения объектом user.id == post.author_id или { author_id: user.id }
Так или иначе каждый программный фрагмент вычисляет правило доступа. Не всегда функции вычисления правил очевидны и привычны, но фактически они делают абсолютно туже работу, что и выполняли ACL хеши в моих простых примерах.
Почему разработчики используют вычисляемые правила?
Почему разработчики используют вычисляемые правила?В экосистеме Rails для этого есть ряд объективных причин. Я вижу следующие.
- Естественное желание программистов описывать вещи привычным им способом. В частности — функциями.
- Использование данного способа в самом первом и по сей день крайне популярном геме CanCan.
Гем CanCan в экосистеме Rails был первым из тех, кто предлагал относительно простое решение для обеспечения распределения прав в системе. По праву перворождения и благодаря медийности автора он стал главным решением в данной области на многие годы.
Не удивительно, что большинство разработчиков, которые так или иначе сталкивались с подобными задачами перенимали опыт организации распределения прав через описание некоторых программных объектов Ability или AccessPolicy и в дальнейшем или привыкали использовать CanCan или создавали свои аналоги с использованием аналогичных подходов.
Насколько объектный и функциональный подходы описания правил доступа подходят для описания ACL я не берусь судить. Но я, откровенно говоря, никогда не был от этого в восторге. Хотя понимая базовые принципы систем распределения прав мне всегда было не сложно понять суть стороннего решения и начать его эффективно использовать.
Подведем промежуточные итоги.
В этой статье вы узнали что:
- Для эффективного распределения прав вы должны знать не только имя действия, которое хочет выполнить пользователь, но и то, где пользователь собирается выполнить это действие.
- ACL может храниться в системе в виде статичного ассоциативного массива и обеспечивать гибкую, поддерживаемую, симантичную и легко модифицируемую систему распределения прав.
- ACL может храниться в базе данных и быть обеспечена графическим интерфейсом управления правами. Обычно для бизнеса это значит огромные затраты на сопровождение и поддержку, а вам, как разработчику, стоит несколько опасаться таких проектов.
- При хранении ACL в базе данных, выборка и кеширование всех правил доступа для текущей Роли — must have. Трудно себе представить систему, в которой этого бы не потребовалось.
- ACL может быть описана в виде функций. Часто это не очень удобный для новичка, но крайне популярный среди разработчиков способ.
А пока я буду вам признателен за любой, а особенно за жесткий, негативный и агрессивный фидбэк. Ведь именно он делает нас лучше.
Я на телеге: https://t.me/iam_the_teacher
Присоединиться к крутым рубистам:
Saint P Ruby Community: https://t.me/saintprug