О распределении прав в Web приложениях для Rails разработчиков. Часть 1.

Ilya Zykin
10 min readNov 30, 2019

--

Подготовленно спецаильно для питерского сообщества Saint P Ruby Community https://t.me/saintprug

Тварь ли я дрожащая или право имею?

Именно на этот вопрос, который мучит так много web разработчиков сегодня я попытаюсь ответить. Ну, как ответить. Скорее порассуждать, поскольку упарывался я на эту тему довольно плотно, и было бы не плохо рассказать вам, мои зайчики, к каким выводам я пришел.

В этом посте вы узнаете

  1. О проверке владения объектом и ее важности при реализации системы распределения прав.
  2. О Списках Контроля Доступа (Access Control List, ACL) и их использовании для организации системы распределения прав.
  3. О check-методе использующем ACL, и простейшей ролевой системе.

Будьте реалистами — делайте невозможное.

И так, вы задумали стать богатым и знаменитым. Что для этого нужно сделать? Скорее всего нужно сделать какой-то супер популярный и массовый продукт, который нужен всем и за который вам заплатят миллионы долларов. Изучив рынок вы понимаете, что самый популярный web движок, который существует на планете это WordPress. Движок для блого-постинга. Даже если ваш продукт не станете таким же популярным как WordPress, то отхватив долю рынка, вы все равно станете довольно состоятельным человеком. Что может быть проще? Простая идея — простая реализация. Деньги, слава, алкоголь, наркотики, половые излишества. Все как вы любите. План кристально чист. Начинаем!

Нет системы распределения прав — нет проблем.

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

Допустим, в системе несколько пользователей. Каждый Пользователь может создать несколько Постов.

Представим, что в нашей системе появилось несколько Пользователей и Постов.

Один из пользователей это Админ. Он ленивый и не создает никаких публикаций. Активный пользователь Alex создал 2 Поста. Второй пользователь Bob создал только один пост. Мы это узнали по полю в базе данных user_id, которое однозначно связывает автора Поста и сам Пост.

Я давно не писал код на Ruby on Rails, но помню, что обычно Rails контроллеры выглядят как-то так.

https://gist.github.com/the-teacher/05a7f85237c756c4560017d31491597e

Алекс хочет отредактировать публикацию Боба

Допустим мы выполнили вход в систему как Алекс (User.id =2)

Если Алекс попробует каким-то образом обновить пост Боба,
(пост с id=3 и user_id=3) то, скорее всего, в Rails приложении случится неявная проверка на владение объектом. Как мы видим из кода метода udpate для обновления мы попробуем найти пост с id=3 для текущего пользователя current_user.posts.find(3). Но у пользователя Алекс с id=2 нет прикрепленных к нему публикаций удовлетворяющих такому условию. Код упадет, вызовет исключение и пользователь увидит страницу 404.

Казалось бы, все под контролем. Однако.

Админ я тут или что?

В любой системе должен быть привилегированный пользователь. Если кто-то из пользователей сайта напишет что-то запрещенное законодательством страны, где находится веб сайт, то придет какой-нибудь тупорылый УругвайКомПозор и в лучшем случае потребует просто отредактировать неугодную публикацию.

Если администратор попробует отредактировать Пост через стандартные средства сайта, то он обнаружит, что к его учетке не прикреплено необходимого поста. current_user.posts.find(3) к этому пользователю вообще не прикреплено ни одного поста. Следовательно, он вообще ничего не может отредактировать.

Чтобы исправить эту ситуацию нам нужно открепить посты от пользователей и позволить всем редактировать их.

Мы изменим код следующим образом. Заменим

На

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

Промежуточный итог

Мы узнали о том, что пользователи могут владеть объектами. Быть их собственниками и распоряжаться их судьбой как угодно. Владение, в простейшем случае, обеспечивается через связующее поле user_id.

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

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

Слова для запоминания:

  1. Владение объектом
  2. Владелец объекта

Чиним одно, ломаем другое. True Development!

На данный момент мы убрали из нашего кода неявную проверку на владение объектом, которая обычно присутствует к коде Rails приложения. Убрав ее мы позволили любому пользователю получить доступ к обновлению объекта. И администратор системы может редактировать посты пользователей и всякий пользователь может отредактировать пост другого пользователя.

Очевидно, что убрав неявную проверку на владение, мы обязаны обеспечить явную проверку на владение объектом, которая ко всему прочему должна удовлетворять нашим требованиям.

Требования следующие.

  1. Только автор поста должен иметь возможность обновлять его.
  2. Кроме автора поста только админ может обновить пост.

Во-первых определимся, какой пользователь является админом. В простейшем случае будем считать, что это пользователь с ID=1

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

user.owner?(post)

Реализовать такой метод очень просто. Конечно, мы будем опираться на систему соглашений в Rails и считать, что признак владения это поле user_id в проверяемом объекте.

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

Полный код нашего воображаемого класса User выглядит вот так

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

Напомню, что current_user здесь это текущий зарегистрированный в системе пользователь. Фактически инстанс класса User.

Из кода мы видим, что всякий пользователь может сделать поиск объекта по параметру id и найти его. Затем в блоке unless; end мы проверяем, что текущий пользователь, который пытается обновить объект является собственником данного объекта.

Собственником объекта, как мы ранее договорились, являются — создатель объекта и админ системы.

А как устанавливается поле user_id?

Если вы вдруг не очень знакомы с Rails, то стоит пояснить, что поле user_id в объектах класса Post устанавливается автоматически в момент создания объекта. Этот идентификатор будет равен ID пользователя, который зарегистрирован в системе на момент создания нового поста и выполняет действие по созданию этого объекта. Подробности этого процесса на данный момент мы упускаем и принимаем происходящее как есть.

Грязный контроллер!

За мою лобовую проверку внутри метода update большинство разработчиков меня бы прокляли. Так, конечно, делать не следует.

Подчистим контроллер сделаем его тоньше. Для этого используем before_action

before_action это метод, который определяется в начале контроллера и выполняется автоматически перед исполнением кода некоторого метода, имя (имена) которого указано как параметр.

Из кода ниже вы можете видеть, что перед методом update сперва будет отрабатывать метод find_post, который определяет объект над которым будут производить действие edit, update или delete.

Следующий before_action будет производить проверку на владение пользователем некоторого объекта и если пользователь не является админом или создателем поста, то его переадресуют на корневую страницу с соответствующим сообщением и не дадут выполнить недозволенное действие.

Кроме того, мы можем обратить внимание как элегантно стал выглядеть метод update

ACL. Списки контроля доступа

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

В нашей системе, если ты владеешь объектом, то ты можешь отредактировать или удалить его. Администратор считается владельцем всех объектов в системе и может выполнять любые действия над любым из них.

Задача на разграничение действий

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

А вот Боб будет у нас пользователем, который может создавать, обновлять и даже удалять свои объекты из нашей системы.

Для подобного рода вещей в проектировании информационных систем до сих пор не придумали ничего более простого и логичного, чем Списки Контроля Доступа или Access Control List.

Давайте вспомним какие базовые действия может выполнять пользователь в типичном Rails приложении

index, show, create, edit, update, delete

Для каждого пользователя сформируем полный список этих действий и булевыми значениями определим есть ли у пользователя доступ.

Для Алекса имеем

Обратите внимание, что удаление deleted для Алекса запрещено. Имеет значение false. Далее Алексу будет запрещено удалять его посты.

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

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

Имея список разрешений создадим простейший Ruby класс, который нам позволит проверять права пользователя на выполнения некоторых действий.

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

В самом начел контроллера я выполню проверку доступа пользователя к запрашиваемому действию.

Почему именно так?

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

В следующем before_action с именем find_post мы выполняем обращение к базе данных. Это относительная дорогая операция. Если наш пользователь вообще не имеет права выполнять некоторое действие для которого должен быть выполнен метод find_post, то зачем нам зря расходовать ресурсы нашей системы и делать ненужный запрос? Именно поэтому check_permissions в данном случае выполняется перед любыми другими методами.

Посмотрим на вторую часть нашего контроллера и простейшую реализацию нашей правовой системы.

Все что мы делаем здесь — проверяем, что текущий пользователь current_user пользователь имеет доступ к текущему действию self.action_name контроллера.

ВАЖНО

Access Control List, Списки Контроля Доступа — некоторая структура, содержащая в себе перечень именованных действий и ассоциированных с ними разрешений (например, булевых значений).

Над ACL должен быть реализован метод проверки (check-метод) , который использует ACL как источник данных для принятия решения о доступе или не доступе субъекта к выполнению действий над объектом.

В данном контексте: Cубъект — тот кто выполняет действия. Объект — то над чем выполняют действия.

Предполагается, что check-метод это булевый метод, который всегда возвращает однозначные true или false.

Обобщаем частное решение

Из-за того, что рассматриваемая нами система крайне проста мы позволяем себе массу условностей. В частности, мы определяли права доступа только для Алекса и Боба.

Однако, не только Алекс и Боб должны будут пользоваться этим определением разрешений на выполнение действий. Обобщим наш подход.

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

Следовательно и метод проверки прав у нас изменится.

Откровенно говоря в методе check_permission? я сделал некоторую ошибку. Предполагается, что данный метод булевый и должен возвращать только true или false. Я уже вижу, что при некоторых условиях он . может возвращать также и nil, что не является большой проблемой, но все же является нежелательным поведением.

Для того чтобы исключить неожиданное поведение — данный метод должен быть хорошенько протестирован. Вообще, все что связано с распределением прав ОБЯЗАНО быть тщательно протестировано, перед началом использования в качестве production решения.

Сами того не желая мы внезапно столкнулись с определением ролей в нашей системе. Сейчас в нашей системе есть 3 роли. REGULAR_USERS, ADVANCED_USERS, и пользователи по-умолчанию.

Как мы можем видеть — роли это лишь синоним ACL. Фактически по словом роль пользователя мы подразумеваем некоторый набор разрешений для заданного пользователя.

На данный момент мне кажется, что ничего сложного я вам не рассказал. И вам наверняка понятен основной ход моей мысли.

В следующей раз я хотел бы рассказать вам про то:

  1. Как можно хранить и структурировать ACL.
  2. Как проверять права для всего, а не только на доступ к действиям контроллера.
  3. Как ACL организован в популярном решени CanCan.
  4. Как ACL организован в моем геме TheRole
  5. Почему мне не нравится CanCan и производные от него, но почему от так популярен.
  6. Какие удивительно сложные задачи можно встретить при реализации системы распределения прав.
  7. Где целесообразно применять сложные системы распределения прав, а где лучше ограничиться простейшими проверками.

А пока я буду вам признателен за любой, а особенно за жесткий, негативный и агрессивный фидбэк. Ведь именно он делает нас лучше.

Я на телеге: https://t.me/iam_the_teacher

Присоединиться к крутым рубистам:
Saint P Ruby Community:
https://t.me/saintprug

--

--

Ilya Zykin
Ilya Zykin

Written by Ilya Zykin

IT coach. React and Rails enthusiast. Passionate programmer, caring husband and pancake baker on Sundays. School teacher of computer studies in the past.

No responses yet