From b9227eb971b78fb085f84627c9045b4273965105 Mon Sep 17 00:00:00 2001 From: Ryan Bates Date: Tue, 17 Nov 2009 16:56:16 -0800 Subject: [PATCH] adding a lot of inline documentation to code for rdocs --- lib/cancan.rb | 2 + lib/cancan/ability.rb | 122 +++++++++++++++++++---- lib/cancan/controller_additions.rb | 82 ++++++++++++++- spec/cancan/controller_additions_spec.rb | 2 + 4 files changed, 188 insertions(+), 20 deletions(-) diff --git a/lib/cancan.rb b/lib/cancan.rb index aefc301..b758257 100644 --- a/lib/cancan.rb +++ b/lib/cancan.rb @@ -1,4 +1,6 @@ module CanCan + # This error is raised when a user isn't allowed to access a given + # controller action. See ControllerAdditions#unauthorized! for details. class AccessDenied < StandardError; end end diff --git a/lib/cancan/ability.rb b/lib/cancan/ability.rb index e227c65..d3d9024 100644 --- a/lib/cancan/ability.rb +++ b/lib/cancan/ability.rb @@ -1,7 +1,37 @@ module CanCan + + # This module is designed to be included into an Ability class. This will + # provide the "can" methods for defining and checking abilities. + # + # class Ability + # include CanCan::Ability + # + # def initialize(user) + # if user.admin? + # can :manage, :all + # else + # can :read, :all + # end + # end + # end + # module Ability attr_accessor :user + # Not only can you use the can? method in the controller and view (see ControllerAdditions), + # but you can also call it directly on an ability instance. + # + # ability.can? :destroy, @project + # + # This makes testing a user's abilities very easy. + # + # def test "user can only destroy projects which he owns" + # user = User.new + # ability = Ability.new(user) + # assert ability.can?(:destroy, Project.new(:user => user)) + # assert ability.cannot?(:destroy, Project.new) + # end + # def can?(original_action, target) # TODO this could use some refactoring (@can_history || []).reverse.each do |can_action, can_target, can_block| can_actions = [can_action].flatten @@ -23,10 +53,83 @@ module CanCan false end + # Convenience method which works the same as "can?" but returns the opposite value. + # + # cannot? :destroy, @project + # def cannot?(*args) !can?(*args) end + # Defines which abilities are allowed using two arguments. The first one is the action + # you're setting the permission for, the second one is the class of object you're setting it on. + # + # can :update, Article + # + # You can pass an array for either of these parameters to match any one. + # + # can [:update, :destroy], [Article, Comment] + # + # In this case the user has the ability to update or destroy both articles and comments. + # + # You can pass a block to provide logic based on the article's attributes. + # + # can :update, Article do |article| + # article && article.user == user + # end + # + # If the block returns true then the user has that :update ability for that article, otherwise he + # will be denied access. It's possible for the passed in model to be nil if one isn't specified, + # so be sure to take that into consideration. + # + # You can pass :all to reference every type of object. In this case the object type will be passed + # into the block as well (just in case object is nil). + # + # can :read, :all do |object_class, object| + # object_class != Order + # end + # + # Here the user has permission to read all objects except orders. + # + # You can also pass :manage as the action which will match any action. In this case the action is + # passed to the block. + # + # can :manage, Comment do |action, comment| + # action != :destroy + # end + # + def can(action, target, &block) + @can_history ||= [] + @can_history << [action, target, block] + end + + # Finally, you can use the "alias_action" method to alias one or more actions into one. + # + # alias_action :update, :destroy, :to => :modify + # can :modify, Comment + # + # The following aliases are added by default for conveniently mapping common controller actions. + # + # alias_action :index, :show, :to => :read + # alias_action :new, :to => :create + # alias_action :edit, :to => :update + # + def alias_action(*args) + @aliased_actions ||= default_alias_actions + target = args.pop[:to] + @aliased_actions[target] = args + end + + private + + def default_alias_actions + { + :read => [:index, :show], + :create => [:new], + :update => [:edit], + } + end + def possible_actions_for(initial_action) actions = [initial_action] (@aliased_actions || default_alias_actions).each do |target, aliases| @@ -34,24 +137,5 @@ module CanCan end actions end - - def can(action, target, &block) - @can_history ||= [] - @can_history << [action, target, block] - end - - def alias_action(*args) - @aliased_actions ||= default_alias_actions - target = args.pop[:to] - @aliased_actions[target] = args - end - - def default_alias_actions - { - :read => [:index, :show], - :create => [:new], - :update => [:edit], - } - end end end diff --git a/lib/cancan/controller_additions.rb b/lib/cancan/controller_additions.rb index b84f991..5a4fe60 100644 --- a/lib/cancan/controller_additions.rb +++ b/lib/cancan/controller_additions.rb @@ -1,25 +1,87 @@ module CanCan + + # This module is automatically included into all controllers. + # It also makes the "can?" and "cannot?" methods available to all views. module ControllerAdditions def self.included(base) base.helper_method :can?, :cannot? end + # Raises the CanCan::AccessDenied exception. This is often used in a + # controller action to mark a request as unauthorized. + # + # def show + # @article = Article.find(params[:id]) + # unauthorized! if cannot? :read, @article + # end + # + # You can rescue from the exception in the controller to specify + # the user experience. + # + # class ApplicationController < ActionController::Base + # rescue_from CanCan::AccessDenied, :with => :access_denied + # + # protected + # + # def access_denied + # flash[:error] = "Sorry, you are not allowed to access that page." + # redirect_to root_url + # end + # end + # + # See the load_and_authorize_resource method to automatically add + # the "unauthorized!" behavior to a RESTful controller's actions. def unauthorized! raise AccessDenied, "You are unable to access this page." end + # Creates and returns the current user's ability. You generally do not invoke + # this method directly, instead you can override this method to change its + # behavior if the Ability class or current_user method are different. + # + # def current_ability + # UserAbility.new(current_account) # instead of Ability.new(current_user) + # end + # def current_ability ::Ability.new(current_user) end + # Use the "can?" method in the controller or view to check the user's permission + # for a given action and object. + # + # can? :destroy, @project + # + # You can also pass the class instead of an instance (if you don't have one handy). + # + # <% if can? :create, Project %> + # <%= link_to "New Project", new_project_path %> + # <% end %> + # + # This simply calls "can?" on the current_ability. def can?(*args) (@current_ability ||= current_ability).can?(*args) end + # Convenience method which works the same as "can?" but returns the opposite value. + # + # cannot? :destroy, @project + # def cannot?(*args) (@current_ability ||= current_ability).cannot?(*args) end + # This method loads the appropriate model resource into an instance variable. For example, + # given an ArticlesController it will load the current article into the @article instance + # variable. It does this by either calling Article.find(params[:id]) or + # Article.new(params[:article]) depending upon the action. It does nothing for the "index" + # action. + # + # You would often use this as a before filter in the controller. See + # load_and_authorize_resource to handle authorization too. + # + # before_filter :load_resource + # def load_resource # TODO this could use some refactoring model_name = params[:controller].split('/').last.singularize unless params[:action] == "index" @@ -31,11 +93,29 @@ module CanCan end end + # Authorizes the resource in the current instance variable. For example, + # if you have an ArticlesController it will check the @article instance variable + # and ensure the user can perform the current action on it. + # Under the hood it is doing something like the following. + # + # unauthorized! if cannot?(params[:action].to_sym, @article || Article) + # + # You would often use this as a before filter in the controller. + # + # before_filter :authorize_resource + # + # See load_and_authorize_resource to automatically load the resource too. def authorize_resource # TODO this could use some refactoring model_name = params[:controller].split('/').last.singularize - unauthorized! unless can?(params[:action].to_sym, instance_variable_get("@#{model_name}") || model_name.camelcase.constantize) + unauthorized! if cannot?(params[:action].to_sym, instance_variable_get("@#{model_name}") || model_name.camelcase.constantize) end + # Calls load_resource to load the current resource model into an instance variable. + # Then calls authorize_resource to ensure the current user is authorized to access the page. + # You would often use this as a before filter in the controller. + # + # before_filter :load_and_authorize_resource + # def load_and_authorize_resource load_resource authorize_resource diff --git a/spec/cancan/controller_additions_spec.rb b/spec/cancan/controller_additions_spec.rb index b72584b..878f35f 100644 --- a/spec/cancan/controller_additions_spec.rb +++ b/spec/cancan/controller_additions_spec.rb @@ -61,6 +61,7 @@ describe CanCan::ControllerAdditions do end it "should perform authorization using controller action and loaded model" do + stub(@controller).current_user { :current_user } @controller.instance_variable_set(:@ability, :some_resource) stub(@controller).params { {:controller => "abilities", :action => "show"} } stub(@controller).can?(:show, :some_resource) { false } @@ -70,6 +71,7 @@ describe CanCan::ControllerAdditions do end it "should perform authorization using controller action and non loaded model" do + stub(@controller).current_user { :current_user } stub(@controller).params { {:controller => "abilities", :action => "show"} } stub(@controller).can?(:show, Ability) { false } lambda {