+1

[Dive into Gems - 2] Cancancan

Authentication and authorization are two fundamental parts of almost every website. While authentication answers the question "who are you?", authorization determines "what can you do?". This sounds pretty straightforward but putting things right is not always a piece of cake. Fortunately, in Rails, there are great libaries can help us out with that. In this blog, I will discuss the authorization problem and what Cancancan actually does to save us from headache.

In a non-trival website, authorization usually consists of a bunch of different rules. This set of rule sometimes can be very complicated. For example, on Facebook we have rules such as:

  • a user can only see public profile of another user if they are not friends
  • a user cannot see a post of his friend if he is on restricted group or the post is hidden to him
  • ...

To implement these rules, the most naive way we can think of is checking in every action. However, this way will quickly polute our code base, make it extremely hard to maintain and often a huge source of bugs. To deal with this problem, the author of Cancancan had a great idea that collect every rules and put them into a separate file. He named that file ability.rb.

This is an example of ability.rb:

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)

    if user.admin?
      can :manage, :all
    else
      can :read, :all
    end
  end
end

As we can see in the example above, authorization rules are defined very clearly. Cancan::Ability is a module which provides handy methods to define authorization rule.

...
def can(action = nil, subject = nil, conditions = nil, &block)
  add_rule(Rule.new(true, action, subject, conditions, block))
end

def cannot(action = nil, subject = nil, conditions = nil, &block)
  add_rule(Rule.new(false, action, subject, conditions, block))
end

def add_rule(rule)
  rules << rule
  add_rule_to_index(rule, rules.size - 1)
end

def add_rule_to_index(rule, position)
  @rules_index ||= Hash.new { |h, k| h[k] = [] }

  subjects = rule.subjects.compact
  subjects << :all if subjects.empty?

  subjects.each do |subject|
    @rules_index[subject] << position
  end
end
...

As we can see, all the rules are put into an array, in which, each element is an instance of class Rule. To increase the performance, they also build an index. This module's responsibility is containing all the rules and exposing methods that check if a user is allowed to perform a specific action or not.

def authorize!(action, subject, *args)
  message = nil
  if args.last.kind_of?(Hash) && args.last.has_key?(:message)
    message = args.pop[:message]
  end
  if cannot?(action, subject, *args)
    message ||= unauthorized_message(action, subject)
    raise AccessDenied.new(message, action, subject)
  end
  subject
end

def cannot?(*args)
  !can?(*args)
end

def can?(action, subject, *extra_args)
  subject = extract_subjects(subject)

  match = subject.map do |subject|
    relevant_rules_for_match(action, subject).detect do |rule|
      rule.matches_conditions?(action, subject, extra_args)
    end
  end.compact.first

  match ? match.base_behavior : false
end

With this module, we have the first half of the solution - defining all the rules. We still need to enforce them. It's done via a module named Cancan::ControllerAdditions which is automatically included into all controllers.

module CanCan
  module ControllerAdditions
    module ClassMethods
      def load_and_authorize_resource(*args)
        cancan_resource_class.add_before_filter(self, :load_and_authorize_resource, *args)
      end

      def load_resource(*args)
        cancan_resource_class.add_before_filter(self, :load_resource, *args)
      end

      def authorize_resource(*args)
        cancan_resource_class.add_before_filter(self, :authorize_resource, *args)
      end
      ...
    end
    ...
  end
end

The most used method probably is load_and_authorize_resource. In most case, we only need to call it at the begining of the controller and everything is magically done. This method actually added a before filter in the controller, which perform every checks needed to make sure no authorization rule is violated.

Behind this module, there is another module which really contains the logic

module CanCan
  class ControllerResource
    def self.add_before_filter(controller_class, method, *args)
      options = args.extract_options!
      resource_name = args.first
      before_filter_method = options.delete(:prepend) ? :prepend_before_filter : :before_filter
      controller_class.send(before_filter_method, options.slice(:only, :except, :if, :unless)) do |controller|
        controller.class.cancan_resource_class.new(controller, resource_name, options.except(:only, :except, :if, :unless)).send(method)
      end
    end

    def load_and_authorize_resource
      load_resource
      authorize_resource
    end

    def load_resource
      unless skip?(:load)
        if load_instance?
          self.resource_instance ||= load_resource_instance
        elsif load_collection?
          self.collection_instance ||= load_collection
        end
      end
    end

    def authorize_resource
      unless skip?(:authorize)
        @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
      end
    end

    def authorize!(*args)
      @_authorized = true
      current_ability.authorize!(*args)
    end
    ...
  end
end
  • add_before_filter makes sure the filter is added before other filters of the controller.
  • load_and_authorize_resource turns out to consist of 2 steps: load_resource and authorize_resource.
  • load_resource loads the object of the model class.
  • authorize_resource invokes the rules storage Cancan::Ability to determined the current user is allowed to perform the current action on this object or not.

With that, the solution is completed. Very handy and clearly. Let take a look at the whole gem again.

Screen Shot 2016-05-16 at 1.11.38 AM.png

We've covered the most fundamental part of the gem. In summary, the main logic of Cancancan reside in 5 files:

  • rule.rb: the rule class
  • ability.rb: stores all the rules
  • controller_resource.rb and inherited_resource.rb (a special kind of controller resource): authorizing process
  • controller_addition.rb: wrapper of controller_resourse

There are also other parts which provides test helpers, model helpers, generator and data adapters. For more details, you can look at the actual code. This is a very well documented gem and pretty easy to read so I highly recommend you take a look at it to better understand a very important part of Rails apps.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí