[Dive into Gems - 2] Cancancan
Bài đăng này đã không được cập nhật trong 3 năm
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
andauthorize_resource
.load_resource
loads the object of the model class.authorize_resource
invokes the rules storageCancan::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.
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 classability.rb
: stores all the rulescontroller_resource.rb
andinherited_resource.rb
(a special kind of controller resource): authorizing processcontroller_addition.rb
: wrapper ofcontroller_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