codeprimate-cancan 1.6.5

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 (39) hide show
  1. data/CHANGELOG.rdoc +291 -0
  2. data/Gemfile +20 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +111 -0
  5. data/Rakefile +18 -0
  6. data/init.rb +1 -0
  7. data/lib/cancan.rb +13 -0
  8. data/lib/cancan/ability.rb +298 -0
  9. data/lib/cancan/controller_additions.rb +389 -0
  10. data/lib/cancan/controller_resource.rb +222 -0
  11. data/lib/cancan/exceptions.rb +50 -0
  12. data/lib/cancan/inherited_resource.rb +19 -0
  13. data/lib/cancan/matchers.rb +14 -0
  14. data/lib/cancan/model_adapters/abstract_adapter.rb +56 -0
  15. data/lib/cancan/model_adapters/active_record_adapter.rb +165 -0
  16. data/lib/cancan/model_adapters/data_mapper_adapter.rb +34 -0
  17. data/lib/cancan/model_adapters/default_adapter.rb +7 -0
  18. data/lib/cancan/model_adapters/mongoid_adapter.rb +53 -0
  19. data/lib/cancan/model_additions.rb +31 -0
  20. data/lib/cancan/rule.rb +142 -0
  21. data/lib/generators/cancan/ability/USAGE +4 -0
  22. data/lib/generators/cancan/ability/ability_generator.rb +11 -0
  23. data/lib/generators/cancan/ability/templates/ability.rb +28 -0
  24. data/spec/README.rdoc +28 -0
  25. data/spec/cancan/ability_spec.rb +419 -0
  26. data/spec/cancan/controller_additions_spec.rb +137 -0
  27. data/spec/cancan/controller_resource_spec.rb +412 -0
  28. data/spec/cancan/exceptions_spec.rb +58 -0
  29. data/spec/cancan/inherited_resource_spec.rb +42 -0
  30. data/spec/cancan/matchers_spec.rb +33 -0
  31. data/spec/cancan/model_adapters/active_record_adapter_spec.rb +278 -0
  32. data/spec/cancan/model_adapters/data_mapper_adapter_spec.rb +119 -0
  33. data/spec/cancan/model_adapters/default_adapter_spec.rb +7 -0
  34. data/spec/cancan/model_adapters/mongoid_adapter_spec.rb +216 -0
  35. data/spec/cancan/rule_spec.rb +39 -0
  36. data/spec/matchers.rb +13 -0
  37. data/spec/spec.opts +2 -0
  38. data/spec/spec_helper.rb +41 -0
  39. metadata +167 -0
@@ -0,0 +1,222 @@
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, method, *args)
6
+ options = args.extract_options!
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)) do |controller|
10
+ controller.class.cancan_resource_class.new(controller, resource_name, options.except(:only, :except)).send(method)
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
+ raise CanCan::ImplementationRemoved, "The :nested option is no longer supported, instead use :through with separate load/authorize call." if @options[:nested]
20
+ raise CanCan::ImplementationRemoved, "The :name option is no longer supported, instead pass the name as the first argument." if @options[:name]
21
+ raise CanCan::ImplementationRemoved, "The :resource option has been renamed back to :class, use false if no class." if @options[:resource]
22
+ end
23
+
24
+ def load_and_authorize_resource
25
+ load_resource
26
+ authorize_resource
27
+ end
28
+
29
+ def load_resource
30
+ unless skip?(:load)
31
+ if load_instance?
32
+ self.resource_instance ||= load_resource_instance
33
+ elsif load_collection?
34
+ self.collection_instance ||= load_collection
35
+ end
36
+ end
37
+ end
38
+
39
+ def authorize_resource
40
+ unless skip?(:authorize)
41
+ @controller.authorize!(authorization_action, resource_instance || resource_class_with_parent)
42
+ end
43
+ end
44
+
45
+ def parent?
46
+ @options.has_key?(:parent) ? @options[:parent] : @name && @name != name_from_controller.to_sym
47
+ end
48
+
49
+ def skip?(behavior) # This could probably use some refactoring
50
+ options = @controller.class.cancan_skipper[behavior][@name]
51
+ if options.nil?
52
+ false
53
+ elsif options == {}
54
+ true
55
+ elsif options[:except] && ![options[:except]].flatten.include?(@params[:action].to_sym)
56
+ true
57
+ elsif [options[:only]].flatten.include?(@params[:action].to_sym)
58
+ true
59
+ end
60
+ end
61
+
62
+ protected
63
+
64
+ def load_resource_instance
65
+ if !parent? && new_actions.include?(@params[:action].to_sym)
66
+ build_resource
67
+ elsif id_param || @options[:singleton]
68
+ find_resource
69
+ end
70
+ end
71
+
72
+ def load_instance?
73
+ parent? || member_action?
74
+ end
75
+
76
+ def load_collection?
77
+ resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, resource_class)
78
+ end
79
+
80
+ def load_collection
81
+ resource_base.accessible_by(current_ability, authorization_action)
82
+ end
83
+
84
+ def build_resource
85
+ resource = resource_base.new(@params[name] || {})
86
+ resource.send("#{parent_name}=", parent_resource) if @options[:singleton] && parent_resource
87
+ initial_attributes.each do |attr_name, value|
88
+ resource.send("#{attr_name}=", value)
89
+ end
90
+ resource
91
+ end
92
+
93
+ def initial_attributes
94
+ current_ability.attributes_for(@params[:action].to_sym, resource_class).delete_if do |key, value|
95
+ @params[name] && @params[name].include?(key)
96
+ end
97
+ end
98
+
99
+ def find_resource
100
+ if @options[:singleton] && parent_resource.respond_to?(name)
101
+ parent_resource.send(name)
102
+ else
103
+ if @options[:find_by]
104
+ if resource_base.respond_to? "find_by_#{@options[:find_by]}!"
105
+ resource_base.send("find_by_#{@options[:find_by]}!", id_param)
106
+ else
107
+ resource_base.send(@options[:find_by], id_param)
108
+ end
109
+ else
110
+ adapter.find(resource_base, id_param)
111
+ end
112
+ end
113
+ end
114
+
115
+ def adapter
116
+ ModelAdapters::AbstractAdapter.adapter_class(resource_class)
117
+ end
118
+
119
+ def authorization_action
120
+ parent? ? :show : @params[:action].to_sym
121
+ end
122
+
123
+ def id_param
124
+ @params[parent? ? :"#{name}_id" : :id]
125
+ end
126
+
127
+ def member_action?
128
+ new_actions.include?(@params[:action].to_sym) || @options[:singleton] || (@params[:id] && !collection_actions.include?(@params[:action].to_sym))
129
+ end
130
+
131
+ # Returns the class used for this resource. This can be overriden by the :class option.
132
+ # If +false+ is passed in it will use the resource name as a symbol in which case it should
133
+ # only be used for authorization, not loading since there's no class to load through.
134
+ def resource_class
135
+ case @options[:class]
136
+ when false then name.to_sym
137
+ when nil then name.to_s.camelize.constantize
138
+ when String then @options[:class].constantize
139
+ else @options[:class]
140
+ end
141
+ end
142
+
143
+ def resource_class_with_parent
144
+ parent_resource ? {parent_resource => resource_class} : resource_class
145
+ end
146
+
147
+ def resource_instance=(instance)
148
+ @controller.instance_variable_set("@#{instance_name}", instance)
149
+ end
150
+
151
+ def resource_instance
152
+ @controller.instance_variable_get("@#{instance_name}") if load_instance?
153
+ end
154
+
155
+ def collection_instance=(instance)
156
+ @controller.instance_variable_set("@#{instance_name.to_s.pluralize}", instance)
157
+ end
158
+
159
+ def collection_instance
160
+ @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
161
+ end
162
+
163
+ # The object that methods (such as "find", "new" or "build") are called on.
164
+ # If the :through option is passed it will go through an association on that instance.
165
+ # If the :shallow option is passed it will use the resource_class if there's no parent
166
+ # If the :singleton option is passed it won't use the association because it needs to be handled later.
167
+ def resource_base
168
+ if @options[:through]
169
+ if parent_resource
170
+ @options[:singleton] ? resource_class : parent_resource.send(@options[:through_association] || name.to_s.pluralize)
171
+ elsif @options[:shallow]
172
+ resource_class
173
+ else
174
+ raise AccessDenied.new(nil, authorization_action, resource_class) # maybe this should be a record not found error instead?
175
+ end
176
+ else
177
+ resource_class
178
+ end
179
+ end
180
+
181
+ def parent_name
182
+ @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
183
+ end
184
+
185
+ # The object to load this resource through.
186
+ def parent_resource
187
+ parent_name && fetch_parent(parent_name)
188
+ end
189
+
190
+ def fetch_parent(name)
191
+ if @controller.instance_variable_defined? "@#{name}"
192
+ @controller.instance_variable_get("@#{name}")
193
+ elsif @controller.respond_to?(name, true)
194
+ @controller.send(name)
195
+ end
196
+ end
197
+
198
+ def current_ability
199
+ @controller.send(:current_ability)
200
+ end
201
+
202
+ def name
203
+ @name || name_from_controller
204
+ end
205
+
206
+ def name_from_controller
207
+ @params[:controller].sub("Controller", "").underscore.split('/').last.singularize
208
+ end
209
+
210
+ def instance_name
211
+ @options[:instance_name] || name
212
+ end
213
+
214
+ def collection_actions
215
+ [:index] + [@options[:collection]].flatten
216
+ end
217
+
218
+ def new_actions
219
+ [:new, :create] + [@options[:new]].flatten
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,50 @@
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
+ # This error is raised when a user isn't allowed to access a given controller action.
15
+ # This usually happens within a call to ControllerAdditions#authorize! but can be
16
+ # raised manually.
17
+ #
18
+ # raise CanCan::AccessDenied.new("Not authorized!", :read, Article)
19
+ #
20
+ # The passed message, action, and subject are optional and can later be retrieved when
21
+ # rescuing from the exception.
22
+ #
23
+ # exception.message # => "Not authorized!"
24
+ # exception.action # => :read
25
+ # exception.subject # => Article
26
+ #
27
+ # If the message is not specified (or is nil) it will default to "You are not authorized
28
+ # to access this page." This default can be overridden by setting default_message.
29
+ #
30
+ # exception.default_message = "Default error message"
31
+ # exception.message # => "Default error message"
32
+ #
33
+ # See ControllerAdditions#authorized! for more information on rescuing from this exception
34
+ # and customizing the message using I18n.
35
+ class AccessDenied < Error
36
+ attr_reader :action, :subject
37
+ attr_writer :default_message
38
+
39
+ def initialize(message = nil, action = nil, subject = nil)
40
+ @message = message
41
+ @action = action
42
+ @subject = subject
43
+ @default_message = I18n.t(:"unauthorized.default", :default => "You are not authorized to access this page.")
44
+ end
45
+
46
+ def to_s
47
+ @message || @default_message
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,19 @@
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
+ @controller.send :build_resource
10
+ else
11
+ @controller.send :resource
12
+ end
13
+ end
14
+
15
+ def resource_base
16
+ @controller.send :end_of_association_chain
17
+ end
18
+ end
19
+ 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,165 @@
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
+ @model_class.where(conditions).joins(joins)
93
+ else
94
+ @model_class.scoped(:conditions => conditions, :joins => joins)
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def override_scope
101
+ conditions = @rules.map(&:conditions).compact
102
+ if defined?(ActiveRecord::Relation) && conditions.any? { |c| c.kind_of?(ActiveRecord::Relation) }
103
+ if conditions.size == 1
104
+ conditions.first
105
+ else
106
+ rule = @rules.detect { |rule| rule.conditions.kind_of?(ActiveRecord::Relation) }
107
+ 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."
108
+ end
109
+ end
110
+ end
111
+
112
+ def merge_conditions(sql, conditions_hash, behavior)
113
+ if conditions_hash.blank?
114
+ behavior ? true_sql : false_sql
115
+ else
116
+ conditions = sanitize_sql(conditions_hash)
117
+ case sql
118
+ when true_sql
119
+ behavior ? true_sql : "not (#{conditions})"
120
+ when false_sql
121
+ behavior ? conditions : false_sql
122
+ else
123
+ behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
124
+ end
125
+ end
126
+ end
127
+
128
+ def false_sql
129
+ sanitize_sql(['?=?', true, false])
130
+ end
131
+
132
+ def true_sql
133
+ sanitize_sql(['?=?', true, true])
134
+ end
135
+
136
+ def sanitize_sql(conditions)
137
+ @model_class.send(:sanitize_sql, conditions)
138
+ end
139
+
140
+ # Takes two hashes and does a deep merge.
141
+ def merge_joins(base, add)
142
+ add.each do |name, nested|
143
+ if base[name].is_a?(Hash) && !nested.empty?
144
+ merge_joins(base[name], nested)
145
+ else
146
+ base[name] = nested
147
+ end
148
+ end
149
+ end
150
+
151
+ # Removes empty hashes and moves everything into arrays.
152
+ def clean_joins(joins_hash)
153
+ joins = []
154
+ joins_hash.each do |name, nested|
155
+ joins << (nested.empty? ? name : {name => clean_joins(nested)})
156
+ end
157
+ joins
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ ActiveRecord::Base.class_eval do
164
+ include CanCan::ModelAdditions
165
+ end