11 Commits
0.2.1 ... 1.0.1

11 changed files with 356 additions and 74 deletions

View File

@@ -1,3 +1,25 @@
1.0.1 (Dec 14, 2009)
* Adding :class option to load_resource so one can customize which class to use for the model - see issue #17
* Don't fetch parent of nested resource if *_id parameter is missing so it works with shallow nested routes - see issue #14
1.0.0 (Dec 13, 2009)
* Don't set resource instance variable if it has been set already - see issue #13
* Allowing :nested option to accept an array for deep nesting
* Adding :nested option to load resource method - see issue #10
* Pass :only and :except options to before filters for load/authorize resource methods.
* Adding :collection and :new options to load_resource method so we can specify behavior of additional actions if needed.
* BACKWARDS INCOMPATIBLE: turning load and authorize resource methods into class methods which set up the before filter so they can accept additional arguments.
0.2.1 (Nov 26, 2009) 0.2.1 (Nov 26, 2009)
* many internal refactorings - see issues #11 and #12 * many internal refactorings - see issues #11 and #12
@@ -6,6 +28,7 @@
* support custom objects (usually symbols) in can definition - see issue #8 * support custom objects (usually symbols) in can definition - see issue #8
0.2.0 (Nov 17, 2009) 0.2.0 (Nov 17, 2009)
* fix behavior of load_and_authorize_resource for namespaced controllers - see issue #3 * fix behavior of load_and_authorize_resource for namespaced controllers - see issue #3

View File

@@ -10,7 +10,7 @@ See the RDocs[http://rdoc.info/projects/ryanb/cancan] and Wiki[http://wiki.githu
You can set it up as a gem in your environment.rb file. You can set it up as a gem in your environment.rb file.
config.gem "cancan", :source => "http://gemcutter.org" config.gem "cancan"
And then install the gem. And then install the gem.
@@ -21,7 +21,7 @@ Alternatively you can install it as a Rails plugin.
script/plugin install git://github.com/ryanb/cancan.git script/plugin install git://github.com/ryanb/cancan.git
== Setup == Getting Started
First, define a class called Ability in "models/ability.rb". First, define a class called Ability in "models/ability.rb".
@@ -52,10 +52,10 @@ You can also use these methods in a controller along with the "unauthorized!" me
unauthorized! if cannot? :read, @article unauthorized! if cannot? :read, @article
end end
Setting this for every action can be tedious, therefore a before filter is also provided to automatically authorize all actions in a RESTful style resource controller. 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.
class ArticlesController < ApplicationController class ArticlesController < ApplicationController
before_filter :load_and_authorize_resource load_and_authorize_resource
def show def show
# @article is already loaded # @article is already loaded
@@ -65,11 +65,7 @@ Setting this for every action can be tedious, therefore a before filter is also
If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior in the ApplicationController. If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior in the ApplicationController.
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied, :with => :access_denied rescue_from CanCan::AccessDenied do |exception|
protected
def access_denied
flash[:error] = "Sorry, you are not allowed to access that page." flash[:error] = "Sorry, you are not allowed to access that page."
redirect_to root_url redirect_to root_url
end end
@@ -150,6 +146,30 @@ The following aliases are added by default for conveniently mapping common contr
alias_action :edit, :to => :update alias_action :edit, :to => :update
== Authorizing Controller Actions
As mentioned in the Getting Started section, you can use the +load_and_authorize_resource+ method in your controller to load the resource into an instance variable and authorize it. If you have a nested resource you can specify that as well.
load_and_authorize_resource :nested => :author
You can also pass an array to the :+nested+ attribute for deep nesting.
If you want to customize the loading behavior on certain actions, you can do so in a before filter.
class BooksController < ApplicationController
before_filter :find_book_by_permalink, :only => :show
load_and_authorize_resource
private
def find_book_by_permalink
@book = Book.find_by_permalink!(params[:id)
end
end
Here the @book instance variable is already set so it will not be loaded again for that action. This works for nested resources as well.
== Assumptions & Configuring == Assumptions & Configuring
CanCan makes two assumptions about your application. CanCan makes two assumptions about your application.

View File

@@ -4,8 +4,8 @@ Gem::Specification.new do |s|
s.description = "Simple authorization solution for Rails which is completely decoupled from the user's roles. All permissions are stored in a single location for convenience." s.description = "Simple authorization solution for Rails which is completely decoupled from the user's roles. All permissions are stored in a single location for convenience."
s.homepage = "http://github.com/ryanb/cancan" s.homepage = "http://github.com/ryanb/cancan"
s.version = "0.2.1" s.version = "1.0.1"
s.date = "2009-11-26" s.date = "2009-12-14"
s.authors = ["Ryan Bates"] s.authors = ["Ryan Bates"]
s.email = "ryan@railscasts.com" s.email = "ryan@railscasts.com"

View File

@@ -5,5 +5,6 @@ module CanCan
end end
require File.dirname(__FILE__) + '/cancan/ability' require File.dirname(__FILE__) + '/cancan/ability'
require File.dirname(__FILE__) + '/cancan/controller_resource'
require File.dirname(__FILE__) + '/cancan/resource_authorization' require File.dirname(__FILE__) + '/cancan/resource_authorization'
require File.dirname(__FILE__) + '/cancan/controller_additions' require File.dirname(__FILE__) + '/cancan/controller_additions'

View File

@@ -3,7 +3,115 @@ module CanCan
# This module is automatically included into all controllers. # This module is automatically included into all controllers.
# It also makes the "can?" and "cannot?" methods available to all views. # It also makes the "can?" and "cannot?" methods available to all views.
module ControllerAdditions module ControllerAdditions
module ClassMethods
# Sets up a before filter which loads and authorizes the current resource. This performs both
# load_resource and authorize_resource and accepts the same arguments. See those methods for details.
#
# class BooksController < ApplicationController
# load_and_authorize_resource
# end
#
def load_and_authorize_resource(options = {})
before_filter(options.slice(:only, :except)) { |c| ResourceAuthorization.new(c, c.params, options.except(:only, :except)).load_and_authorize_resource }
end
# Sets up a before filter which 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.
#
# Call this method directly on the controller class.
#
# class BooksController < ApplicationController
# load_resource
# end
#
# A resource is not loaded if the instance variable is already set. This makes it easy to override
# the behavior through a before_filter on certain actions.
#
# class BooksController < ApplicationController
# before_filter :find_book_by_permalink, :only => :show
# load_resource
#
# private
#
# def find_book_by_permalink
# @book = Book.find_by_permalink!(params[:id)
# end
# end
#
# See load_and_authorize_resource to automatically authorize the resource too.
#
# Options:
# [:+only+]
# Only applies before filter to given actions.
#
# [:+except+]
# Does not apply before filter to given actions.
#
# [:+nested+]
# Specify which resource this is nested under.
#
# load_resource :nested => :author
#
# Deep nesting can be defined in an array.
#
# load_resource :nested => [:publisher, :author]
#
# [:+class+]
# The class to use for the model.
#
# [:+collection+]
# Specify which actions are resource collection actions in addition to :+index+. This
# is usually not necessary because it will try to guess depending on if an :+id+
# is present in +params+.
#
# load_resource :collection => [:sort, :list]
#
# [:+new+]
# Specify which actions are new resource actions in addition to :+new+ and :+create+.
# Pass an action name into here if you would like to build a new resource instead of
# fetch one.
#
# load_resource :new => :build
#
def load_resource(options = {})
before_filter(options.slice(:only, :except)) { |c| ResourceAuthorization.new(c, c.params, options.except(:only, :except)).load_resource }
end
# Sets up a before filter which authorizes the current resource using the 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)
#
# Call this method directly on the controller class.
#
# class BooksController < ApplicationController
# authorize_resource
# end
#
# See load_and_authorize_resource to automatically load the resource too.
#
# Options:
# [:+only+]
# Only applies before filter to given actions.
#
# [:+except+]
# Does not apply before filter to given actions.
#
# [:+class+]
# The class to use for the model.
#
def authorize_resource(options = {})
before_filter(options.slice(:only, :except)) { |c| ResourceAuthorization.new(c, c.params, options.except(:only, :except)).authorize_resource }
end
end
def self.included(base) def self.included(base)
base.extend ClassMethods
base.helper_method :can?, :cannot? base.helper_method :can?, :cannot?
end end
@@ -70,48 +178,6 @@ module CanCan
def cannot?(*args) def cannot?(*args)
(@current_ability ||= current_ability).cannot?(*args) (@current_ability ||= current_ability).cannot?(*args)
end 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
ResourceAuthorization.new(self, params).load_resource
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
ResourceAuthorization.new(self, params).authorize_resource
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
end
end end
end end

View File

@@ -0,0 +1,40 @@
module CanCan
class ControllerResource # :nodoc:
def initialize(controller, name, parent = nil, options = {})
@controller = controller
@name = name
@parent = parent
@options = options
end
def model_class
@options[:class] || @name.to_s.camelize.constantize
end
def find(id)
self.model_instance ||= base.find(id)
end
def build(attributes)
if base.kind_of? Class
self.model_instance ||= base.new(attributes)
else
self.model_instance ||= base.build(attributes)
end
end
def model_instance
@controller.instance_variable_get("@#{@name}")
end
def model_instance=(instance)
@controller.instance_variable_set("@#{@name}", instance)
end
private
def base
@parent ? @parent.model_instance.send(@name.to_s.pluralize) : model_class
end
end
end

View File

@@ -2,9 +2,10 @@ module CanCan
class ResourceAuthorization # :nodoc: class ResourceAuthorization # :nodoc:
attr_reader :params attr_reader :params
def initialize(controller, params) def initialize(controller, params, options = {})
@controller = controller @controller = controller
@params = params @params = params
@options = options
end end
def load_and_authorize_resource def load_and_authorize_resource
@@ -13,29 +14,49 @@ module CanCan
end end
def load_resource def load_resource
self.model_instance = params[:id] ? model_class.find(params[:id]) : model_class.new(params[model_name.to_sym]) unless params[:action] == "index" unless collection_actions.include? params[:action].to_sym
if new_actions.include? params[:action].to_sym
resource.build(params[model_name.to_sym])
elsif params[:id]
resource.find(params[:id])
end
end
end end
def authorize_resource def authorize_resource
@controller.unauthorized! if @controller.cannot?(params[:action].to_sym, model_instance || model_class) @controller.unauthorized! if @controller.cannot?(params[:action].to_sym, resource.model_instance || resource.model_class)
end end
private private
def resource
@resource ||= ControllerResource.new(@controller, model_name, parent_resource, @options)
end
def parent_resource
parent = nil
[@options[:nested]].flatten.compact.each do |name|
id = @params["#{name}_id".to_sym]
if id
parent = ControllerResource.new(@controller, name, parent)
parent.find(id)
else
parent = nil
end
end
parent
end
def model_name def model_name
params[:controller].split('/').last.singularize params[:controller].split('/').last.singularize
end end
def model_class def collection_actions
model_name.camelcase.constantize [:index] + [@options[:collection]].flatten
end end
def model_instance def new_actions
@controller.instance_variable_get("@#{model_name}") [:new, :create] + [@options[:new]].flatten
end
def model_instance=(instance)
@controller.instance_variable_set("@#{model_name}", instance)
end end
end end
end end

View File

@@ -27,19 +27,21 @@ describe CanCan::ControllerAdditions do
@controller.cannot?(:foo, :bar).should be_true @controller.cannot?(:foo, :bar).should be_true
end end
it "should load resource" do it "load_and_authorize_resource should setup a before filter which passes call to ResourceAuthorization" do
mock.instance_of(CanCan::ResourceAuthorization).load_resource stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.load_and_authorize_resource
@controller.load_resource mock(@controller_class).before_filter({}) { |options, block| block.call(@controller) }
@controller_class.load_and_authorize_resource :foo => :bar
end end
it "should authorize resource" do it "authorize_resource should setup a before filter which passes call to ResourceAuthorization" do
mock.instance_of(CanCan::ResourceAuthorization).authorize_resource stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.authorize_resource
@controller.authorize_resource mock(@controller_class).before_filter(:except => :show) { |options, block| block.call(@controller) }
@controller_class.authorize_resource :foo => :bar, :except => :show
end end
it "should load and authorize resource in one call through controller" do it "load_resource should setup a before filter which passes call to ResourceAuthorization" do
mock(@controller).load_resource stub(CanCan::ResourceAuthorization).new(@controller, @controller.params, :foo => :bar).mock!.load_resource
mock(@controller).authorize_resource mock(@controller_class).before_filter(:only => [:show, :index]) { |options, block| block.call(@controller) }
@controller.load_and_authorize_resource @controller_class.load_resource :foo => :bar, :only => [:show, :index]
end end
end end

View File

@@ -0,0 +1,49 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe CanCan::ControllerResource do
before(:each) do
@controller = Object.new
end
it "should determine model class by constantizing give name" do
CanCan::ControllerResource.new(@controller, :ability).model_class.should == Ability
end
it "should fetch model through model class and assign it to the instance" do
stub(Ability).find(123) { :some_ability }
CanCan::ControllerResource.new(@controller, :ability).find(123)
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should fetch model through parent and assign it to the instance" do
parent = Object.new
stub(parent).model_instance.stub!.abilities.stub!.find(123) { :some_ability }
CanCan::ControllerResource.new(@controller, :ability, parent).find(123)
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should build model through model class and assign it to the instance" do
stub(Ability).new(123) { :some_ability }
CanCan::ControllerResource.new(@controller, :ability).build(123)
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should build model through parent and assign it to the instance" do
parent = Object.new
stub(parent).model_instance.stub!.abilities.stub!.build(123) { :some_ability }
CanCan::ControllerResource.new(@controller, :ability, parent).build(123)
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should not load resource if instance variable is already provided" do
@controller.instance_variable_set(:@ability, :some_ability)
CanCan::ControllerResource.new(@controller, :ability).find(123)
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should use the model class option if provided" do
stub(Person).find(123) { :some_resource }
CanCan::ControllerResource.new(@controller, :ability, nil, :class => Person).find(123)
@controller.instance_variable_get(:@ability).should == :some_resource
end
end

View File

@@ -56,4 +56,60 @@ describe CanCan::ResourceAuthorization do
authorization.authorize_resource authorization.authorize_resource
}.should raise_error(CanCan::AccessDenied) }.should raise_error(CanCan::AccessDenied)
end end
it "should call load_resource and authorize_resource for load_and_authorize_resource" do
authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "show")
mock(authorization).load_resource
mock(authorization).authorize_resource
authorization.load_and_authorize_resource
end
it "should not build a resource when on custom collection action" do
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "sort"}, {:collection => [:sort, :list]})
authorization.load_resource
@controller.instance_variable_get(:@ability).should be_nil
end
it "should build a resource when on custom new action even when params[:id] exists" do
stub(Ability).new(nil) { :some_resource }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "build", :id => 123}, {:new => :build})
authorization.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
it "should not try to load resource for other action if params[:id] is undefined" do
authorization = CanCan::ResourceAuthorization.new(@controller, :controller => "abilities", :action => "list")
authorization.load_resource
@controller.instance_variable_get(:@ability).should be_nil
end
it "should load nested resource and fetch other resource through the association" do
stub(Person).find(456).stub!.abilities.stub!.find(123) { :some_ability }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "show", :id => 123, :person_id => 456}, {:nested => :person})
authorization.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should load nested resource and build resource through a deep association" do
stub(Person).find(456).stub!.behaviors.stub!.find(789).stub!.abilities.stub!.build(nil) { :some_ability }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "new", :person_id => 456, :behavior_id => 789}, {:nested => [:person, :behavior]})
authorization.load_resource
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should not load nested resource and build through this if *_id param isn't specified" do
stub(Person).find(456) { :some_person }
stub(Ability).new(nil) { :some_ability }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "new", :person_id => 456}, {:nested => [:person, :behavior]})
authorization.load_resource
@controller.instance_variable_get(:@person).should == :some_person
@controller.instance_variable_get(:@ability).should == :some_ability
end
it "should load the model using a custom class" do
stub(Person).find(123) { :some_resource }
authorization = CanCan::ResourceAuthorization.new(@controller, {:controller => "abilities", :action => "show", :id => 123}, {:class => Person})
authorization.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
end end

View File

@@ -16,3 +16,7 @@ class Ability
def initialize(user) def initialize(user)
end end
end end
# this class helps out in testing nesting
class Person
end