codeprimate-cancan 1.6.5

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