News

Welcome to End Point’s blog

Ongoing observations by End Point people

Ruby on Rails: Attributes Management through Rights

I've written a couple times about a large Rails project that a few End Pointers have been working on. The system has a fairly robust system for managing rights and accessibility. The basic data model for rights, users, groups, and roles is this:

In this data model, right_assignments belong to a single right, and belong to a subject, through a polymorphic relationship. The subject can be a Role, Group, or User. A User can have a role or belong to a group. This allows for the ability to assign a right to a user directly or through a group or role.

In our code, there is a simple method for grabbing a user's set of rights:

  class User < ActiveRecord::Base
    ...
    def all_rights
      rights = [self.rights +
                self.groups.collect { |g| g.allowed_rights } +
                self.roles.collect { |r| r.rights }]
      rights = rights.flatten.uniq.collect { |r| r.action }

      rights
    end 
    ...
  end

In the case of this data model, groups also have a boolean which specifies whether or not rights can be assigned to them. The allowed_rights method looks like this:

  class Group < ActiveRecord::Base
    ...
    def allowed_rights
      self.assignable_rights ? self.rights : []
    end 
    ...
  end

This additional layer of protection on the group rights is because groups represent a collection of users that have implied behavior attached to them (different from roles). A group may or may not have rights assigned only through a user with the ability to assign rights to groups.

The interesting part of this application is how the rights are used. In Rails, you can define the accessible attributes for an object on the fly. In our code, rights may translate to attributes that can be updated. For example, the following [extremely simplified] example demonstrates this:

  # controller
  def update # or create
    if @item.custom_save(params)
      # saved
    else
      # handle errors
    end
  end

  # Given an item, with attributes title, description, tags, origin
  class Item < ActiveRecord::Base
    ...
    def custom_save(parameters)
      self.accessible = self.attr_accessible_for(current_user)

      item.update_attributes(parameters)
    end

    def attr_accessible_for(user)
      attrs = [:title, :description]  # Defaults

      [:tags, :origin].each do |field|
        # check for if user can_set_(tags || origin)
        if current_user.all_rights.include?("can_set_#{field.to_s}")
          attrs << field
        end
      end
 
      attrs
    end
    ...
  end

The above block updates an existing item. The default accessible attributes are title and description, which means that anyone can set those values. Tags and origin can only be set if the user has the right that correspond to that attribute. Tags and origin will not be saved for a user without those rights, even if they are passed in the parameters. The obvious disadvantage with this method is that there's no exception handling when a user tries to submit parameters that they cannot set.

The method above is reused to define which fields are editable to the current user, with the code shown below. So in theory, a user submitting parameters that they can't edit would only be doing it through a [malicious] post not based on the view below.

  # controller
  def new
    @item = Item.new

    @accessible = attr_accessible_for(current_user)
  end

  # view
  <%= form_for @item do |f| %>
    <% [:title, :description, :tag, :origin].each do |field| %>
      <%= f.label field %>

      <% if @accessible.include?(field) %>
        <%= f.text_field %>
      <% else -%> 
        <%= @item.send(field) %>
      <% end -%>

    <% end -%>
  <% end -%>

The data model and rights management described in this article isn't novel, but applying it in this fashion is elegant and produces modular and reusable methods. The code shown here has been simplified for this blog post. In reality, there are a few additional complexities:

  • The application utilizes acl9 for access control, which is an additional layer of security that will prevent non-registered users from creating items, and will prohibit specific users from updating existing items.
  • The user's all_rights method utilizes Rails low-level caching, with appropriate cache invalidation when the user's rights, groups, or roles change. I've given a simple example of Rails low-level caching in this blog article.
  • The logic in attr_accessible_for is more complex and can be based on the object's values or parameters. For example, an item may have a boolean that indicates anyone can tag it. The attr_accessible_for method will incorporate additional logic to determine if the :tags field is editable.
  • The view handles different field types (checkbox, textarea, etc) and allows for overriding the field label.

Here are several articles related to the same large Ruby on Rails project, for your reading pleasure:

Or, read about our services and contact us to help with your next Rails project.

1 comment:

lucj said...

Thanks for this great article.
I'm testing what you described but got an error when calling User.first.all_rights as User does not have any "rights" method.

My user and right_assignment classes are the following ones:

class User < ActiveRecord::Base
has_many :right_assignments, as: :subject

def all_rights
rights = [self.rights +
self.groups.collect { |g| g.allowed_rights } +
self.roles.collect { |r| r.rights }]
rights = rights.flatten.uniq.collect { |r| r.action }

rights
end
end

class RightAssignment < ActiveRecord::Base
belongs_to :right
belongs_to :subject, polymorphic: true
end

Any idea of what could be wrong ?
Thansk a lot,
Luc