Heimdallr: защита полей модели и новый CanCan

в 20:22, , рубрики: acl, activerecord, ruby, ruby on rails, Блог компании Round Lake

В процессе превращения большей части web-проектов в браузерные приложения, появляется много вопросов. И один из самых весомых из них – обработка прав доступа без лишнего кода. Размышления на эту тему привели нас к большой проблеме: комфортного способа реализовать защиту на уровне полей модели для ActiveRecord просто нет (Егор, привет! ;). CanCan добавляет ограничения на уровне контроллеров, но это слишком высокий уровень чтобы решить все проблемы.

Немножко пободавшись, мы написали два милых гема. Встречайте, Heimdallr (Хеймдаль) и его расширение Heimdallr::Resource. Они принесут в ваши модели мир и безопасность.

Heimdallr

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

Heimdallr: защита полей модели и новый CanCan

Для многих RESTful-приложений, 1-й и 2-й уровни идентичны. Поэтому в сухом остатке у нас:

  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. Права доступа задают следующие вопросы:

  1. Какие сущности я могу получить через index?
  2. Какие из них я могу менять?
  3. Какие из них я могу удалить?
  4. Могу ли я создать новую сущность?
  5. Какие поля я могу задать при обновлении?
  6. Какие поля я могу задать при создании?

Первый вопрос решается 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js