8 Commits
0.1.0 ... 0.2.0

7 changed files with 122 additions and 47 deletions

View File

@@ -1,3 +1,14 @@
*0.1.0* (Nov 16th, 2009) 0.2.0 (Nov 17, 2009)
* fix behavior of load_and_authorize_resource for namespaced controllers - see issue #3
* support arrays being passed to "can" to specify multiple actions or classes - see issue #2
* adding "cannot?" method to ability, controller, and view which is inverse of "can?" - see issue #1
* BACKWARDS INCOMPATIBLE: use Ability#initialize instead of 'prepare' to set up abilities - see issue #4
0.1.0 (Nov 16, 2009)
* initial release * initial release

View File

@@ -1,8 +1,8 @@
= CanCan = CanCan
This is a simple authorization solution for Rails which is completely decoupled from how you set up the user's roles. All permissions are stored in a single location for convenience. This is a simple authorization solution for Ruby on Rails to restrict what a given user is allowed to access in the application. This is completely decoupled from any role based implementation allowing you to define user roles the way you want. All permissions are stored in a single location for convenience.
This assumes you already have an authentication solution (such as Authlogic) which proves a current_user model. This assumes you already have authentication (such as Authlogic[http://github.com/binarylogic/authlogic]) which provides a current_user model.
== Installation == Installation
@@ -13,7 +13,7 @@ You can set it up as a gem in your environment.rb file.
And then install the gem. And then install the gem.
gem install cancan sudo rake gems:install
Alternatively you can install it as a Rails plugin. Alternatively you can install it as a Rails plugin.
@@ -22,12 +22,12 @@ Alternatively you can install it as a Rails plugin.
== Setup == Setup
First define a class called Ability, place it in "models/ability.rb". First, define a class called Ability in "models/ability.rb".
class Ability class Ability
include CanCan::Ability include CanCan::Ability
def prepare(user) def initialize(user)
if user.admin? if user.admin?
can :manage, :all can :manage, :all
else else
@@ -36,22 +36,22 @@ First define a class called Ability, place it in "models/ability.rb".
end end
end end
This class 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.
In the view layer you can access the current permissions at any point using the "can?" method. See "Checking Abilities" section below. You can access the current permissions at any point using the "can?" and "cannot?" methods in the view.
<% 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 this method in the controller layer along with the "unauthorized!" method to restrict access. You can also use these methods in a controller along with the "unauthorized!" method to restrict access.
def show def show
@article = Article.find(params[:id]) @article = Article.find(params[:id])
unauthorized! unless can? :read, @article unauthorized! if cannot? :read, @article
end end
Setting this for every action can be tedious, therefore a before filter is also provided for automatically applying this setting to a RESTful style resource controller. 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.
class ArticlesController < ApplicationController class ArticlesController < ApplicationController
before_filter :load_and_authorize_resource before_filter :load_and_authorize_resource
@@ -61,7 +61,7 @@ Setting this for every action can be tedious, therefore a before filter is also
end end
end end
If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior. 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, :with => :access_denied
@@ -77,7 +77,7 @@ If the user authorization fails, a CanCan::AccessDenied exception will be raised
== Defining Abilities == Defining Abilities
As shown above, the Ability#prepare method is where all user permissions are defined. The user model is passed into this method so you are free to modify the permissions based on the user's attributes. This way CanCan is completely decoupled with how you choose to handle roles. As shown above, the Ability class is where all user permissions are defined. The user model is passed into the initialize method so you are free to modify the permissions based on the user's attributes. This way CanCan is completely decoupled with how you choose to handle roles.
The "can" method accepts 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. The "can" method accepts 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.
@@ -89,7 +89,7 @@ You can pass an array for either of these parameters to match any one.
In this case the user has the ability to update or destroy both articles and comments. 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. For example: You can pass a block to provide logic based on the article's attributes.
can :update, Article do |article| can :update, Article do |article|
article && article.user == user article && article.user == user
@@ -129,16 +129,20 @@ Use the "can?" method in the controller or view to check the user's permission f
can? :destroy, @project can? :destroy, @project
You can also pass the class instead of an instance (if you don't have one handy). For example: You can also pass the class instead of an instance (if you don't have one handy).
<% if can? :create, Project %> <% if can? :create, Project %>
<%= link_to "New Project", new_project_path %> <%= link_to "New Project", new_project_path %>
<% end %> <% end %>
The "cannot?" method is for convenience and performs the opposite check of "can?"
cannot? :destroy, @project
== Custom Actions == Custom Actions
There is no limit to what actions you can use to determine abilities. For example, if only pro users are allowed to upload a picture for their product, you might add restrictions like this. You can have fine grained control over abilities by coming up with new actions. For example, if only pro users are allowed to upload a picture for their product, you could add the following restrictions.
# ability.rb # ability.rb
can :upload_picture, Project if user.pro? can :upload_picture, Project if user.pro?
@@ -148,24 +152,22 @@ There is no limit to what actions you can use to determine abilities. For exampl
# projects_controller.rb # projects_controller.rb
def update def update
unauthorized! if params[:project][:upload_picture] && !can?(:upload_picture, @project) unauthorized! if params[:project][:upload_picture] && cannot?(:upload_picture, @project)
# ... # ...
end end
== Customizing Assumptions == Assumptions & Configuring
CanCan makes two assumptions about your application. CanCan makes two assumptions about your application.
* The permissions are defined in Ability#prepare. * You have an Ability class which defines the permissions.
* The user is fetched with current_user method in the controller. * You have a current_user method in the controller which returns the current user model.
You can override these by defining the "current_ability" method in your ApplicationController. You can override these by overriding the "current_ability" method in your ApplicationController.
def current_ability def current_ability
ability = UserAbility.new # instead of Ability UserAbility.new(current_account) # instead of Ability.new(current_user)
ability.prepare(current_account) # instead of current_user
ability # be sure to return the ability
end end
That's it! That's it!
@@ -180,14 +182,41 @@ For example, let's assume that each user has_many :permissions, and each permiss
class Ability class Ability
include CanCan::Ability include CanCan::Ability
def prepare(user) def initialize(user)
can :manage, :all do |action, object_class, object| can :manage, :all do |action, object_class, object|
user.permissions.find_all_by_action(action).any? do |permission| user.permissions.find_all_by_action(action).any? do |permission|
permission.object_type.constantize == object_class && permission.object_type == object_class.to_s &&
(object.nil? || permission.object_id.nil? || permission.object_id == object.id) (object.nil? || permission.object_id.nil? || permission.object_id == object.id)
end end
end end
end end
end end
An alternatie approach is to define a separate "can" ability for each permission.
def initialize(user)
user.permissions.each do |permission|
can permission.action, permission.object_type.constantize do |object|
object.nil? || permission.object_id.nil? || permission.object_id == object.id
end
end
end
The actual details will depend largely on your application requirements, but hopefully you can see how it's possible to define permissions in the database and use them with CanCan. The actual details will depend largely on your application requirements, but hopefully you can see how it's possible to define permissions in the database and use them with CanCan.
== Testing Abilities
It is very easy to test the Ability model since you can call "can?" directly on it as you would in the view or controller.
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
== Special Thanks
CanCan was inspired by declarative_authorization[http://github.com/stffn/declarative_authorization/] and aegis[http://github.com/makandra/aegis]. Many thanks to the authors and contributors.

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.1.0" s.version = "0.2.0"
s.date = "2009-11-16" s.date = "2009-11-17"
s.authors = ["Ryan Bates"] s.authors = ["Ryan Bates"]
s.email = "ryan@railscasts.com" s.email = "ryan@railscasts.com"

View File

@@ -4,14 +4,16 @@ module CanCan
def can?(original_action, target) # TODO this could use some refactoring def can?(original_action, target) # TODO this could use some refactoring
(@can_history || []).reverse.each do |can_action, can_target, can_block| (@can_history || []).reverse.each do |can_action, can_target, can_block|
can_actions = [can_action].flatten
can_targets = [can_target].flatten
possible_actions_for(original_action).each do |action| possible_actions_for(original_action).each do |action|
if (can_action == :manage || can_action == action) && (can_target == :all || can_target == target || target.kind_of?(can_target)) if (can_actions.include?(:manage) || can_actions.include?(action)) && (can_targets.include?(:all) || can_targets.include?(target) || can_targets.any? { |c| target.kind_of?(c) })
if can_block.nil? if can_block.nil?
return true return true
else else
block_args = [] block_args = []
block_args << action if can_action == :manage block_args << action if can_actions.include?(:manage)
block_args << (target.class == Class ? target : target.class) if can_target == :all block_args << (target.class == Class ? target : target.class) if can_targets.include?(:all)
block_args << (target.class == Class ? nil : target) block_args << (target.class == Class ? nil : target)
return can_block.call(*block_args) return can_block.call(*block_args)
end end
@@ -21,6 +23,10 @@ module CanCan
false false
end end
def cannot?(*args)
!can?(*args)
end
def possible_actions_for(initial_action) def possible_actions_for(initial_action)
actions = [initial_action] actions = [initial_action]
(@aliased_actions || default_alias_actions).each do |target, aliases| (@aliased_actions || default_alias_actions).each do |target, aliases|
@@ -47,9 +53,5 @@ module CanCan
:update => [:edit], :update => [:edit],
} }
end end
def prepare(user)
# to be overriden by included class
end
end end
end end

View File

@@ -1,7 +1,7 @@
module CanCan module CanCan
module ControllerAdditions module ControllerAdditions
def self.included(base) def self.included(base)
base.helper_method :can? base.helper_method :can?, :cannot?
end end
def unauthorized! def unauthorized!
@@ -9,27 +9,31 @@ module CanCan
end end
def current_ability def current_ability
ability = ::Ability.new ::Ability.new(current_user)
ability.prepare(current_user)
ability
end end
def can?(*args) def can?(*args)
(@current_ability ||= current_ability).can?(*args) (@current_ability ||= current_ability).can?(*args)
end end
def cannot?(*args)
(@current_ability ||= current_ability).cannot?(*args)
end
def load_resource # TODO this could use some refactoring def load_resource # TODO this could use some refactoring
model_name = params[:controller].split('/').last.singularize
unless params[:action] == "index" unless params[:action] == "index"
if params[:id] if params[:id]
instance_variable_set("@#{params[:controller].singularize}", params[:controller].singularize.camelcase.constantize.find(params[:id])) instance_variable_set("@#{model_name}", model_name.camelcase.constantize.find(params[:id]))
else else
instance_variable_set("@#{params[:controller].singularize}", params[:controller].singularize.camelcase.constantize.new(params[params[:controller].singularize.to_sym])) instance_variable_set("@#{model_name}", model_name.camelcase.constantize.new(params[model_name.to_sym]))
end end
end end
end end
def authorize_resource # TODO this could use some refactoring def authorize_resource # TODO this could use some refactoring
unauthorized! unless can?(params[:action].to_sym, instance_variable_get("@#{params[:controller].singularize}") || params[:controller].singularize.camelcase.constantize) model_name = params[:controller].split('/').last.singularize
unauthorized! unless can?(params[:action].to_sym, instance_variable_get("@#{model_name}") || model_name.camelcase.constantize)
end end
def load_and_authorize_resource def load_and_authorize_resource

View File

@@ -78,7 +78,25 @@ describe CanCan::Ability do
@ability.can?(:edit, 123).should == :update_called @ability.can?(:edit, 123).should == :update_called
end end
it "should respond to prepare" do it "should not respond to prepare (now using initialize)" do
@ability.should respond_to(:prepare) @ability.should_not respond_to(:prepare)
end
it "should offer cannot? method which is simply invert of can?" do
@ability.cannot?(:tie, String).should be_true
end
it "should be able to specify multiple actions and match any" do
@ability.can [:read, :update], :all
@ability.can?(:read, 123).should be_true
@ability.can?(:update, 123).should be_true
@ability.can?(:count, 123).should be_false
end
it "should be able to specify multiple classes and match any" do
@ability.can :update, [String, Array]
@ability.can?(:update, "foo").should be_true
@ability.can?(:update, []).should be_true
@ability.can?(:update, 123).should be_false
end end
end end

View File

@@ -2,13 +2,16 @@ require File.dirname(__FILE__) + '/../spec_helper'
class Ability class Ability
include CanCan::Ability include CanCan::Ability
def initialize(user)
end
end end
describe CanCan::ControllerAdditions do describe CanCan::ControllerAdditions do
before(:each) do before(:each) do
@controller_class = Class.new @controller_class = Class.new
@controller = @controller_class.new @controller = @controller_class.new
mock(@controller_class).helper_method(:can?) mock(@controller_class).helper_method(:can?, :cannot?)
@controller_class.send(:include, CanCan::ControllerAdditions) @controller_class.send(:include, CanCan::ControllerAdditions)
end end
@@ -23,10 +26,11 @@ describe CanCan::ControllerAdditions do
@controller.current_ability.should be_kind_of(Ability) @controller.current_ability.should be_kind_of(Ability)
end end
it "should provide a can? method which goes 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 } 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
end end
it "should load the resource if params[:id] is specified" do it "should load the resource if params[:id] is specified" do
@@ -78,4 +82,11 @@ describe CanCan::ControllerAdditions do
stub(@controller).authorize_resource stub(@controller).authorize_resource
@controller.load_and_authorize_resource @controller.load_and_authorize_resource
end end
it "should properly load resource for namespaced controller" do
stub(@controller).params { {:controller => "admin/abilities", :action => "show", :id => 123} }
stub(Ability).find(123) { :some_resource }
@controller.load_resource
@controller.instance_variable_get(:@ability).should == :some_resource
end
end end