cancan 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ *0.1.0* (Nov 16th, 2009)
2
+
3
+ * initial release
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Ryan Bates
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,187 @@
1
+ = CanCan
2
+
3
+ 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.
4
+
5
+ This assumes you already have an authentication solution (such as Authlogic) which proves a current_user model.
6
+
7
+
8
+ == Installation
9
+
10
+ Install it as a Rails plugin.
11
+
12
+ script/plugin install git://github.com/ryanb/cancan.git
13
+
14
+ It will be available as a gem soon.
15
+
16
+
17
+ == Setup
18
+
19
+ First define a class called Ability, place it in "models/ability.rb".
20
+
21
+ class Ability
22
+ include CanCan::Ability
23
+
24
+ def prepare(user)
25
+ if user.admin?
26
+ can :manage, :all
27
+ else
28
+ can :read, :all
29
+ end
30
+ end
31
+ end
32
+
33
+ This class is where all permissions will go. See the "Defining Abilities" section below for more information.
34
+
35
+ In the view layer you can access the current permissions at any point using the "can?" method. See "Checking Abilities" section below.
36
+
37
+ <% if can? :update, @article %>
38
+ <%= link_to "Edit", edit_article_path(@article) %>
39
+ <% end %>
40
+
41
+ You can also use this method in the controller layer along with the "unauthorized!" method to restrict access.
42
+
43
+ def show
44
+ @article = Article.find(params[:id])
45
+ unauthorized! unless can? :read, @article
46
+ end
47
+
48
+ 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.
49
+
50
+ class ArticlesController < ApplicationController
51
+ before_filter :load_and_authorize_resource
52
+
53
+ def show
54
+ # @article is already loaded
55
+ end
56
+ end
57
+
58
+ If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior.
59
+
60
+ class ApplicationController < ActionController::Base
61
+ rescue_from CanCan::AccessDenied, :with => :access_denied
62
+
63
+ protected
64
+
65
+ def access_denied
66
+ flash[:error] = "Sorry, you are not allowed to access that page."
67
+ redirect_to root_url
68
+ end
69
+ end
70
+
71
+
72
+ == Defining Abilities
73
+
74
+ 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.
75
+
76
+ 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.
77
+
78
+ can :update, Article
79
+
80
+ You can pass an array for either of these parameters to match any one.
81
+
82
+ can [:update, :destroy], [Article, Comment]
83
+
84
+ In this case the user has the ability to update or destroy both articles and comments.
85
+
86
+ You can pass a block to provide logic based on the article's attributes. For example:
87
+
88
+ can :update, Article do |article|
89
+ article && article.user == user
90
+ end
91
+
92
+ 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.
93
+
94
+ 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).
95
+
96
+ can :read, :all do |object_class, object|
97
+ object_class != Order
98
+ end
99
+
100
+ Here the user has permission to read all objects except orders.
101
+
102
+ You can also pass :manage as the action which will match any action. In this case the action is passed to the block.
103
+
104
+ can :manage, Comment do |action, comment|
105
+ action != :destroy
106
+ end
107
+
108
+ Finally, you can use the "alias_action" method to alias one or more actions into one.
109
+
110
+ alias_action :update, :destroy, :to => :modify
111
+ can :modify, Comment
112
+
113
+ The following aliases are added by default for conveniently mapping common controller actions.
114
+
115
+ alias_action :index, :show, :to => :read
116
+ alias_action :new, :to => :create
117
+ alias_action :edit, :to => :update
118
+
119
+
120
+ == Checking Abilities
121
+
122
+ Use the "can?" method in the controller or view to check the user's permission for a given action and object.
123
+
124
+ can? :destroy, @project
125
+
126
+ You can also pass the class instead of an instance (if you don't have one handy). For example:
127
+
128
+ <% if can? :create, Project %>
129
+ <%= link_to "New Project", new_project_path %>
130
+ <% end %>
131
+
132
+
133
+ == Custom Actions
134
+
135
+ 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.
136
+
137
+ # ability.rb
138
+ can :upload_picture, Project if user.pro?
139
+
140
+ # projects/_form.html.erb
141
+ <%= f.file_field :picture if can? :upload_picture, @project %>
142
+
143
+ # projects_controller.rb
144
+ def update
145
+ unauthorized! if params[:project][:upload_picture] && !can?(:upload_picture, @project)
146
+ # ...
147
+ end
148
+
149
+
150
+ == Customizing Assumptions
151
+
152
+ CanCan makes two assumptions about your application.
153
+
154
+ * The permissions are defined in Ability#prepare.
155
+ * The user is fetched with current_user method in the controller.
156
+
157
+ You can override these by defining the "current_ability" method in your ApplicationController.
158
+
159
+ def current_ability
160
+ ability = UserAbility.new # instead of Ability
161
+ ability.prepare(current_account) # instead of current_user
162
+ ability # be sure to return the ability
163
+ end
164
+
165
+ That's it!
166
+
167
+
168
+ == Permissions in Database
169
+
170
+ Perhaps a non-coder needs the ability to modify the user abilities, or you want to change them without having to re-deploy the application. In that case it may be best to store the permission logic in a separate model, let's call it Permission. It is easy to use the database records when defining abilities.
171
+
172
+ For example, let's assume that each user has_many :permissions, and each permission has "action", "object_type" and "object_id" columns. The last of which is optional.
173
+
174
+ class Ability
175
+ include CanCan::Ability
176
+
177
+ def prepare(user)
178
+ can :manage, :all do |action, object_class, object|
179
+ user.permissions.find_all_by_action(action).any? do |permission|
180
+ permission.object_type.constantize == object_class &&
181
+ (object.nil? || permission.object_id.nil? || permission.object_id == object.id)
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ 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.
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'spec/rake/spectask'
4
+
5
+ spec_files = Rake::FileList["spec/**/*_spec.rb"]
6
+
7
+ desc "Run specs"
8
+ Spec::Rake::SpecTask.new do |t|
9
+ t.spec_files = spec_files
10
+ t.spec_opts = ["-c"]
11
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'cancan'
@@ -0,0 +1,6 @@
1
+ module CanCan
2
+ class AccessDenied < StandardError; end
3
+ end
4
+
5
+ require File.dirname(__FILE__) + '/cancan/ability'
6
+ require File.dirname(__FILE__) + '/cancan/controller_additions'
@@ -0,0 +1,55 @@
1
+ module CanCan
2
+ module Ability
3
+ attr_accessor :user
4
+
5
+ def can?(original_action, target) # TODO this could use some refactoring
6
+ (@can_history || []).reverse.each do |can_action, can_target, can_block|
7
+ possible_actions_for(original_action).each do |action|
8
+ if (can_action == :manage || can_action == action) && (can_target == :all || can_target == target || target.kind_of?(can_target))
9
+ if can_block.nil?
10
+ return true
11
+ else
12
+ block_args = []
13
+ block_args << action if can_action == :manage
14
+ block_args << (target.class == Class ? target : target.class) if can_target == :all
15
+ block_args << (target.class == Class ? nil : target)
16
+ return can_block.call(*block_args)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ false
22
+ end
23
+
24
+ def possible_actions_for(initial_action)
25
+ actions = [initial_action]
26
+ (@aliased_actions || default_alias_actions).each do |target, aliases|
27
+ actions += possible_actions_for(target) if aliases.include? initial_action
28
+ end
29
+ actions
30
+ end
31
+
32
+ def can(action, target, &block)
33
+ @can_history ||= []
34
+ @can_history << [action, target, block]
35
+ end
36
+
37
+ def alias_action(*args)
38
+ @aliased_actions ||= default_alias_actions
39
+ target = args.pop[:to]
40
+ @aliased_actions[target] = args
41
+ end
42
+
43
+ def default_alias_actions
44
+ {
45
+ :read => [:index, :show],
46
+ :create => [:new],
47
+ :update => [:edit],
48
+ }
49
+ end
50
+
51
+ def prepare(user)
52
+ # to be overriden by included class
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,46 @@
1
+ module CanCan
2
+ module ControllerAdditions
3
+ def self.included(base)
4
+ base.helper_method :can?
5
+ end
6
+
7
+ def unauthorized!
8
+ raise AccessDenied, "You are unable to access this page."
9
+ end
10
+
11
+ def current_ability
12
+ ability = ::Ability.new
13
+ ability.prepare(current_user)
14
+ ability
15
+ end
16
+
17
+ def can?(*args)
18
+ (@current_ability ||= current_ability).can?(*args)
19
+ end
20
+
21
+ def load_resource # TODO this could use some refactoring
22
+ unless params[:action] == "index"
23
+ if params[:id]
24
+ instance_variable_set("@#{params[:controller].singularize}", params[:controller].singularize.camelcase.constantize.find(params[:id]))
25
+ else
26
+ instance_variable_set("@#{params[:controller].singularize}", params[:controller].singularize.camelcase.constantize.new(params[params[:controller].singularize.to_sym]))
27
+ end
28
+ end
29
+ end
30
+
31
+ def authorize_resource # TODO this could use some refactoring
32
+ unauthorized! unless can?(params[:action].to_sym, instance_variable_get("@#{params[:controller].singularize}") || params[:controller].singularize.camelcase.constantize)
33
+ end
34
+
35
+ def load_and_authorize_resource
36
+ load_resource
37
+ authorize_resource
38
+ end
39
+ end
40
+ end
41
+
42
+ if defined? ActionController
43
+ ActionController::Base.class_eval do
44
+ include CanCan::ControllerAdditions
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe CanCan::Ability do
4
+ before(:each) do
5
+ @ability_class = Class.new
6
+ @ability_class.send(:include, CanCan::Ability)
7
+ @ability = @ability_class.new
8
+ end
9
+
10
+ it "should be able to :read anything" do
11
+ @ability.can :read, :all
12
+ @ability.can?(:read, String).should be_true
13
+ @ability.can?(:read, 123).should be_true
14
+ end
15
+
16
+ it "should not have permission to do something it doesn't know about" do
17
+ @ability.can?(:foodfight, String).should be_false
18
+ end
19
+
20
+ it "should return what block returns on a can call" do
21
+ @ability.can :read, :all
22
+ @ability.can :read, Symbol do |sym|
23
+ sym
24
+ end
25
+ @ability.can?(:read, Symbol).should be_nil
26
+ @ability.can?(:read, :some_symbol).should == :some_symbol
27
+ end
28
+
29
+ it "should pass class with object if :all objects are accepted" do
30
+ @ability.can :preview, :all do |object_class, object|
31
+ [object_class, object]
32
+ end
33
+ @ability.can?(:preview, 123).should == [Fixnum, 123]
34
+ end
35
+
36
+ it "should pass class with no object if :all objects are accepted and class is passed directly" do
37
+ @ability.can :preview, :all do |object_class, object|
38
+ [object_class, object]
39
+ end
40
+ @ability.can?(:preview, Hash).should == [Hash, nil]
41
+ end
42
+
43
+ it "should pass action and object for global manage actions" do
44
+ @ability.can :manage, Array do |action, object|
45
+ [action, object]
46
+ end
47
+ @ability.can?(:stuff, [1, 2]).should == [:stuff, [1, 2]]
48
+ @ability.can?(:stuff, Array).should == [:stuff, nil]
49
+ end
50
+
51
+ it "should alias update or destroy actions to modify action" do
52
+ @ability.alias_action :update, :destroy, :to => :modify
53
+ @ability.can :modify, :all do |object_class, object|
54
+ :modify_called
55
+ end
56
+ @ability.can?(:update, 123).should == :modify_called
57
+ @ability.can?(:destroy, 123).should == :modify_called
58
+ end
59
+
60
+ it "should return block result for action, object_class, and object for any action" do
61
+ @ability.can :manage, :all do |action, object_class, object|
62
+ [action, object_class, object]
63
+ end
64
+ @ability.can?(:foo, 123).should == [:foo, Fixnum, 123]
65
+ @ability.can?(:bar, Fixnum).should == [:bar, Fixnum, nil]
66
+ end
67
+
68
+ it "should automatically alias index and show into read calls" do
69
+ @ability.can :read, :all
70
+ @ability.can?(:index, 123).should be_true
71
+ @ability.can?(:show, 123).should be_true
72
+ end
73
+
74
+ it "should automatically alias new and edit into create and update respectively" do
75
+ @ability.can(:create, :all) { :create_called }
76
+ @ability.can(:update, :all) { :update_called }
77
+ @ability.can?(:new, 123).should == :create_called
78
+ @ability.can?(:edit, 123).should == :update_called
79
+ end
80
+
81
+ it "should respond to prepare" do
82
+ @ability.should respond_to(:prepare)
83
+ end
84
+ end
@@ -0,0 +1,81 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ class Ability
4
+ include CanCan::Ability
5
+ end
6
+
7
+ describe CanCan::ControllerAdditions do
8
+ before(:each) do
9
+ @controller_class = Class.new
10
+ @controller = @controller_class.new
11
+ mock(@controller_class).helper_method(:can?)
12
+ @controller_class.send(:include, CanCan::ControllerAdditions)
13
+ end
14
+
15
+ it "should read from the cache with request uri as key and render that text" do
16
+ lambda {
17
+ @controller.unauthorized!
18
+ }.should raise_error(CanCan::AccessDenied)
19
+ end
20
+
21
+ it "should have a current_ability method which generates an ability for the current user" do
22
+ stub(@controller).current_user { :current_user }
23
+ @controller.current_ability.should be_kind_of(Ability)
24
+ end
25
+
26
+ it "should provide a can? method which goes through the current ability" do
27
+ stub(@controller).current_user { :current_user }
28
+ @controller.current_ability.should be_kind_of(Ability)
29
+ @controller.can?(:foo, :bar).should be_false
30
+ end
31
+
32
+ it "should load the resource if params[:id] is specified" do
33
+ stub(@controller).params { {:controller => "abilities", :action => "show", :id => 123} }
34
+ stub(Ability).find(123) { :some_resource }
35
+ @controller.load_resource
36
+ @controller.instance_variable_get(:@ability).should == :some_resource
37
+ end
38
+
39
+ it "should build a new resource with hash if params[:id] is not specified" do
40
+ stub(@controller).params { {:controller => "abilities", :action => "create", :ability => {:foo => "bar"}} }
41
+ stub(Ability).new(:foo => "bar") { :some_resource }
42
+ @controller.load_resource
43
+ @controller.instance_variable_get(:@ability).should == :some_resource
44
+ end
45
+
46
+ it "should build a new resource even if attribute hash isn't specified" do
47
+ stub(@controller).params { {:controller => "abilities", :action => "new"} }
48
+ stub(Ability).new(nil) { :some_resource }
49
+ @controller.load_resource
50
+ @controller.instance_variable_get(:@ability).should == :some_resource
51
+ end
52
+
53
+ it "should not build a resource when on index action" do
54
+ stub(@controller).params { {:controller => "abilities", :action => "index"} }
55
+ @controller.load_resource
56
+ @controller.instance_variable_get(:@ability).should be_nil
57
+ end
58
+
59
+ it "should perform authorization using controller action and loaded model" do
60
+ @controller.instance_variable_set(:@ability, :some_resource)
61
+ stub(@controller).params { {:controller => "abilities", :action => "show"} }
62
+ stub(@controller).can?(:show, :some_resource) { false }
63
+ lambda {
64
+ @controller.authorize_resource
65
+ }.should raise_error(CanCan::AccessDenied)
66
+ end
67
+
68
+ it "should perform authorization using controller action and non loaded model" do
69
+ stub(@controller).params { {:controller => "abilities", :action => "show"} }
70
+ stub(@controller).can?(:show, Ability) { false }
71
+ lambda {
72
+ @controller.authorize_resource
73
+ }.should raise_error(CanCan::AccessDenied)
74
+ end
75
+
76
+ it "should load and authorize resource in one call" do
77
+ mock(@controller).load_resource
78
+ stub(@controller).authorize_resource
79
+ @controller.load_and_authorize_resource
80
+ end
81
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'active_support'
4
+ require 'active_record'
5
+ require 'action_controller'
6
+ require 'action_view'
7
+ require File.dirname(__FILE__) + '/../lib/cancan.rb'
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.mock_with :rr
11
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cancan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Bates
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-16 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ 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.
17
+ email: ryan@railscasts.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ - CHANGELOG.rdoc
25
+ - LICENSE
26
+ files:
27
+ - lib/cancan/ability.rb
28
+ - lib/cancan/controller_additions.rb
29
+ - lib/cancan.rb
30
+ - spec/cancan/ability_spec.rb
31
+ - spec/cancan/controller_additions_spec.rb
32
+ - spec/spec_helper.rb
33
+ - LICENSE
34
+ - README.rdoc
35
+ - Rakefile
36
+ - CHANGELOG.rdoc
37
+ - init.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/ryanb/cancan
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --line-numbers
45
+ - --inline-source
46
+ - --title
47
+ - CanCan
48
+ - --main
49
+ - README.rdoc
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "1.2"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.3.5
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Simple authorization solution for Rails.
71
+ test_files: []
72
+