В процессе превращения большей части web-проектов в браузерные приложения, появляется много вопросов. И один из самых весомых из них – обработка прав доступа без лишнего кода. Размышления на эту тему привели нас к большой проблеме: комфортного способа реализовать защиту на уровне полей модели для ActiveRecord просто нет (Егор, привет! ;). CanCan добавляет ограничения на уровне контроллеров, но это слишком высокий уровень чтобы решить все проблемы.
Немножко пободавшись, мы написали два милых гема. Встречайте, Heimdallr (Хеймдаль) и его расширение Heimdallr::Resource. Они принесут в ваши модели мир и безопасность.
Heimdallr
Давайте для начала рассмотрим проблему глубже. Огромная часть проектов действительно приравнивает безопасность к управлению доступом REST-контроллеров. Большие проекты нередко спускаются к моделям, чтобы не дублировать код. А чтобы число экшнов в контроллерах не стало невыносимо большим, иногда спускаются и к контролю доступа полей.
Для многих RESTful-приложений, 1-й и 2-й уровни идентичны. Поэтому в сухом остатке у нас:
- Доступ к моделям
- Доступ к полям моделей
При этом важность управления доступом к полям стремительно растет с ростом проекта. И недавний пример с дискредитацией Github – яркий пример последствий подхода «Поля? Да кому это надо!».
Вот пример того, как Heimdallr может помочь с этим:
class Article < ActiveRecord::Base
include Heimdallr::Model
belongs_to :owner, :class_name => 'User'
restrict do |user, record|
if user.admin?
# Администратор может делать что угодно
scope :fetch
scope :delete
can [:view, :create, :update]
else
# Другие пользователи видят свои и не-секретные статьи
scope :fetch, -> { where('owner_id = ? or secrecy_level < ?', user.id, 5) }
scope :delete, -> { where('owner_id = ?', user.id) }
# ... и видят все поля кроме уровня секретности
# (хотя владельцы видет все поля)...
if record.try(:owner) == user
can :view
can :update, {
secrecy_level: { inclusion: { in: 0..4 } }
}
else
can :view
cannot :view, [:secrecy_level]
end
# ... а еще они могут их создавать, правда с ограничениями.
can :create, %w(content)
can :create, {
owner_id: user.id,
secrecy_level: { inclusion: { in: 0..4 } }
}
end
end
end
Используя простой DSL внутри моделей, мы объявляем как ограничения доступа к самим моделям, так и к их полям. Heimdallr расширяет использующие его модели методом .restrict
. Вызов этого метода обернет класс модели в прокси-обертку, которую можно использовать совершенно прозрачно.
Article.restrict(current_user).where(:typical => true)
Обратите внимание, что для вызовов Class.restrict
, вторым параметром блока будет nil. Поэтому все проверки, зависящие от состояния полей текущего объекта должны быть обернуты в .try(:field)
.
Эти ограничения можно использовать в проекте где угодно, не только в контроллерах. И это важно. Если вы попытаетесь прочитать защищенное поле – исключение. Такое поведение предсказуемо, но это не очень удобно для оформления вьюшек.
Чтобы решить проблему с вьюшками, Heimdallr реализует две стратегии, явную и неявную. По умолчанию, Heimdallr будет следовать явной модели поведение. А вот альтернативное поведение:
article = Article.restrict(current_user).first
@article = article.implicit
@article.protected_thing # => nil
Ок. В начале статьи я упомянул CanCan. Но разве он не решает проблему принципиально иначе?
CanCan
Для многих Rails-проектов термин «Безопасность» является синонимом именно для гема CanCan. CanCan действительно был целой эпохой и до сих пор отлично работает. Но у него есть ряд проблем:
- CanCan был задуман как инструмент, который не работает с моделями. Он предлагает архитектуру, в которой REST-контроллеры защищены, а до моделей злоумышленник попросту не дойдет. Иногда эта стратегия хороша, иногда нет. Но факт в том, что до полей при этом не добраться, как ни старайся. CanCan попросту не знает и ничего не может знать о полях.
- Ветка 1.х мертва и не поддерживается. В ней есть несколько неприятных багов, которые препятствуют работе в сложных случаях с namespac'ами. А ветка 2.х разрабатывается непозволительно долго.
Мы начали разработку Heimdallr'я как инструмента контроля моделей, но на практике оказалось, что у нас достаточно данных, чтобы ограничивать и контроллеры. Поэтому мы взяли и написали Heimdallr::Resource.
Эта часть Heimdallr'я мимикрирует под CanCan настолько, насколько это возможно. У вас есть тот же фильтр load_and_authorize
и вот как он работает:
- Если для текущего контекста не объявлен скоуп :create (и следовательно вы не можете создавать сущности), значит вам нельзя в new и create
- Если у вас нет скоупа :update, нельзя в edit и update.
- Аналогичный подход для скоупа :destroy
- В экшны вы получаете сразу защищенную сущность, а следовательно не можете забыть вручную вызвать
restrict
Выглядит это так:
class ArticlesController < ApplicationController
include Heimdallr::Resource
load_and_authorize_resource
# если имя контроллера отличается:
#
# load_and_authorize_resource :resource => :article
# для вложенных:
#
# routes.rb:
# resources :categories do
# resources :articles
# end
#
# load_and_authorize_resource :through => :category
def index
# @articles заполняются и restrict'ятся
end
def create
# @article заполняются и restrict'ятся
end
end
Провайдеры REST-API
В начале повествования, я рассказал о корне идеи, синхронизации прав доступа между клиентским приложением и серверным REST-API. И вот к каким конвенциям мы в итоге пришли.
Представьте, что у вас есть простой CRUD-интерфейс с ролями, который нужно реализовать как клиентское JS-приложение. При этом на сервере у вас есть REST с index/create/update/destroy. Права доступа задают следующие вопросы:
- Какие сущности я могу получить через index?
- Какие из них я могу менять?
- Какие из них я могу удалить?
- Могу ли я создать новую сущность?
- Какие поля я могу задать при обновлении?
- Какие поля я могу задать при создании?
Первый вопрос решается Heimdallr'ом от природы. Вы просто определяется нужный скоуп и все. Никто ничего лишнего просто не видит. Касательно остального. В своей последней статье я рассказал как мы рендерим JSON-представления для REST-провайдеров. Используя эту же технику, вьюху очень легко расширить следующими полями:
{modifiable: self.modifiable?, destroyable: self.destroyable?}
Могу ли я создавать? И с какими полями?
Для REST API метод new практически бесполезен. И это отличное место чтобы определить можем ли мы создавать что-то и что именно. Например так:
Article.restrictions(current_user).allowed_fields[:create]
Если мы не можем создавать вообще, Heimdallr::Resource ответит на этот запрос ошибкой. Иначе мы получим список полей, доступных для заполнения.
Хеймдаль также объявляет метод .creatable?
, так что и его можно прокинуть через REST.
Могу ли я обновлять?
Идея аналогична созданию. Только в этот раз мы объявим метод edit:
Article.restrictions(current_user).allowed_fields[:update]
В заключение
Использование Хеймдалля и его расширения, Heimdallr::Resource, поможет легко управлять правами доступа без лишнего мусора в коде. И, что немаловажно, вы получаете дополнительную магию для ваших REST-API. Помните, Хомяков следит за вами!
ಠ_ಠ
Автор: inossidabile