marnen-cancan 2.0.0.alpha.pre.f1cebde51a87be149b4970a3287826bb63c0ac0b

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.
Files changed (41) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG.rdoc +381 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +108 -0
  6. data/Rakefile +18 -0
  7. data/init.rb +1 -0
  8. data/lib/cancan.rb +13 -0
  9. data/lib/cancan/ability.rb +348 -0
  10. data/lib/cancan/controller_additions.rb +392 -0
  11. data/lib/cancan/controller_resource.rb +265 -0
  12. data/lib/cancan/exceptions.rb +53 -0
  13. data/lib/cancan/inherited_resource.rb +20 -0
  14. data/lib/cancan/matchers.rb +14 -0
  15. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  16. data/lib/cancan/model_adapters/active_record_adapter.rb +172 -0
  17. data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
  18. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  19. data/lib/cancan/model_adapters/mongoid_adapter.rb +54 -0
  20. data/lib/cancan/model_additions.rb +29 -0
  21. data/lib/cancan/rule.rb +178 -0
  22. data/lib/generators/cancan/ability/USAGE +5 -0
  23. data/lib/generators/cancan/ability/ability_generator.rb +16 -0
  24. data/lib/generators/cancan/ability/templates/ability.rb +24 -0
  25. data/lib/generators/cancan/ability/templates/ability_spec.rb +16 -0
  26. data/lib/generators/cancan/ability/templates/ability_test.rb +10 -0
  27. data/spec/README.rdoc +28 -0
  28. data/spec/cancan/ability_spec.rb +541 -0
  29. data/spec/cancan/controller_additions_spec.rb +118 -0
  30. data/spec/cancan/controller_resource_spec.rb +535 -0
  31. data/spec/cancan/exceptions_spec.rb +58 -0
  32. data/spec/cancan/inherited_resource_spec.rb +58 -0
  33. data/spec/cancan/matchers_spec.rb +33 -0
  34. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
  35. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +120 -0
  36. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  37. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +227 -0
  38. data/spec/cancan/rule_spec.rb +55 -0
  39. data/spec/matchers.rb +13 -0
  40. data/spec/spec_helper.rb +49 -0
  41. metadata +197 -0
@@ -0,0 +1,265 @@
1
+ module CanCan
2
+ # Handle the load and authorization controller logic so we don't clutter up all controllers with non-interface methods.
3
+ # This class is used internally, so you do not need to call methods directly on it.
4
+ class ControllerResource # :nodoc:
5
+ def self.add_before_filter(controller_class, behavior, *args)
6
+ options = args.extract_options!.merge(behavior)
7
+ resource_name = args.first
8
+ before_filter_method = options.delete(:prepend) ? :prepend_before_filter : :before_filter
9
+ controller_class.send(before_filter_method, options.slice(:only, :except, :if, :unless)) do |controller|
10
+ controller.class.cancan_resource_class.new(controller, resource_name, options.except(:only, :except, :if, :unless)).process
11
+ end
12
+ end
13
+
14
+ def initialize(controller, *args)
15
+ @controller = controller
16
+ @params = controller.params
17
+ @options = args.extract_options!
18
+ @name = args.first
19
+ end
20
+
21
+ def process
22
+ if @options[:load]
23
+ if load_instance?
24
+ self.resource_instance ||= load_resource_instance
25
+ elsif load_collection?
26
+ self.collection_instance ||= load_collection
27
+ current_ability.fully_authorized! @params[:action], @params[:controller]
28
+ end
29
+ end
30
+ if @options[:authorize]
31
+ if resource_instance
32
+ if resource_params && (authorization_action == :create || authorization_action == :update)
33
+ resource_params.each do |key, value|
34
+ @controller.authorize!(authorization_action, resource_instance, key.to_sym)
35
+ end
36
+ else
37
+ @controller.authorize!(authorization_action, resource_instance)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def parent?
44
+ @options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
45
+ end
46
+
47
+ # def skip?(behavior) # This could probably use some refactoring
48
+ # options = @controller.class.cancan_skipper[behavior][@name]
49
+ # if options.nil?
50
+ # false
51
+ # elsif options == {}
52
+ # true
53
+ # elsif options[:except] && ![options[:except]].flatten.include?(@params[:action].to_sym)
54
+ # true
55
+ # elsif [options[:only]].flatten.include?(@params[:action].to_sym)
56
+ # true
57
+ # end
58
+ # end
59
+
60
+ protected
61
+
62
+ def load_resource_instance
63
+ if !parent? && new_actions.include?(@params[:action].to_sym)
64
+ build_resource
65
+ elsif id_param || @options[:singleton]
66
+ find_and_update_resource
67
+ end
68
+ end
69
+
70
+ def load_instance?
71
+ parent? || member_action?
72
+ end
73
+
74
+ def load_collection?
75
+ resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, subject_name)
76
+ end
77
+
78
+ def load_collection
79
+ resource_base.accessible_by(current_ability, authorization_action)
80
+ end
81
+
82
+ def build_resource
83
+ resource = resource_base.new(resource_params || {})
84
+ assign_attributes(resource)
85
+ end
86
+
87
+ def assign_attributes(resource)
88
+ resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
89
+ initial_attributes.each do |attr_name, value|
90
+ resource.send("#{attr_name}=", value)
91
+ end
92
+ resource
93
+ end
94
+
95
+ def initial_attributes
96
+ current_ability.attributes_for(@params[:action].to_sym, subject_name).delete_if do |key, value|
97
+ resource_params && resource_params.include?(key)
98
+ end
99
+ end
100
+
101
+ def find_and_update_resource
102
+ resource = find_resource
103
+ if resource_params
104
+ @controller.authorize!(authorization_action, resource) if @options[:authorize]
105
+ resource.attributes = resource_params
106
+ end
107
+ resource
108
+ end
109
+
110
+ def find_resource
111
+ if @options[:singleton] && parent_resource.respond_to?(name)
112
+ parent_resource.send(name)
113
+ else
114
+ if @options[:find_by]
115
+ if resource_base.respond_to? "find_by_#{@options[:find_by]}!"
116
+ resource_base.send("find_by_#{@options[:find_by]}!", id_param)
117
+ else
118
+ resource_base.send(@options[:find_by], id_param)
119
+ end
120
+ else
121
+ adapter.find(resource_base, id_param)
122
+ end
123
+ end
124
+ end
125
+
126
+ def adapter
127
+ ModelAdapters::AbstractAdapter.adapter_class(resource_class)
128
+ end
129
+
130
+ def authorization_action
131
+ parent? ? :show : @params[:action].to_sym
132
+ end
133
+
134
+ def id_param
135
+ if @options[:id_param]
136
+ @params[@options[:id_param]]
137
+ else
138
+ @params[parent? ? :"#{name}_id" : :id]
139
+ end
140
+ end
141
+
142
+ def member_action?
143
+ new_actions.include?(@params[:action].to_sym) || @options[:singleton] || ( (@params[:id] || @params[@options[:id_param]]) && !collection_actions.include?(@params[:action].to_sym))
144
+ end
145
+
146
+ # Returns the class used for this resource. This can be overriden by the :class option.
147
+ # If +false+ is passed in it will use the resource name as a symbol in which case it should
148
+ # only be used for authorization, not loading since there's no class to load through.
149
+ def resource_class
150
+ case @options[:class]
151
+ when false then name.to_sym
152
+ when nil then namespaced_name.to_s.camelize.constantize
153
+ when String then @options[:class].constantize
154
+ else @options[:class]
155
+ end
156
+ end
157
+
158
+ def subject_name
159
+ resource_class.to_s.underscore.pluralize.to_sym
160
+ end
161
+
162
+ def subject_name_with_parent
163
+ parent_resource ? {parent_resource => subject_name} : subject_name
164
+ end
165
+
166
+ def resource_instance=(instance)
167
+ @controller.instance_variable_set("@#{instance_name}", instance)
168
+ end
169
+
170
+ def resource_instance
171
+ if load_instance?
172
+ if @controller.instance_variable_defined? "@#{instance_name}"
173
+ @controller.instance_variable_get("@#{instance_name}")
174
+ elsif @controller.respond_to?(instance_name, true)
175
+ @controller.send(instance_name)
176
+ end
177
+ end
178
+ end
179
+
180
+ def collection_instance=(instance)
181
+ @controller.instance_variable_set("@#{instance_name.to_s.pluralize}", instance)
182
+ end
183
+
184
+ def collection_instance
185
+ @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
186
+ end
187
+
188
+ # The object that methods (such as "find", "new" or "build") are called on.
189
+ # If the :through option is passed it will go through an association on that instance.
190
+ # If the :shallow option is passed it will use the resource_class if there's no parent
191
+ # If the :singleton option is passed it won't use the association because it needs to be handled later.
192
+ def resource_base
193
+ if @options[:through]
194
+ if parent_resource
195
+ @options[:singleton] ? resource_class : parent_resource.send(@options[:through_association] || name.to_s.pluralize)
196
+ elsif @options[:shallow]
197
+ resource_class
198
+ else
199
+ raise Unauthorized.new(nil, authorization_action, @params[:controller].to_sym) # maybe this should be a record not found error instead?
200
+ end
201
+ else
202
+ resource_class
203
+ end
204
+ end
205
+
206
+ def parent_name
207
+ @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
208
+ end
209
+
210
+ # The object to load this resource through.
211
+ def parent_resource
212
+ parent_name && fetch_parent(parent_name)
213
+ end
214
+
215
+ def fetch_parent(name)
216
+ if @controller.instance_variable_defined? "@#{name}"
217
+ @controller.instance_variable_get("@#{name}")
218
+ elsif @controller.respond_to?(name, true)
219
+ @controller.send(name)
220
+ end
221
+ end
222
+
223
+ def current_ability
224
+ @controller.send(:current_ability)
225
+ end
226
+
227
+ def name
228
+ @name || name_from_controller
229
+ end
230
+
231
+ def resource_params
232
+ if @options[:class]
233
+ @params[@options[:class].to_s.underscore.gsub('/', '_')]
234
+ else
235
+ @params[namespaced_name.to_s.underscore.gsub("/", "_")]
236
+ end
237
+ end
238
+
239
+ def namespace
240
+ @params[:controller].split(/::|\//)[0..-2]
241
+ end
242
+
243
+ def namespaced_name
244
+ [namespace, name.camelize].join('::').singularize.camelize.constantize
245
+ rescue NameError
246
+ name
247
+ end
248
+
249
+ def name_from_controller
250
+ @params[:controller].sub("Controller", "").underscore.split('/').last.singularize
251
+ end
252
+
253
+ def instance_name
254
+ @options[:instance_name] || name
255
+ end
256
+
257
+ def collection_actions
258
+ [:index] + [@options[:collection]].flatten
259
+ end
260
+
261
+ def new_actions
262
+ [:new, :create] + [@options[:new]].flatten
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,53 @@
1
+ module CanCan
2
+ # A general CanCan exception
3
+ class Error < StandardError; end
4
+
5
+ # Raised when behavior is not implemented, usually used in an abstract class.
6
+ class NotImplemented < Error; end
7
+
8
+ # Raised when removed code is called, an alternative solution is provided in message.
9
+ class ImplementationRemoved < Error; end
10
+
11
+ # Raised when using check_authorization without calling authorized!
12
+ class AuthorizationNotPerformed < Error; end
13
+
14
+ # Raised when enable_authorization is used and not fully authorized by the end of the action
15
+ class InsufficientAuthorizationCheck < Error; end
16
+
17
+ # This error is raised when a user isn't allowed to access a given controller action.
18
+ # This usually happens within a call to ControllerAdditions#authorize! but can be
19
+ # raised manually.
20
+ #
21
+ # raise CanCan::Unauthorized.new("Not authorized!", :read, Article)
22
+ #
23
+ # The passed message, action, and subject are optional and can later be retrieved when
24
+ # rescuing from the exception.
25
+ #
26
+ # exception.message # => "Not authorized!"
27
+ # exception.action # => :read
28
+ # exception.subject # => Article
29
+ #
30
+ # If the message is not specified (or is nil) it will default to "You are not authorized
31
+ # to access this page." This default can be overridden by setting default_message.
32
+ #
33
+ # exception.default_message = "Default error message"
34
+ # exception.message # => "Default error message"
35
+ #
36
+ # See ControllerAdditions#authorize! for more information on rescuing from this exception
37
+ # and customizing the message using I18n.
38
+ class Unauthorized < Error
39
+ attr_reader :action, :subject
40
+ attr_writer :default_message
41
+
42
+ def initialize(message = nil, action = nil, subject = nil)
43
+ @message = message
44
+ @action = action
45
+ @subject = subject
46
+ @default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
47
+ end
48
+
49
+ def to_s
50
+ @message || @default_message
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ module CanCan
2
+ # For use with Inherited Resources
3
+ class InheritedResource < ControllerResource # :nodoc:
4
+ def load_resource_instance
5
+ if parent?
6
+ @controller.send :association_chain
7
+ @controller.instance_variable_get("@#{instance_name}")
8
+ elsif new_actions.include? @params[:action].to_sym
9
+ resource = @controller.send :build_resource
10
+ assign_attributes(resource)
11
+ else
12
+ @controller.send :resource
13
+ end
14
+ end
15
+
16
+ def resource_base
17
+ @controller.send :end_of_association_chain
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ rspec_module = defined?(RSpec::Core) ? 'RSpec' : 'Spec' # for RSpec 1 compatability
2
+ Kernel.const_get(rspec_module)::Matchers.define :be_able_to do |*args|
3
+ match do |ability|
4
+ ability.can?(*args)
5
+ end
6
+
7
+ failure_message_for_should do |ability|
8
+ "expected to be able to #{args.map(&:inspect).join(" ")}"
9
+ end
10
+
11
+ failure_message_for_should_not do |ability|
12
+ "expected not to be able to #{args.map(&:inspect).join(" ")}"
13
+ end
14
+ end
@@ -0,0 +1,56 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class AbstractAdapter
4
+ def self.inherited(subclass)
5
+ @subclasses ||= []
6
+ @subclasses << subclass
7
+ end
8
+
9
+ def self.adapter_class(model_class)
10
+ @subclasses.detect { |subclass| subclass.for_class?(model_class) } || DefaultAdapter
11
+ end
12
+
13
+ # Used to determine if the given adapter should be used for the passed in class.
14
+ def self.for_class?(member_class)
15
+ false # override in subclass
16
+ end
17
+
18
+ # Override if you need custom find behavior
19
+ def self.find(model_class, id)
20
+ model_class.find(id)
21
+ end
22
+
23
+ # Used to determine if this model adapter will override the matching behavior for a hash of conditions.
24
+ # If this returns true then matches_conditions_hash? will be called. See Rule#matches_conditions_hash
25
+ def self.override_conditions_hash_matching?(subject, conditions)
26
+ false
27
+ end
28
+
29
+ # Override if override_conditions_hash_matching? returns true
30
+ def self.matches_conditions_hash?(subject, conditions)
31
+ raise NotImplemented, "This model adapter does not support matching on a conditions hash."
32
+ end
33
+
34
+ # Used to determine if this model adapter will override the matching behavior for a specific condition.
35
+ # If this returns true then matches_condition? will be called. See Rule#matches_conditions_hash
36
+ def self.override_condition_matching?(subject, name, value)
37
+ false
38
+ end
39
+
40
+ # Override if override_condition_matching? returns true
41
+ def self.matches_condition?(subject, name, value)
42
+ raise NotImplemented, "This model adapter does not support matching on a specific condition."
43
+ end
44
+
45
+ def initialize(model_class, rules)
46
+ @model_class = model_class
47
+ @rules = rules
48
+ end
49
+
50
+ def database_records
51
+ # This should be overridden in a subclass to return records which match @rules
52
+ raise NotImplemented, "This model adapter does not support fetching records from the database."
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,172 @@
1
+ module CanCan
2
+ module ModelAdapters
3
+ class ActiveRecordAdapter < AbstractAdapter
4
+ def self.for_class?(model_class)
5
+ model_class <= ActiveRecord::Base
6
+ end
7
+
8
+ def self.override_condition_matching?(subject, name, value)
9
+ name.kind_of?(MetaWhere::Column) if defined? MetaWhere
10
+ end
11
+
12
+ def self.matches_condition?(subject, name, value)
13
+ subject_value = subject.send(name.column)
14
+ if name.method.to_s.ends_with? "_any"
15
+ value.any? { |v| meta_where_match? subject_value, name.method.to_s.sub("_any", ""), v }
16
+ elsif name.method.to_s.ends_with? "_all"
17
+ value.all? { |v| meta_where_match? subject_value, name.method.to_s.sub("_all", ""), v }
18
+ else
19
+ meta_where_match? subject_value, name.method, value
20
+ end
21
+ end
22
+
23
+ def self.meta_where_match?(subject_value, method, value)
24
+ case method.to_sym
25
+ when :eq then subject_value == value
26
+ when :not_eq then subject_value != value
27
+ when :in then value.include?(subject_value)
28
+ when :not_in then !value.include?(subject_value)
29
+ when :lt then subject_value < value
30
+ when :lteq then subject_value <= value
31
+ when :gt then subject_value > value
32
+ when :gteq then subject_value >= value
33
+ when :matches then subject_value =~ Regexp.new("^" + Regexp.escape(value).gsub("%", ".*") + "$", true)
34
+ when :does_not_match then !meta_where_match?(subject_value, :matches, value)
35
+ else raise NotImplemented, "The #{method} MetaWhere condition is not supported."
36
+ end
37
+ end
38
+
39
+ # Returns conditions intended to be used inside a database query. Normally you will not call this
40
+ # method directly, but instead go through ModelAdditions#accessible_by.
41
+ #
42
+ # If there is only one "can" definition, a hash of conditions will be returned matching the one defined.
43
+ #
44
+ # can :manage, User, :id => 1
45
+ # query(:manage, User).conditions # => { :id => 1 }
46
+ #
47
+ # If there are multiple "can" definitions, a SQL string will be returned to handle complex cases.
48
+ #
49
+ # can :manage, User, :id => 1
50
+ # can :manage, User, :manager_id => 1
51
+ # cannot :manage, User, :self_managed => true
52
+ # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
53
+ #
54
+ def conditions
55
+ if @rules.size == 1 && @rules.first.base_behavior
56
+ # Return the conditions directly if there's just one definition
57
+ tableized_conditions(@rules.first.conditions).dup
58
+ else
59
+ @rules.reverse.inject(false_sql) do |sql, rule|
60
+ merge_conditions(sql, tableized_conditions(rule.conditions).dup, rule.base_behavior)
61
+ end
62
+ end
63
+ end
64
+
65
+ def tableized_conditions(conditions, model_class = @model_class)
66
+ return conditions unless conditions.kind_of? Hash
67
+ conditions.inject({}) do |result_hash, (name, value)|
68
+ if value.kind_of? Hash
69
+ association_class = model_class.reflect_on_association(name).class_name.constantize
70
+ name = model_class.reflect_on_association(name).table_name.to_sym
71
+ value = tableized_conditions(value, association_class)
72
+ end
73
+ result_hash[name] = value
74
+ result_hash
75
+ end
76
+ end
77
+
78
+ # Returns the associations used in conditions for the :joins option of a search.
79
+ # See ModelAdditions#accessible_by
80
+ def joins
81
+ joins_hash = {}
82
+ @rules.each do |rule|
83
+ merge_joins(joins_hash, rule.associations_hash)
84
+ end
85
+ clean_joins(joins_hash) unless joins_hash.empty?
86
+ end
87
+
88
+ def database_records
89
+ if override_scope
90
+ @model_class.scoped.merge(override_scope)
91
+ elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
92
+ mergeable_conditions = @rules.select {|rule| rule.unmergeable? }.blank?
93
+ if mergeable_conditions
94
+ @model_class.where(conditions).joins(joins)
95
+ else
96
+ @model_class.where(*(@rules.map(&:conditions))).joins(joins)
97
+ end
98
+ else
99
+ @model_class.scoped(:conditions => conditions, :joins => joins)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def override_scope
106
+ conditions = @rules.map(&:conditions).compact
107
+ if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
108
+ if conditions.size == 1
109
+ conditions.first
110
+ else
111
+ rule = @rules.detect { |rule| rule.conditions.kind_of?(ActiveRecord::Relation) }
112
+ raise Error, "Unable to merge an Active Record scope with other conditions. Instead use a hash or SQL for #{rule.actions.first} #{rule.subjects.first} ability."
113
+ end
114
+ end
115
+ end
116
+
117
+ def merge_conditions(sql, conditions_hash, behavior)
118
+ if conditions_hash.blank?
119
+ behavior ? true_sql : false_sql
120
+ else
121
+ conditions = sanitize_sql(conditions_hash)
122
+ case sql
123
+ when true_sql
124
+ behavior ? true_sql : "not (#{conditions})"
125
+ when false_sql
126
+ behavior ? conditions : false_sql
127
+ else
128
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
129
+ end
130
+ end
131
+ end
132
+
133
+ def false_sql
134
+ sanitize_sql(['?=?', true, false])
135
+ end
136
+
137
+ def true_sql
138
+ sanitize_sql(['?=?', true, true])
139
+ end
140
+
141
+ def sanitize_sql(conditions)
142
+ @model_class.send(:sanitize_sql, conditions)
143
+ end
144
+
145
+ # Takes two hashes and does a deep merge.
146
+ def merge_joins(base, add)
147
+ add.each do |name, nested|
148
+ if base[name].is_a?(Hash) && !nested.empty?
149
+ merge_joins(base[name], nested)
150
+ else
151
+ base[name] = nested
152
+ end
153
+ end
154
+ end
155
+
156
+ # Removes empty hashes and moves everything into arrays.
157
+ def clean_joins(joins_hash)
158
+ joins = []
159
+ joins_hash.each do |name, nested|
160
+ joins << (nested.empty? ? name : {name => clean_joins(nested)})
161
+ end
162
+ joins
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ ActiveSupport.on_load(:active_record) do
169
+ ActiveRecord::Base.class_eval do
170
+ include CanCan::ModelAdditions
171
+ end
172
+ end