cancan 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +12 -1
- data/README.rdoc +62 -27
- data/lib/cancan/ability.rb +9 -7
- data/lib/cancan/controller_additions.rb +12 -8
- data/spec/cancan/ability_spec.rb +20 -2
- data/spec/cancan/controller_additions_spec.rb +13 -2
- metadata +2 -2
data/CHANGELOG.rdoc
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
-
|
1
|
+
0.2.0 (Nov 17, 2009)
|
2
|
+
|
3
|
+
* fix behavior of load_and_authorize_resource for namespaced controllers - see issue #3
|
4
|
+
|
5
|
+
* support arrays being passed to "can" to specify multiple actions or classes - see issue #2
|
6
|
+
|
7
|
+
* adding "cannot?" method to ability, controller, and view which is inverse of "can?" - see issue #1
|
8
|
+
|
9
|
+
* BACKWARDS INCOMPATIBLE: use Ability#initialize instead of 'prepare' to set up abilities - see issue #4
|
10
|
+
|
11
|
+
|
12
|
+
0.1.0 (Nov 16, 2009)
|
2
13
|
|
3
14
|
* initial release
|
data/README.rdoc
CHANGED
@@ -1,27 +1,33 @@
|
|
1
1
|
= CanCan
|
2
2
|
|
3
|
-
This is a simple authorization solution for Rails
|
3
|
+
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.
|
4
4
|
|
5
|
-
This assumes you already have
|
5
|
+
This assumes you already have authentication (such as Authlogic[http://github.com/binarylogic/authlogic]) which provides a current_user model.
|
6
6
|
|
7
7
|
|
8
8
|
== Installation
|
9
9
|
|
10
|
-
|
10
|
+
You can set it up as a gem in your environment.rb file.
|
11
|
+
|
12
|
+
config.gem "cancan", :source => "http://gemcutter.org"
|
13
|
+
|
14
|
+
And then install the gem.
|
11
15
|
|
12
|
-
|
16
|
+
sudo rake gems:install
|
17
|
+
|
18
|
+
Alternatively you can install it as a Rails plugin.
|
13
19
|
|
14
|
-
|
20
|
+
script/plugin install git://github.com/ryanb/cancan.git
|
15
21
|
|
16
22
|
|
17
23
|
== Setup
|
18
24
|
|
19
|
-
First define a class called Ability
|
25
|
+
First, define a class called Ability in "models/ability.rb".
|
20
26
|
|
21
27
|
class Ability
|
22
28
|
include CanCan::Ability
|
23
29
|
|
24
|
-
def
|
30
|
+
def initialize(user)
|
25
31
|
if user.admin?
|
26
32
|
can :manage, :all
|
27
33
|
else
|
@@ -30,22 +36,22 @@ First define a class called Ability, place it in "models/ability.rb".
|
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
33
|
-
This
|
39
|
+
This is where all permissions will go. See the "Defining Abilities" section below for more information.
|
34
40
|
|
35
|
-
|
41
|
+
You can access the current permissions at any point using the "can?" and "cannot?" methods in the view.
|
36
42
|
|
37
43
|
<% if can? :update, @article %>
|
38
44
|
<%= link_to "Edit", edit_article_path(@article) %>
|
39
45
|
<% end %>
|
40
46
|
|
41
|
-
You can also use
|
47
|
+
You can also use these methods in a controller along with the "unauthorized!" method to restrict access.
|
42
48
|
|
43
49
|
def show
|
44
50
|
@article = Article.find(params[:id])
|
45
|
-
unauthorized!
|
51
|
+
unauthorized! if cannot? :read, @article
|
46
52
|
end
|
47
53
|
|
48
|
-
Setting this for every action can be tedious, therefore a before filter is also provided
|
54
|
+
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.
|
49
55
|
|
50
56
|
class ArticlesController < ApplicationController
|
51
57
|
before_filter :load_and_authorize_resource
|
@@ -55,7 +61,7 @@ Setting this for every action can be tedious, therefore a before filter is also
|
|
55
61
|
end
|
56
62
|
end
|
57
63
|
|
58
|
-
If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior.
|
64
|
+
If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior in the ApplicationController.
|
59
65
|
|
60
66
|
class ApplicationController < ActionController::Base
|
61
67
|
rescue_from CanCan::AccessDenied, :with => :access_denied
|
@@ -71,7 +77,7 @@ If the user authorization fails, a CanCan::AccessDenied exception will be raised
|
|
71
77
|
|
72
78
|
== Defining Abilities
|
73
79
|
|
74
|
-
As shown above, the Ability
|
80
|
+
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.
|
75
81
|
|
76
82
|
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
83
|
|
@@ -83,7 +89,7 @@ You can pass an array for either of these parameters to match any one.
|
|
83
89
|
|
84
90
|
In this case the user has the ability to update or destroy both articles and comments.
|
85
91
|
|
86
|
-
You can pass a block to provide logic based on the article's attributes.
|
92
|
+
You can pass a block to provide logic based on the article's attributes.
|
87
93
|
|
88
94
|
can :update, Article do |article|
|
89
95
|
article && article.user == user
|
@@ -123,16 +129,20 @@ Use the "can?" method in the controller or view to check the user's permission f
|
|
123
129
|
|
124
130
|
can? :destroy, @project
|
125
131
|
|
126
|
-
You can also pass the class instead of an instance (if you don't have one handy).
|
132
|
+
You can also pass the class instead of an instance (if you don't have one handy).
|
127
133
|
|
128
134
|
<% if can? :create, Project %>
|
129
135
|
<%= link_to "New Project", new_project_path %>
|
130
136
|
<% end %>
|
131
137
|
|
138
|
+
The "cannot?" method is for convenience and performs the opposite check of "can?"
|
139
|
+
|
140
|
+
cannot? :destroy, @project
|
141
|
+
|
132
142
|
|
133
143
|
== Custom Actions
|
134
144
|
|
135
|
-
|
145
|
+
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.
|
136
146
|
|
137
147
|
# ability.rb
|
138
148
|
can :upload_picture, Project if user.pro?
|
@@ -142,24 +152,22 @@ There is no limit to what actions you can use to determine abilities. For exampl
|
|
142
152
|
|
143
153
|
# projects_controller.rb
|
144
154
|
def update
|
145
|
-
unauthorized! if params[:project][:upload_picture] &&
|
155
|
+
unauthorized! if params[:project][:upload_picture] && cannot?(:upload_picture, @project)
|
146
156
|
# ...
|
147
157
|
end
|
148
158
|
|
149
159
|
|
150
|
-
==
|
160
|
+
== Assumptions & Configuring
|
151
161
|
|
152
162
|
CanCan makes two assumptions about your application.
|
153
163
|
|
154
|
-
|
155
|
-
|
164
|
+
* You have an Ability class which defines the permissions.
|
165
|
+
* You have a current_user method in the controller which returns the current user model.
|
156
166
|
|
157
|
-
You can override these by
|
167
|
+
You can override these by overriding the "current_ability" method in your ApplicationController.
|
158
168
|
|
159
169
|
def current_ability
|
160
|
-
|
161
|
-
ability.prepare(current_account) # instead of current_user
|
162
|
-
ability # be sure to return the ability
|
170
|
+
UserAbility.new(current_account) # instead of Ability.new(current_user)
|
163
171
|
end
|
164
172
|
|
165
173
|
That's it!
|
@@ -174,14 +182,41 @@ For example, let's assume that each user has_many :permissions, and each permiss
|
|
174
182
|
class Ability
|
175
183
|
include CanCan::Ability
|
176
184
|
|
177
|
-
def
|
185
|
+
def initialize(user)
|
178
186
|
can :manage, :all do |action, object_class, object|
|
179
187
|
user.permissions.find_all_by_action(action).any? do |permission|
|
180
|
-
permission.object_type
|
188
|
+
permission.object_type == object_class.to_s &&
|
181
189
|
(object.nil? || permission.object_id.nil? || permission.object_id == object.id)
|
182
190
|
end
|
183
191
|
end
|
184
192
|
end
|
185
193
|
end
|
186
194
|
|
195
|
+
An alternatie approach is to define a separate "can" ability for each permission.
|
196
|
+
|
197
|
+
def initialize(user)
|
198
|
+
user.permissions.each do |permission|
|
199
|
+
can permission.action, permission.object_type.constantize do |object|
|
200
|
+
object.nil? || permission.object_id.nil? || permission.object_id == object.id
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
187
205
|
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.
|
206
|
+
|
207
|
+
|
208
|
+
== Testing Abilities
|
209
|
+
|
210
|
+
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.
|
211
|
+
|
212
|
+
def test "user can only destroy projects which he owns"
|
213
|
+
user = User.new
|
214
|
+
ability = Ability.new(user)
|
215
|
+
assert ability.can?(:destroy, Project.new(:user => user))
|
216
|
+
assert ability.cannot?(:destroy, Project.new)
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
== Special Thanks
|
221
|
+
|
222
|
+
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.
|
data/lib/cancan/ability.rb
CHANGED
@@ -4,14 +4,16 @@ module CanCan
|
|
4
4
|
|
5
5
|
def can?(original_action, target) # TODO this could use some refactoring
|
6
6
|
(@can_history || []).reverse.each do |can_action, can_target, can_block|
|
7
|
+
can_actions = [can_action].flatten
|
8
|
+
can_targets = [can_target].flatten
|
7
9
|
possible_actions_for(original_action).each do |action|
|
8
|
-
if (
|
10
|
+
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) })
|
9
11
|
if can_block.nil?
|
10
12
|
return true
|
11
13
|
else
|
12
14
|
block_args = []
|
13
|
-
block_args << action if
|
14
|
-
block_args << (target.class == Class ? target : target.class) if
|
15
|
+
block_args << action if can_actions.include?(:manage)
|
16
|
+
block_args << (target.class == Class ? target : target.class) if can_targets.include?(:all)
|
15
17
|
block_args << (target.class == Class ? nil : target)
|
16
18
|
return can_block.call(*block_args)
|
17
19
|
end
|
@@ -21,6 +23,10 @@ module CanCan
|
|
21
23
|
false
|
22
24
|
end
|
23
25
|
|
26
|
+
def cannot?(*args)
|
27
|
+
!can?(*args)
|
28
|
+
end
|
29
|
+
|
24
30
|
def possible_actions_for(initial_action)
|
25
31
|
actions = [initial_action]
|
26
32
|
(@aliased_actions || default_alias_actions).each do |target, aliases|
|
@@ -47,9 +53,5 @@ module CanCan
|
|
47
53
|
:update => [:edit],
|
48
54
|
}
|
49
55
|
end
|
50
|
-
|
51
|
-
def prepare(user)
|
52
|
-
# to be overriden by included class
|
53
|
-
end
|
54
56
|
end
|
55
57
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module CanCan
|
2
2
|
module ControllerAdditions
|
3
3
|
def self.included(base)
|
4
|
-
base.helper_method :can?
|
4
|
+
base.helper_method :can?, :cannot?
|
5
5
|
end
|
6
6
|
|
7
7
|
def unauthorized!
|
@@ -9,27 +9,31 @@ module CanCan
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def current_ability
|
12
|
-
|
13
|
-
ability.prepare(current_user)
|
14
|
-
ability
|
12
|
+
::Ability.new(current_user)
|
15
13
|
end
|
16
14
|
|
17
15
|
def can?(*args)
|
18
16
|
(@current_ability ||= current_ability).can?(*args)
|
19
17
|
end
|
20
18
|
|
19
|
+
def cannot?(*args)
|
20
|
+
(@current_ability ||= current_ability).cannot?(*args)
|
21
|
+
end
|
22
|
+
|
21
23
|
def load_resource # TODO this could use some refactoring
|
24
|
+
model_name = params[:controller].split('/').last.singularize
|
22
25
|
unless params[:action] == "index"
|
23
26
|
if params[:id]
|
24
|
-
instance_variable_set("@#{
|
27
|
+
instance_variable_set("@#{model_name}", model_name.camelcase.constantize.find(params[:id]))
|
25
28
|
else
|
26
|
-
instance_variable_set("@#{
|
29
|
+
instance_variable_set("@#{model_name}", model_name.camelcase.constantize.new(params[model_name.to_sym]))
|
27
30
|
end
|
28
31
|
end
|
29
32
|
end
|
30
33
|
|
31
34
|
def authorize_resource # TODO this could use some refactoring
|
32
|
-
|
35
|
+
model_name = params[:controller].split('/').last.singularize
|
36
|
+
unauthorized! unless can?(params[:action].to_sym, instance_variable_get("@#{model_name}") || model_name.camelcase.constantize)
|
33
37
|
end
|
34
38
|
|
35
39
|
def load_and_authorize_resource
|
@@ -43,4 +47,4 @@ if defined? ActionController
|
|
43
47
|
ActionController::Base.class_eval do
|
44
48
|
include CanCan::ControllerAdditions
|
45
49
|
end
|
46
|
-
end
|
50
|
+
end
|
data/spec/cancan/ability_spec.rb
CHANGED
@@ -78,7 +78,25 @@ describe CanCan::Ability do
|
|
78
78
|
@ability.can?(:edit, 123).should == :update_called
|
79
79
|
end
|
80
80
|
|
81
|
-
it "should respond to prepare" do
|
82
|
-
@ability.
|
81
|
+
it "should not respond to prepare (now using initialize)" do
|
82
|
+
@ability.should_not respond_to(:prepare)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should offer cannot? method which is simply invert of can?" do
|
86
|
+
@ability.cannot?(:tie, String).should be_true
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should be able to specify multiple actions and match any" do
|
90
|
+
@ability.can [:read, :update], :all
|
91
|
+
@ability.can?(:read, 123).should be_true
|
92
|
+
@ability.can?(:update, 123).should be_true
|
93
|
+
@ability.can?(:count, 123).should be_false
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should be able to specify multiple classes and match any" do
|
97
|
+
@ability.can :update, [String, Array]
|
98
|
+
@ability.can?(:update, "foo").should be_true
|
99
|
+
@ability.can?(:update, []).should be_true
|
100
|
+
@ability.can?(:update, 123).should be_false
|
83
101
|
end
|
84
102
|
end
|
@@ -2,13 +2,16 @@ require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
2
|
|
3
3
|
class Ability
|
4
4
|
include CanCan::Ability
|
5
|
+
|
6
|
+
def initialize(user)
|
7
|
+
end
|
5
8
|
end
|
6
9
|
|
7
10
|
describe CanCan::ControllerAdditions do
|
8
11
|
before(:each) do
|
9
12
|
@controller_class = Class.new
|
10
13
|
@controller = @controller_class.new
|
11
|
-
mock(@controller_class).helper_method(:can?)
|
14
|
+
mock(@controller_class).helper_method(:can?, :cannot?)
|
12
15
|
@controller_class.send(:include, CanCan::ControllerAdditions)
|
13
16
|
end
|
14
17
|
|
@@ -23,10 +26,11 @@ describe CanCan::ControllerAdditions do
|
|
23
26
|
@controller.current_ability.should be_kind_of(Ability)
|
24
27
|
end
|
25
28
|
|
26
|
-
it "should provide a can?
|
29
|
+
it "should provide a can? and cannot? methods which go through the current ability" do
|
27
30
|
stub(@controller).current_user { :current_user }
|
28
31
|
@controller.current_ability.should be_kind_of(Ability)
|
29
32
|
@controller.can?(:foo, :bar).should be_false
|
33
|
+
@controller.cannot?(:foo, :bar).should be_true
|
30
34
|
end
|
31
35
|
|
32
36
|
it "should load the resource if params[:id] is specified" do
|
@@ -78,4 +82,11 @@ describe CanCan::ControllerAdditions do
|
|
78
82
|
stub(@controller).authorize_resource
|
79
83
|
@controller.load_and_authorize_resource
|
80
84
|
end
|
85
|
+
|
86
|
+
it "should properly load resource for namespaced controller" do
|
87
|
+
stub(@controller).params { {:controller => "admin/abilities", :action => "show", :id => 123} }
|
88
|
+
stub(Ability).find(123) { :some_resource }
|
89
|
+
@controller.load_resource
|
90
|
+
@controller.instance_variable_get(:@ability).should == :some_resource
|
91
|
+
end
|
81
92
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cancan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Bates
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-11-
|
12
|
+
date: 2009-11-17 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|