cancan 1.2.0 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,41 @@
1
+ 1.3.2 (August 7, 2010)
2
+
3
+ * Fixing slice error when passing in custom resource name - see issue #112
4
+
5
+
6
+ 1.3.1 (August 6, 2010)
7
+
8
+ * Fixing protected sanitize_sql error - see issue #111
9
+
10
+
11
+ 1.3.0 (August 6, 2010)
12
+
13
+ * Adding :find_by option to load_resource - see issue #19
14
+
15
+ * Adding :singleton option to load_resource - see issue #93
16
+
17
+ * Supporting multiple resources in :through option for polymorphic
18
+ associations - see issue #73
19
+
20
+ * Supporting Single Table Inheritance for "can" comparisons - see issue #55
21
+
22
+ * Adding :instance_name option to load/authorize_resource - see issue #44
23
+
24
+ * Don't pass nil to "new" to keep MongoMapper happy - see issue #63
25
+
26
+ * Parent resources are now authorized with :read action.
27
+
28
+ * Changing :resource option in load/authorize_resource back to :class with ability to pass false
29
+
30
+ * Removing :nested option in favor of :through option with separate load/authorize call
31
+
32
+ * Moving internal logic from ResourceAuthorization to ControllerResource class
33
+
34
+ * Supporting multiple "can" and "cannot" calls with accessible_by (thanks funny-falcon) - see issue #71
35
+
36
+ * Supporting deeply nested aliases - see issue #98
37
+
38
+
1
39
  1.2.0 (July 16, 2010)
2
40
 
3
41
  * Load nested parent resources on collection actions such as "index" (thanks dohzya)
data/README.rdoc CHANGED
@@ -1,18 +1,22 @@
1
1
  = CanCan
2
2
 
3
- Wiki[http://wiki.github.com/ryanb/cancan] | RDocs[http://rdoc.info/projects/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan] | Metrics[http://getcaliper.com/caliper/project?repo=git%3A%2F%2Fgithub.com%2Fryanb%2Fcancan.git]
3
+ Wiki[http://wiki.github.com/ryanb/cancan] | RDocs[http://rdoc.info/projects/ryanb/cancan] | Screencast[http://railscasts.com/episodes/192-authorization-with-cancan]
4
4
 
5
- CanCan is an authorization solution for Ruby on Rails. This restricts what a given user is allowed to access throughout the application. It is completely decoupled from any role based implementation and focusses on keeping permission logic in a single location (the +Ability+ class) so it is not duplicated across controllers, views, and database queries.
5
+ CanCan is an authorization solution for Ruby on Rails for restricting what a given user is allowed to access throughout the application. It does not care how your user roles are defined, it simply focusses on keeping permission logic in a single location (the +Ability+ class) so it is not duplicated across controllers, views, and database queries.
6
6
 
7
- This assumes you already have authentication (such as Authlogic[http://github.com/binarylogic/authlogic] or Devise[http://github.com/plataformatec/devise]) that provides a +current_user+ method which CanCan relies on. See {Changing Defaults}[http://wiki.github.com/ryanb/cancan/changing-defaults] if you need different behavior.
7
+ By default, the +current_user+ method is required, so if you have not already, set up some authentication (such as Authlogic[http://github.com/binarylogic/authlogic] or Devise[http://github.com/plataformatec/devise]). See {Changing Defaults}[http://wiki.github.com/ryanb/cancan/changing-defaults] if you need different behavior.
8
8
 
9
9
 
10
10
  == Installation
11
11
 
12
- CanCan is provided as a gem. Simply include it in your environment.rb or Gemfile.
12
+ To install CanCan, include the gem in the environment.rb in Rails 2.3.
13
13
 
14
14
  config.gem "cancan"
15
15
 
16
+ Or the Gemfile in Rails 3.
17
+
18
+ gem "cancan"
19
+
16
20
  Alternatively it can be installed as a plugin.
17
21
 
18
22
  script/plugin install git://github.com/ryanb/cancan.git
@@ -20,11 +24,11 @@ Alternatively it can be installed as a plugin.
20
24
 
21
25
  == Getting Started
22
26
 
23
- First, define a class called +Ability+ in "models/ability.rb". It should look something like this.
27
+ First, define a class called +Ability+ in "models/ability.rb" or anywhere else in the load path. It should look something like this.
24
28
 
25
29
  class Ability
26
30
  include CanCan::Ability
27
-
31
+
28
32
  def initialize(user)
29
33
  if user.admin?
30
34
  can :manage, :all
@@ -55,7 +59,7 @@ Setting this for every action can be tedious, therefore the +load_and_authorize_
55
59
 
56
60
  class ArticlesController < ApplicationController
57
61
  load_and_authorize_resource
58
-
62
+
59
63
  def show
60
64
  # @article is already loaded and authorized
61
65
  end
@@ -136,7 +140,8 @@ See {Fetching Records}[http://wiki.github.com/ryanb/cancan/fetching-records] for
136
140
 
137
141
  == Additional Docs
138
142
 
139
- * {Upgrading to 1.1}[http://wiki.github.com/ryanb/cancan/upgrading-to-11]
143
+ * {Upgrading to 1.3}[http://wiki.github.com/ryanb/cancan/upgrading-to-13]
144
+ * {Nested Resources}[http://wiki.github.com/ryanb/cancan/nested-resources]
140
145
  * {Testing Abilities}[http://wiki.github.com/ryanb/cancan/testing-abilities]
141
146
  * {Accessing Request Data}[http://wiki.github.com/ryanb/cancan/accessing-request-data]
142
147
  * {Admin Namespace}[http://wiki.github.com/ryanb/cancan/admin-namespace]
@@ -144,4 +149,4 @@ See {Fetching Records}[http://wiki.github.com/ryanb/cancan/fetching-records] for
144
149
 
145
150
  == Special Thanks
146
151
 
147
- 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.
152
+ CanCan was inspired by declarative_authorization[http://github.com/stffn/declarative_authorization/] and aegis[http://github.com/makandra/aegis]. Also many thanks to the CanCan contributors[http://github.com/ryanb/cancan/contributors]. See the CHANGELOG[http://github.com/ryanb/cancan/blob/master/CHANGELOG.rdoc] for the full list.
data/Rakefile CHANGED
@@ -1,13 +1,13 @@
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
12
-
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
12
+
13
13
  task :default => :spec
data/lib/cancan.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'cancan/ability'
2
2
  require 'cancan/can_definition'
3
3
  require 'cancan/controller_resource'
4
- require 'cancan/resource_authorization'
5
4
  require 'cancan/controller_additions'
6
5
  require 'cancan/active_record_additions'
7
6
  require 'cancan/exceptions'
7
+ require 'cancan/query'
@@ -50,8 +50,10 @@ module CanCan
50
50
  # Also see the RSpec Matchers to aid in testing.
51
51
  def can?(action, subject, *extra_args)
52
52
  raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
53
- can_definition = matching_can_definition(action, subject)
54
- can_definition && can_definition.can?(action, subject, extra_args)
53
+ match = relevant_can_definitions(action, subject).detect do |can_definition|
54
+ can_definition.matches_conditions?(action, subject, extra_args)
55
+ end
56
+ match ? match.base_behavior : false
55
57
  end
56
58
 
57
59
  # Convenience method which works the same as "can?" but returns the opposite value.
@@ -180,47 +182,42 @@ module CanCan
180
182
  @aliased_actions = {}
181
183
  end
182
184
 
183
- # Returns a hash of conditions which match the given ability. This is useful if you need to generate a database
184
- # query based on the current ability.
185
- #
186
- # can :read, Article, :visible => true
187
- # conditions :read, Article # returns { :visible => true }
188
- #
189
- # Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
190
- #
191
- # If the ability is not defined then false is returned so be sure to take that into consideration.
192
- # If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
193
- # determined from that.
194
- def conditions(action, subject, options = {})
195
- can_definition = matching_can_definition(action, subject)
196
- if can_definition
197
- raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if can_definition.block
198
- can_definition.conditions(options) || {}
199
- else
200
- false
201
- end
202
- end
203
-
204
- # Returns the associations used in conditions. This is usually used in the :joins option for a search.
205
- # See ActiveRecordAdditions#accessible_by for use in Active Record.
206
- def association_joins(action, subject)
207
- can_definition = matching_can_definition(action, subject)
208
- if can_definition
209
- raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block
210
- can_definition.association_joins
211
- end
185
+ # Returns a CanCan::Query instance to help generate database queries based on the ability.
186
+ # If any relevant can definitions use a block then an exception will be raised because an
187
+ # SQL query cannot be generated from blocks of code.
188
+ def query(action, subject)
189
+ Query.new(subject, relevant_can_definitions_for_query(action, subject))
212
190
  end
213
191
 
214
192
  private
215
193
 
194
+ # Accepts a hash of aliased actions and returns an array of actions which match.
195
+ # This should be called before "matches?" and other checking methods since they
196
+ # rely on the actions to be expanded.
197
+ def expand_actions(actions)
198
+ actions.map do |action|
199
+ aliased_actions[action] ? [action, *expand_actions(aliased_actions[action])] : action
200
+ end.flatten
201
+ end
202
+
216
203
  def can_definitions
217
204
  @can_definitions ||= []
218
205
  end
219
206
 
220
- def matching_can_definition(action, subject)
221
- can_definitions.reverse.detect do |can_definition|
222
- can_definition.expand_actions(aliased_actions)
223
- can_definition.matches? action, subject
207
+ # Returns an array of CanDefinition instances which match the action and subject
208
+ # This does not take into consideration any hash conditions or block statements
209
+ def relevant_can_definitions(action, subject)
210
+ can_definitions.reverse.select do |can_definition|
211
+ can_definition.expanded_actions = expand_actions(can_definition.actions)
212
+ can_definition.relevant? action, subject
213
+ end
214
+ end
215
+
216
+ def relevant_can_definitions_for_query(action, subject)
217
+ relevant_can_definitions(action, subject).each do |can_definition|
218
+ if can_definition.only_block?
219
+ raise Error, "Cannot determine SQL conditions or joins from block for #{action.inspect} #{subject.inspect}"
220
+ end
224
221
  end
225
222
  end
226
223
 
@@ -20,12 +20,11 @@ module CanCan
20
20
  # Here only the articles which the user can update are returned. This
21
21
  # internally uses Ability#conditions method, see that for more information.
22
22
  def accessible_by(ability, action = :read)
23
- conditions = ability.conditions(action, self, :tableize => true) || {:id => nil}
24
- joins = ability.association_joins(action, self)
23
+ query = ability.query(action, self)
25
24
  if respond_to? :where
26
- where(conditions).joins(joins)
25
+ where(query.conditions).joins(query.joins)
27
26
  else
28
- scoped(:conditions => conditions, :joins => joins)
27
+ scoped(:conditions => query.conditions, :joins => query.joins)
29
28
  end
30
29
  end
31
30
  end
@@ -3,9 +3,9 @@ module CanCan
3
3
  # it holds the information about a "can" call made on Ability and provides
4
4
  # helpful methods to determine permission checking and conditions hash generation.
5
5
  class CanDefinition # :nodoc:
6
- include ActiveSupport::Inflector
7
- attr_reader :block
8
-
6
+ attr_reader :base_behavior, :actions
7
+ attr_writer :expanded_actions
8
+
9
9
  # The first argument when initializing is the base_behavior which is a true/false
10
10
  # value. True for "can" and false for "cannot". The next two arguments are the action
11
11
  # and subject respectively (such as :read, @project). The third argument is a hash
@@ -18,51 +18,47 @@ module CanCan
18
18
  @block = block
19
19
  end
20
20
 
21
- # Accepts a hash of aliased actions and returns an array of actions which match.
22
- # This should be called before "matches?" and other checking methods since they
23
- # rely on the actions to be expanded.
24
- def expand_actions(aliased_actions)
25
- @expanded_actions = @actions.map do |action|
26
- aliased_actions[action] ? [action, *aliased_actions[action]] : action
27
- end.flatten
28
- end
29
-
30
- def matches?(action, subject)
21
+ # Matches both the subject and action, not necessarily the conditions
22
+ def relevant?(action, subject)
31
23
  matches_action?(action) && matches_subject?(subject)
32
24
  end
33
25
 
34
- def can?(action, subject, extra_args)
35
- result = can_without_base_behavior?(action, subject, extra_args)
36
- @base_behavior ? result : !result
26
+ # Matches the block or conditions hash
27
+ def matches_conditions?(action, subject, extra_args)
28
+ if @block
29
+ call_block(action, subject, extra_args)
30
+ elsif @conditions.kind_of?(Hash) && subject.class != Class
31
+ matches_conditions_hash?(subject)
32
+ else
33
+ true
34
+ end
37
35
  end
38
36
 
39
- # Returns a hash of conditions. If the ":tableize => true" option is passed
40
- # it will pluralize the association conditions to match the table name.
41
- def conditions(options = {})
42
- if options[:tableize] && @conditions.kind_of?(Hash)
37
+ # Returns a hash of conditions, pluralizing the table names
38
+ def tableized_conditions
39
+ if @conditions
43
40
  @conditions.inject({}) do |tableized_conditions, (name, value)|
44
- name = tableize(name).to_sym if value.kind_of? Hash
41
+ name = name.to_s.tableize.to_sym if value.kind_of? Hash
45
42
  tableized_conditions[name] = value
46
43
  tableized_conditions
47
44
  end
48
- else
49
- @conditions
50
45
  end
51
46
  end
52
47
 
53
- def association_joins(conditions = @conditions)
54
- joins = []
55
- conditions.each do |name, value|
56
- if value.kind_of? Hash
57
- nested = association_joins(value)
58
- if nested
59
- joins << {name => nested}
60
- else
61
- joins << name
62
- end
63
- end
48
+ def only_block?
49
+ conditions_empty? && !@block.nil?
50
+ end
51
+
52
+ def conditions_empty?
53
+ @conditions == {} || @conditions.nil?
54
+ end
55
+
56
+ def associations_hash(conditions = @conditions)
57
+ hash = {}
58
+ conditions.map do |name, value|
59
+ hash[name] = associations_hash(value) if value.kind_of? Hash
64
60
  end
65
- joins unless joins.empty?
61
+ hash
66
62
  end
67
63
 
68
64
  private
@@ -72,27 +68,21 @@ module CanCan
72
68
  end
73
69
 
74
70
  def matches_subject?(subject)
75
- @subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) }
71
+ @subjects.include?(:all) || @subjects.include?(subject) || matches_subject_class?(subject)
76
72
  end
77
73
 
78
- def can_without_base_behavior?(action, subject, extra_args)
79
- if @block
80
- call_block(action, subject, extra_args)
81
- elsif @conditions && subject.class != Class
82
- matches_conditions? subject
83
- else
84
- true
85
- end
74
+ def matches_subject_class?(subject)
75
+ @subjects.any? { |sub| sub.kind_of?(Class) && (subject.kind_of?(sub) || subject.kind_of?(Class) && subject.ancestors.include?(sub)) }
86
76
  end
87
77
 
88
- def matches_conditions?(subject, conditions = @conditions)
78
+ def matches_conditions_hash?(subject, conditions = @conditions)
89
79
  conditions.all? do |name, value|
90
80
  attribute = subject.send(name)
91
81
  if value.kind_of?(Hash)
92
82
  if attribute.kind_of? Array
93
- attribute.any? { |element| matches_conditions? element, value }
83
+ attribute.any? { |element| matches_conditions_hash? element, value }
94
84
  else
95
- matches_conditions? attribute, value
85
+ matches_conditions_hash? attribute, value
96
86
  end
97
87
  elsif value.kind_of?(Array) || value.kind_of?(Range)
98
88
  value.include? attribute
@@ -11,11 +11,11 @@ module CanCan
11
11
  # load_and_authorize_resource
12
12
  # end
13
13
  #
14
- def load_and_authorize_resource(options = {})
15
- ResourceAuthorization.add_before_filter(self, :load_and_authorize_resource, options)
14
+ def load_and_authorize_resource(*args)
15
+ ControllerResource.add_before_filter(self, :load_and_authorize_resource, *args)
16
16
  end
17
17
 
18
- # Sets up a before filter which loads the appropriate model resource into an instance variable.
18
+ # Sets up a before filter which loads the model resource into an instance variable.
19
19
  # For example, given an ArticlesController it will load the current article into the @article
20
20
  # instance variable. It does this by either calling Article.find(params[:id]) or
21
21
  # Article.new(params[:article]) depending upon the action. It does nothing for the "index"
@@ -41,6 +41,20 @@ module CanCan
41
41
  # end
42
42
  # end
43
43
  #
44
+ # If a name is provided which does not match the controller it assumes it is a parent resource. Child
45
+ # resources can then be loaded through it.
46
+ #
47
+ # class BooksController < ApplicationController
48
+ # load_resource :author
49
+ # load_resource :book, :through => :author
50
+ # end
51
+ #
52
+ # Here the author resource will be loaded before each action using params[:author_id]. The book resource
53
+ # will then be loaded through the @author instance variable.
54
+ #
55
+ # That first argument is optional and will default to the singular name of the controller.
56
+ # A hash of options (see below) can also be passed to this method to further customize it.
57
+ #
44
58
  # See load_and_authorize_resource to automatically authorize the resource too.
45
59
  #
46
60
  # Options:
@@ -50,27 +64,30 @@ module CanCan
50
64
  # [:+except+]
51
65
  # Does not apply before filter to given actions.
52
66
  #
53
- # [:+nested+]
54
- # Specify which resource this is nested under.
67
+ # [:+through+]
68
+ # Load this resource through another one. This should match the name of the parent instance variable.
55
69
  #
56
- # load_resource :nested => :author
70
+ # [:+singleton+]
71
+ # Pass +true+ if this is a singleton resource through a +has_one+ association.
57
72
  #
58
- # Deep nesting can be defined in an array.
73
+ # [:+parent+]
74
+ # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
75
+ # name is given which does not match the controller.
59
76
  #
60
- # load_resource :nested => [:publisher, :author]
77
+ # [:+class+]
78
+ # The class to use for the model (string or constant).
61
79
  #
62
- # [:+name+]
63
- # The name of the resource if it cannot be determined from controller (string or symbol).
80
+ # [:+instance_name+]
81
+ # The name of the instance variable to load the resource into.
64
82
  #
65
- # load_resource :name => :article
83
+ # [:+find_by+]
84
+ # Find using a different attribute other than id. For example.
66
85
  #
67
- # [:+resource+]
68
- # The class to use for the model (string or constant).
86
+ # load_resource :find_by => :permalink # will use find_by_permlink!(params[:id])
69
87
  #
70
88
  # [:+collection+]
71
89
  # Specify which actions are resource collection actions in addition to :+index+. This
72
- # is usually not necessary because it will try to guess depending on if an :+id+
73
- # is present in +params+.
90
+ # is usually not necessary because it will try to guess depending on if the id param is present.
74
91
  #
75
92
  # load_resource :collection => [:sort, :list]
76
93
  #
@@ -81,11 +98,11 @@ module CanCan
81
98
  #
82
99
  # load_resource :new => :build
83
100
  #
84
- def load_resource(options = {})
85
- ResourceAuthorization.add_before_filter(self, :load_resource, options)
101
+ def load_resource(*args)
102
+ ControllerResource.add_before_filter(self, :load_resource, *args)
86
103
  end
87
104
 
88
- # Sets up a before filter which authorizes the current resource using the instance variable.
105
+ # Sets up a before filter which authorizes the resource using the instance variable.
89
106
  # For example, if you have an ArticlesController it will check the @article instance variable
90
107
  # and ensure the user can perform the current action on it. Under the hood it is doing
91
108
  # something like the following.
@@ -98,6 +115,19 @@ module CanCan
98
115
  # authorize_resource
99
116
  # end
100
117
  #
118
+ # If you pass in the name of a resource which does not match the controller it will assume
119
+ # it is a parent resource.
120
+ #
121
+ # class BooksController < ApplicationController
122
+ # authorize_resource :author
123
+ # authorize_resource :book
124
+ # end
125
+ #
126
+ # Here it will authorize :+show+, @+author+ on every action before authorizing the book.
127
+ #
128
+ # That first argument is optional and will default to the singular name of the controller.
129
+ # A hash of options (see below) can also be passed to this method to further customize it.
130
+ #
101
131
  # See load_and_authorize_resource to automatically load the resource too.
102
132
  #
103
133
  # Options:
@@ -107,17 +137,19 @@ module CanCan
107
137
  # [:+except+]
108
138
  # Does not apply before filter to given actions.
109
139
  #
110
- # [:+name+]
111
- # The name of the resource if it cannot be determined from controller (string or symbol).
140
+ # [:+parent+]
141
+ # True or false depending on if the resource is considered a parent resource. This defaults to +true+ if a resource
142
+ # name is given which does not match the controller.
112
143
  #
113
- # load_resource :name => :article
144
+ # [:+class+]
145
+ # The class to use for the model (string or constant). This passed in when the instance variable is not set.
146
+ # Pass +false+ if there is no associated class for this resource and it will use a symbol of the resource name.
114
147
  #
115
- # [:+resource+]
116
- # The class to use for the model (string or constant). Alternatively pass a symbol
117
- # to represent a resource which does not have a class.
148
+ # [:+instance_name+]
149
+ # The name of the instance variable for this resource.
118
150
  #
119
- def authorize_resource(options = {})
120
- ResourceAuthorization.add_before_filter(self, :authorize_resource, options)
151
+ def authorize_resource(*args)
152
+ ControllerResource.add_before_filter(self, :authorize_resource, *args)
121
153
  end
122
154
  end
123
155