removing unauthorized! in favor of authorize! and including more information in AccessDenied exception - closes #40

This commit is contained in:
Ryan Bates 2010-04-16 14:54:18 -07:00
parent ecf2818a9e
commit 8903feee70
12 changed files with 152 additions and 49 deletions

View File

@ -1,5 +1,9 @@
1.1.0 (not released) 1.1.0 (not released)
* Removing "unauthorized!" method in favor of "authorize!"
* Adding action, subject and default_message abilities to AccessDenied exception - see issue #40
* Adding caching to current_ability controller method, if you're overriding this be sure to add caching too. * Adding caching to current_ability controller method, if you're overriding this be sure to add caching too.
* Adding "accessible_by" method to Active Record for fetching records matching a specific ability * Adding "accessible_by" method to Active Record for fetching records matching a specific ability

View File

@ -39,17 +39,17 @@ First, define a class called Ability in "models/ability.rb".
This is where all permissions will go. See the "Defining Abilities" section below for more information. This is where all permissions will go. See the "Defining Abilities" section below for more information.
You can access the current permissions at any point using the "can?" and "cannot?" methods in the view. You can access the current permissions at any point using the "can?" and "cannot?" methods in the view and controller.
<% if can? :update, @article %> <% if can? :update, @article %>
<%= link_to "Edit", edit_article_path(@article) %> <%= link_to "Edit", edit_article_path(@article) %>
<% end %> <% end %>
You can also use these methods in a controller along with the "unauthorized!" method to restrict access. The "authorize!" method in the controller will raise CanCan::AccessDenied if the user is not able to perform the given action.
def show def show
@article = Article.find(params[:id]) @article = Article.find(params[:id])
unauthorized! if cannot? :read, @article authorize! :read, @article
end end
Setting this for every action can be tedious, therefore the load_and_authorize_resource method is also provided to automatically authorize all actions in a RESTful style resource controller. It will set up a before filter which loads the resource into the instance variable and authorizes it. Setting this for every action can be tedious, therefore the load_and_authorize_resource method is also provided to automatically authorize all actions in a RESTful style resource controller. It will set up a before filter which loads the resource into the instance variable and authorizes it.
@ -71,6 +71,8 @@ If the user authorization fails, a CanCan::AccessDenied exception will be raised
end end
end end
See the CanCan::AccessDenied rdoc for more information on exception handling.
== Defining Abilities == Defining Abilities

View File

@ -1,14 +1,6 @@
module CanCan
# A general CanCan exception
class Error < StandardError; end
# This error is raised when a user isn't allowed to access a given
# controller action. See ControllerAdditions#unauthorized! for details.
class AccessDenied < Error; end
end
require 'cancan/ability' require 'cancan/ability'
require 'cancan/controller_resource' require 'cancan/controller_resource'
require 'cancan/resource_authorization' require 'cancan/resource_authorization'
require 'cancan/controller_additions' require 'cancan/controller_additions'
require 'cancan/active_record_additions' require 'cancan/active_record_additions'
require 'cancan/exceptions'

View File

@ -85,7 +85,7 @@ module CanCan
# and ensure the user can perform the current action on it. Under the hood it is doing # and ensure the user can perform the current action on it. Under the hood it is doing
# something like the following. # something like the following.
# #
# unauthorized! if cannot?(params[:action].to_sym, @article || Article) # authorize!(params[:action].to_sym, @article || Article)
# #
# Call this method directly on the controller class. # Call this method directly on the controller class.
# #
@ -116,18 +116,21 @@ module CanCan
base.helper_method :can?, :cannot? base.helper_method :can?, :cannot?
end end
# Raises the CanCan::AccessDenied exception. This is often used in a # Raises a CanCan::AccessDenied exception if the current_ability cannot
# controller action to mark a request as unauthorized. # perform the given action. This is usually called in a controller action or
# before filter to perform the authorization.
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# unauthorized! if cannot? :read, @article # authorize! :read, @article
# end # end
# #
# The unauthorized! method accepts an optional argument which sets the # A :message option can be passed to specify a different message.
# message of the exception.
# #
# You can rescue from the exception in the controller to define the behavior. # authorize! :read, @article, :message => "Not authorized to read #{@article.name}"
#
# You can rescue from the exception in the controller to customize how unauthorized
# access is displayed to the user.
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# rescue_from CanCan::AccessDenied do |exception| # rescue_from CanCan::AccessDenied do |exception|
@ -136,10 +139,20 @@ module CanCan
# end # end
# end # end
# #
# See the load_and_authorize_resource method to automatically add # See the CanCan::AccessDenied exception for more details on working with the exception.
# the "unauthorized!" behavior to a RESTful controller's actions. #
def unauthorized!(message = "You are not authorized to access this page.") # See the load_and_authorize_resource method to automatically add the authorize! behavior
raise AccessDenied, message # to the default RESTful actions.
def authorize!(action, subject, *args)
message = nil
if args.last.kind_of?(Hash) && args.last.has_key?(:message)
message = args.pop[:message]
end
raise AccessDenied.new(message, action, subject) if cannot?(action, subject, *args)
end
def unauthorized!(message = nil)
raise ImplementationRemoved, "The unauthorized! method has been removed from CanCan, use authorize! instead."
end end
# Creates and returns the current user's ability and caches it. If you # Creates and returns the current user's ability and caches it. If you

View File

@ -1,7 +1,7 @@
module CanCan module CanCan
class ControllerResource # :nodoc: class ControllerResource # :nodoc:
def initialize(controller, name, parent = nil, options = {}) def initialize(controller, name, parent = nil, options = {})
raise CanCan::Error, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class raise ImplementationRemoved, "The :class option has been renamed to :resource for specifying the class in CanCan." if options.has_key? :class
@controller = controller @controller = controller
@name = name @name = name
@parent = parent @parent = parent

43
lib/cancan/exceptions.rb Normal file
View File

@ -0,0 +1,43 @@
module CanCan
# A general CanCan exception
class Error < StandardError; end
# Raised when removed code is called, an alternative solution is provided in message.
class ImplementationRemoved < Error; end
# This error is raised when a user isn't allowed to access a given controller action.
# This usually happens within a call to ControllerAdditions#authorized! but can be
# raised manually.
#
# raise CanCan::AccessDenied.new("Not authorized!", :read, Article)
#
# The passed message, action, and subject are optional and can later be retrieved when
# rescuing from the exception.
#
# exception.message # => "Not authorized!"
# exception.action # => :read
# exception.subject # => Article
#
# If the message is not specified (or is nil) it will default to "You are anot authorized
# to access this page." This default can be overridden by setting default_message.
#
# exception.default_message = "Default error message"
# exception.message # => "Default error message"
#
# See ControllerAdditions#authorized! for more information on rescuing from this exception.
class AccessDenied < Error
attr_reader :action, :subject
attr_writer :default_message
def initialize(message = nil, action = nil, subject = nil)
@message = message
@action = action
@subject = subject
@default_message = "You are not authorized to access this page."
end
def to_s
@message || @default_message
end
end
end

View File

@ -1,13 +1,13 @@
Spec::Matchers.define :be_able_to do |*args| Spec::Matchers.define :be_able_to do |*args|
match do |model| match do |ability|
model.can?(*args) ability.can?(*args)
end end
failure_message_for_should do |model| failure_message_for_should do |ability|
"expected to be able to #{args.map(&:inspect).join(" ")}" "expected to be able to #{args.map(&:inspect).join(" ")}"
end end
failure_message_for_should_not do |model| failure_message_for_should_not do |ability|
"expected not to be able to #{args.map(&:inspect).join(" ")}" "expected not to be able to #{args.map(&:inspect).join(" ")}"
end end
end end

View File

@ -30,7 +30,7 @@ module CanCan
end end
def authorize_resource def authorize_resource
@controller.unauthorized! if @controller.cannot?(params[:action].to_sym, resource.model_instance || resource.model_class) @controller.authorize!(params[:action].to_sym, resource.model_instance || resource.model_class)
end end
private private

View File

@ -5,29 +5,48 @@ describe CanCan::ControllerAdditions do
@controller_class = Class.new @controller_class = Class.new
@controller = @controller_class.new @controller = @controller_class.new
stub(@controller).params { {} } stub(@controller).params { {} }
stub(@controller).current_user { :current_user }
mock(@controller_class).helper_method(:can?, :cannot?) mock(@controller_class).helper_method(:can?, :cannot?)
@controller_class.send(:include, CanCan::ControllerAdditions) @controller_class.send(:include, CanCan::ControllerAdditions)
end end
it "should raise access denied with default message when calling unauthorized!" do it "should raise ImplementationRemoved when attempting to call 'unauthorized!' on a controller" do
lambda { lambda { @controller.unauthorized! }.should raise_error(CanCan::ImplementationRemoved)
@controller.unauthorized!
}.should raise_error(CanCan::AccessDenied, "You are not authorized to access this page.")
end end
it "should raise access denied with custom message when calling unauthorized!" do it "should raise access denied exception if ability us unauthorized to perform a certain action" do
lambda { begin
@controller.unauthorized! "Access denied!" @controller.authorize! :read, :foo, 1, 2, 3, :message => "Access denied!"
}.should raise_error(CanCan::AccessDenied, "Access denied!") rescue CanCan::AccessDenied => e
e.message.should == "Access denied!"
e.action.should == :read
e.subject.should == :foo
else
fail "Expected CanCan::AccessDenied exception to be raised"
end
end
it "should not raise access denied exception if ability is authorized to perform an action" do
@controller.current_ability.can :read, :foo
lambda { @controller.authorize!(:read, :foo) }.should_not raise_error
end
it "should raise access denied exception with default message if not specified" do
begin
@controller.authorize! :read, :foo
rescue CanCan::AccessDenied => e
e.default_message = "Access denied!"
e.message.should == "Access denied!"
else
fail "Expected CanCan::AccessDenied exception to be raised"
end
end end
it "should have a current_ability method which generates an ability for the current user" do it "should have a current_ability method which generates an ability for the current user" do
stub(@controller).current_user { :current_user }
@controller.current_ability.should be_kind_of(Ability) @controller.current_ability.should be_kind_of(Ability)
end end
it "should provide a can? and cannot? methods which go through the current ability" do it "should provide a can? and cannot? methods which go through the current ability" do
stub(@controller).current_user { :current_user }
@controller.current_ability.should be_kind_of(Ability) @controller.current_ability.should be_kind_of(Ability)
@controller.can?(:foo, :bar).should be_false @controller.can?(:foo, :bar).should be_false
@controller.cannot?(:foo, :bar).should be_true @controller.cannot?(:foo, :bar).should be_true

View File

@ -54,6 +54,6 @@ describe CanCan::ControllerResource do
it "should raise an exception when specifying :class option since it is no longer used" do it "should raise an exception when specifying :class option since it is no longer used" do
lambda { lambda {
CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person) CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person)
}.should raise_error(CanCan::Error) }.should raise_error(CanCan::ImplementationRemoved)
end end
end end

View File

@ -0,0 +1,35 @@
require "spec_helper"
describe CanCan::AccessDenied do
describe "with action and subject" do
before(:each) do
@exception = CanCan::AccessDenied.new(nil, :some_action, :some_subject)
end
it "should have action and subject accessors" do
@exception.action.should == :some_action
@exception.subject.should == :some_subject
end
it "should have a changable default message" do
@exception.message.should == "You are not authorized to access this page."
@exception.default_message = "Unauthorized!"
@exception.message.should == "Unauthorized!"
end
end
describe "with only a message" do
before(:each) do
@exception = CanCan::AccessDenied.new("Access denied!")
end
it "should have nil action and subject" do
@exception.action.should be_nil
@exception.subject.should be_nil
end
it "should have passed message" do
@exception.message.should == "Access denied!"
end
end
end

View File

@ -3,7 +3,6 @@ require "spec_helper"
describe CanCan::ResourceAuthorization do describe CanCan::ResourceAuthorization do
before(:each) do before(:each) do
@controller = Object.new # simple stub for now @controller = Object.new # simple stub for now
stub(@controller).unauthorized! { raise CanCan::AccessDenied }
end end
it "should load the resource into an instance variable if params[:id] is specified" do it "should load the resource into an instance variable if params[:id] is specified" do
@ -49,19 +48,15 @@ describe CanCan::ResourceAuthorization do
it "should perform authorization using controller action and loaded model" do it "should perform authorization using controller action and loaded model" do
@controller.instance_variable_set(:@ability, :some_resource) @controller.instance_variable_set(:@ability, :some_resource)
stub(@controller).cannot?(:show, :some_resource) { true } stub(@controller).authorize!(:show, :some_resource) { raise CanCan::AccessDenied }
authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show") authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show")
lambda { lambda { authorization.authorize_resource }.should raise_error(CanCan::AccessDenied)
authorization.authorize_resource
}.should raise_error(CanCan::AccessDenied)
end end
it "should perform authorization using controller action and non loaded model" do it "should perform authorization using controller action and non loaded model" do
stub(@controller).cannot?(:show, Ability) { true } stub(@controller).authorize!(:show, Ability) { raise CanCan::AccessDenied }
authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show") authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show")
lambda { lambda { authorization.authorize_resource }.should raise_error(CanCan::AccessDenied)
authorization.authorize_resource
}.should raise_error(CanCan::AccessDenied)
end end
it "should call load_resource and authorize_resource for load_and_authorize_resource" do it "should call load_resource and authorize_resource for load_and_authorize_resource" do