cancan 1.2.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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