Heimdallr: model field protection and the new CanCan

Original author: Boris Staal
  • Transfer
In the process of turning most of web-projects into browser-based applications, many questions arise. And one of the most significant of them is the processing of access rights without unnecessary code. Reflections on this topic led us to a big problem: there is simply no convenient way to implement protection at the field level of the model for ActiveRecord ( Egor , hello!;). CanCan adds restrictions at the controller level, but it's too high to solve all the problems.

After a little bit of frustration, we wrote two cute gems. Meet Heimdallr (Heimdal) and its extension Heimdallr :: Resource . They will bring peace and security to your models.

Heimdallr


Let's first look at the problem deeper. A huge part of the projects really equates security with the access control of REST controllers. Large projects often go down to models so as not to duplicate code. And so that the number of actions in the controllers does not become unbearably large, sometimes they go down to control access fields.



For many RESTful applications, the 1st and 2nd levels are identical. Therefore, in the bottom line, we have:

  1. Model Access
  2. Access to model fields

At the same time, the importance of managing access to fields is growing rapidly with the growth of the project. And the recent Github discredit example is a prime example of the implications of the Fields? But who needs it! ”

Here is an example of how Heimdallr can help with this:

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

Using a simple DSL inside models, we declare both restrictions on access to the models themselves and to their fields. Heimdallr extends the models that use it .restrict. Calling this method will wrap the model class in a proxy wrapper that you can use completely transparently.

Article.restrict(current_user).where(:typical => true)

Note that for calls Class.restrict, the second parameter to the block will be nil. Therefore, all checks that depend on the state of the fields of the current object must be wrapped in .try(:field).

These restrictions can be used in the project anywhere, not only in controllers. And that is important. If you try to read a protected field, an exception. This behavior is predictable, but it is not very convenient for the design of the views.

To solve the problem with the views, Heimdallr implements two strategies, explicit and implicit. By default, Heimdallr will follow an explicit behavior model. And here is an alternative behavior:

article  = Article.restrict(current_user).first
@article = article.implicit
@article.protected_thing # => nil

OK. At the beginning of the article, I mentioned CanCan. But does he not solve the problem in a fundamentally different way?

Cancan


For many Rails projects, the term "Security" is a synonym for the CanCan gem. CanCan really was an era and it still works great. But he has a number of problems:

  • CanCan was conceived as a tool that does not work with models. It offers an architecture in which REST controllers are protected, and an attacker simply won’t reach the models. Sometimes this strategy is good, sometimes not. But the fact is that you can’t get to the fields, no matter how hard you try. CanCan simply does not know and cannot know anything about fields.
  • Branch 1.x is dead and not supported. It has several unpleasant bugs that prevent it from working in complex cases with namespacs. A branch 2.x is developed prohibitively long.

We started developing Heimdallr as a model control tool, but in practice it turned out that we have enough data to limit the controllers. Therefore, we took and wrote Heimdallr :: Resource.

This part of Heimdallr mimics CanCan as much as possible. You have the same filter load_and_authorizeand this is how it works:

  • If the scope: create is not declared for the current context (and therefore you cannot create entities), then you cannot in new and create
  • If you do not have scope: update, you cannot edit and update either.
  • A similar approach for scope: destroy
  • In action, you get an instantly protected entity, and therefore you can’t forget to manually call restrict

It looks like this:

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 Providers


At the beginning of the story, I talked about the root of the idea, synchronization of access rights between the client application and the server REST-API. And so what conventions did we end up with.

Imagine that you have a simple CRUD interface with roles that you need to implement as a client JS application. At the same time, on the server, you have REST with index / create / update / destroy. Access rights ask the following questions:

  1. What entities can I get through index?
  2. Which of them can I change?
  3. Which of them can I remove?
  4. Can I create a new entity?
  5. What fields can I set when updating?
  6. What fields can I set when creating?

The first question is decided by Heimdallr by nature. You just determined the desired scope and all. Nobody just sees anything superfluous. Regarding the rest. In my last article, I talked about how we render JSON representations for REST providers. Using the same technique, the view is very easy to expand with the following fields:

{modifiable: self.modifiable?, destroyable: self.destroyable?}

Can i create? And with what fields?


For the REST API, the new method is practically useless. And this is a great place to determine if we can create something and what exactly. For example, like this:

Article.restrictions(current_user).allowed_fields[:create]

If we cannot create at all, Heimdallr :: Resource will respond to this request with an error. Otherwise, we get a list of fields available for filling.

Heimdahl also declares a method .creatable?, so that it can also be cast through REST.

Can i update?


The idea is similar to creation. Only this time we will declare the edit method:

Article.restrictions(current_user).allowed_fields[:update]

Finally


Using Heimdall and its extension, Heimdallr :: Resource, will help you easily manage access rights without unnecessary garbage in the code. And, importantly, you get extra magic for your REST-APIs. Remember, Khomyakov is watching you!

ಠ_ಠ

Also popular now: