marnen-cancan 2.0.0.alpha.pre.f1cebde51a87be149b4970a3287826bb63c0ac0b

Sign up to get free protection for your applications and to get access to all the features.
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