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)
* 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 "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.
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 %>
<%= link_to "Edit", edit_article_path(@article) %>
<% 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
@article = Article.find(params[:id])
unauthorized! if cannot? :read, @article
authorize! :read, @article
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.
@ -71,6 +71,8 @@ If the user authorization fails, a CanCan::AccessDenied exception will be raised
end
end
See the CanCan::AccessDenied rdoc for more information on exception handling.
== 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/controller_resource'
require 'cancan/resource_authorization'
require 'cancan/controller_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
# 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.
#
@ -116,18 +116,21 @@ module CanCan
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.
# Raises a CanCan::AccessDenied exception if the current_ability cannot
# perform the given action. This is usually called in a controller action or
# before filter to perform the authorization.
#
# def show
# @article = Article.find(params[:id])
# unauthorized! if cannot? :read, @article
# authorize! :read, @article
# end
#
# The unauthorized! method accepts an optional argument which sets the
# message of the exception.
# A :message option can be passed to specify a different message.
#
# 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
# rescue_from CanCan::AccessDenied do |exception|
@ -136,10 +139,20 @@ module CanCan
# end
# end
#
# See the load_and_authorize_resource method to automatically add
# the "unauthorized!" behavior to a RESTful controller's actions.
def unauthorized!(message = "You are not authorized to access this page.")
raise AccessDenied, message
# See the CanCan::AccessDenied exception for more details on working with the exception.
#
# See the load_and_authorize_resource method to automatically add the authorize! behavior
# 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
# Creates and returns the current user's ability and caches it. If you

View File

@ -1,7 +1,7 @@
module CanCan
class ControllerResource # :nodoc:
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
@name = name
@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|
match do |model|
model.can?(*args)
match do |ability|
ability.can?(*args)
end
failure_message_for_should do |model|
failure_message_for_should do |ability|
"expected to be able to #{args.map(&:inspect).join(" ")}"
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(" ")}"
end
end

View File

@ -30,7 +30,7 @@ module CanCan
end
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
private

View File

@ -5,29 +5,48 @@ describe CanCan::ControllerAdditions do
@controller_class = Class.new
@controller = @controller_class.new
stub(@controller).params { {} }
stub(@controller).current_user { :current_user }
mock(@controller_class).helper_method(:can?, :cannot?)
@controller_class.send(:include, CanCan::ControllerAdditions)
end
it "should raise access denied with default message when calling unauthorized!" do
lambda {
@controller.unauthorized!
}.should raise_error(CanCan::AccessDenied, "You are not authorized to access this page.")
it "should raise ImplementationRemoved when attempting to call 'unauthorized!' on a controller" do
lambda { @controller.unauthorized! }.should raise_error(CanCan::ImplementationRemoved)
end
it "should raise access denied with custom message when calling unauthorized!" do
lambda {
@controller.unauthorized! "Access denied!"
}.should raise_error(CanCan::AccessDenied, "Access denied!")
it "should raise access denied exception if ability us unauthorized to perform a certain action" do
begin
@controller.authorize! :read, :foo, 1, 2, 3, :message => "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
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)
end
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.can?(:foo, :bar).should be_false
@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
lambda {
CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person)
}.should raise_error(CanCan::Error)
}.should raise_error(CanCan::ImplementationRemoved)
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
before(:each) do
@controller = Object.new # simple stub for now
stub(@controller).unauthorized! { raise CanCan::AccessDenied }
end
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
@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")
lambda {
authorization.authorize_resource
}.should raise_error(CanCan::AccessDenied)
lambda { authorization.authorize_resource }.should raise_error(CanCan::AccessDenied)
end
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")
lambda {
authorization.authorize_resource
}.should raise_error(CanCan::AccessDenied)
lambda { authorization.authorize_resource }.should raise_error(CanCan::AccessDenied)
end
it "should call load_resource and authorize_resource for load_and_authorize_resource" do