culturecode-cancan 2.0.0.alpha

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 +7 -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 +266 -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 +551 -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 +226 -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 +194 -0
@@ -0,0 +1,266 @@
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
+ initial_attributes.each do |attr_name, value|
89
+ resource.send("#{attr_name}=", value)
90
+ end
91
+
92
+ resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
93
+ resource
94
+ end
95
+
96
+ def initial_attributes
97
+ current_ability.attributes_for(@params[:action].to_sym, subject_name).delete_if do |key, value|
98
+ resource_params && resource_params.include?(key)
99
+ end
100
+ end
101
+
102
+ def find_and_update_resource
103
+ resource = find_resource
104
+ if resource_params
105
+ @controller.authorize!(authorization_action, resource) if @options[:authorize]
106
+ resource.attributes = resource_params
107
+ end
108
+ resource
109
+ end
110
+
111
+ def find_resource
112
+ if @options[:singleton] && parent_resource.respond_to?(name)
113
+ parent_resource.send(name)
114
+ else
115
+ if @options[:find_by]
116
+ if resource_base.respond_to? "find_by_#{@options[:find_by]}!"
117
+ resource_base.send("find_by_#{@options[:find_by]}!", id_param)
118
+ else
119
+ resource_base.send(@options[:find_by], id_param)
120
+ end
121
+ else
122
+ adapter.find(resource_base, id_param)
123
+ end
124
+ end
125
+ end
126
+
127
+ def adapter
128
+ ModelAdapters::AbstractAdapter.adapter_class(resource_class)
129
+ end
130
+
131
+ def authorization_action
132
+ parent? ? :show : @params[:action].to_sym
133
+ end
134
+
135
+ def id_param
136
+ if @options[:id_param]
137
+ @params[@options[:id_param]]
138
+ else
139
+ @params[parent? ? :"#{name}_id" : :id]
140
+ end
141
+ end
142
+
143
+ def member_action?
144
+ new_actions.include?(@params[:action].to_sym) || @options[:singleton] || ( (@params[:id] || @params[@options[:id_param]]) && !collection_actions.include?(@params[:action].to_sym))
145
+ end
146
+
147
+ # Returns the class used for this resource. This can be overriden by the :class option.
148
+ # If +false+ is passed in it will use the resource name as a symbol in which case it should
149
+ # only be used for authorization, not loading since there's no class to load through.
150
+ def resource_class
151
+ case @options[:class]
152
+ when false then name.to_sym
153
+ when nil then namespaced_name.to_s.camelize.constantize
154
+ when String then @options[:class].constantize
155
+ else @options[:class]
156
+ end
157
+ end
158
+
159
+ def subject_name
160
+ resource_class.to_s.underscore.pluralize.to_sym
161
+ end
162
+
163
+ def subject_name_with_parent
164
+ parent_resource ? {parent_resource => subject_name} : subject_name
165
+ end
166
+
167
+ def resource_instance=(instance)
168
+ @controller.instance_variable_set("@#{instance_name}", instance)
169
+ end
170
+
171
+ def resource_instance
172
+ if load_instance?
173
+ if @controller.instance_variable_defined? "@#{instance_name}"
174
+ @controller.instance_variable_get("@#{instance_name}")
175
+ elsif @controller.respond_to?(instance_name, true)
176
+ @controller.send(instance_name)
177
+ end
178
+ end
179
+ end
180
+
181
+ def collection_instance=(instance)
182
+ @controller.instance_variable_set("@#{instance_name.to_s.pluralize}", instance)
183
+ end
184
+
185
+ def collection_instance
186
+ @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
187
+ end
188
+
189
+ # The object that methods (such as "find", "new" or "build") are called on.
190
+ # If the :through option is passed it will go through an association on that instance.
191
+ # If the :shallow option is passed it will use the resource_class if there's no parent
192
+ # If the :singleton option is passed it won't use the association because it needs to be handled later.
193
+ def resource_base
194
+ if @options[:through]
195
+ if parent_resource
196
+ @options[:singleton] ? resource_class : parent_resource.send(@options[:through_association] || name.to_s.pluralize)
197
+ elsif @options[:shallow]
198
+ resource_class
199
+ else
200
+ raise Unauthorized.new(nil, authorization_action, @params[:controller].to_sym) # maybe this should be a record not found error instead?
201
+ end
202
+ else
203
+ resource_class
204
+ end
205
+ end
206
+
207
+ def parent_name
208
+ @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
209
+ end
210
+
211
+ # The object to load this resource through.
212
+ def parent_resource
213
+ parent_name && fetch_parent(parent_name)
214
+ end
215
+
216
+ def fetch_parent(name)
217
+ if @controller.instance_variable_defined? "@#{name}"
218
+ @controller.instance_variable_get("@#{name}")
219
+ elsif @controller.respond_to?(name, true)
220
+ @controller.send(name)
221
+ end
222
+ end
223
+
224
+ def current_ability
225
+ @controller.send(:current_ability)
226
+ end
227
+
228
+ def name
229
+ @name || name_from_controller
230
+ end
231
+
232
+ def resource_params
233
+ if @options[:class]
234
+ @params[@options[:class].to_s.underscore.gsub('/', '_')]
235
+ else
236
+ @params[namespaced_name.to_s.underscore.gsub("/", "_")]
237
+ end
238
+ end
239
+
240
+ def namespace
241
+ @params[:controller].split(/::|\//)[0..-2]
242
+ end
243
+
244
+ def namespaced_name
245
+ [namespace, name.camelize].join('::').singularize.camelize.constantize
246
+ rescue NameError
247
+ name
248
+ end
249
+
250
+ def name_from_controller
251
+ @params[:controller].sub("Controller", "").underscore.split('/').last.singularize
252
+ end
253
+
254
+ def instance_name
255
+ @options[:instance_name] || name
256
+ end
257
+
258
+ def collection_actions
259
+ [:index] + [@options[:collection]].flatten
260
+ end
261
+
262
+ def new_actions
263
+ [:new, :create] + [@options[:new]].flatten
264
+ end
265
+ end
266
+ 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