heimdallr 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -110,6 +110,15 @@ that means it will raise an exception for every insecure request. Calling `.impl
110
110
  of proxy object switched to another strategy. With that it will silently return nil for every attribute
111
111
  that is inaccessible.
112
112
 
113
+ There are several options which alter Heimdallr's behavior in security-sensitive ways. They are described
114
+ in [Heimdallr](http://rubydoc.info/gems/heimdallr/master/Heimdallr).
115
+
116
+ Rails notes
117
+ -----------
118
+
119
+ As of Rails 3.2.3 attr_accessible is in whitelist mode by default. That makes no sense when using Heimdallr. To
120
+ turn it off set the `config.active_record.whitelist_attributes` value to false at yours `application.rb`.
121
+
113
122
  Typical cases
114
123
  -------------
115
124
 
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "heimdallr"
6
- s.version = "1.0.3"
6
+ s.version = "1.0.4"
7
7
  s.authors = ["Peter Zotov", "Boris Staal"]
8
8
  s.email = ["whitequark@whitequark.org", "boris@roundlake.ru"]
9
9
  s.homepage = "http://github.com/roundlake/heimdallr"
@@ -25,9 +25,24 @@ module Heimdallr
25
25
  #
26
26
  # @return [Boolean]
27
27
  attr_accessor :allow_insecure_associations
28
+
29
+ # Allow unrestricted association fetching in case of eager loading.
30
+ #
31
+ # By default, associations are restricted with fetch scope either when
32
+ # they are accessed or when they are eagerly loaded (with #includes).
33
+ # Condition injection on eager loads are known to be quirky in some cases,
34
+ # particularly deeply nested polymorphic associations, and if the layout
35
+ # of your database guarantees that any data fetched through explicitly
36
+ # eagerly loaded associations will be safe to view (or if you restrict
37
+ # it manually), you can enable this setting to skip automatic condition
38
+ # injection.
39
+ #
40
+ # @return [Boolean]
41
+ attr_accessor :skip_eager_condition_injection
28
42
  end
29
43
 
30
44
  self.allow_insecure_associations = false
45
+ self.skip_eager_condition_injection = false
31
46
 
32
47
  # {PermissionError} is raised when a security policy prevents
33
48
  # a called operation from being executed.
@@ -35,7 +35,7 @@ module Heimdallr
35
35
  if block
36
36
  @restrictions = Evaluator.new(self, block)
37
37
  else
38
- Proxy::Collection.new(context, restrictions(context).request_scope, options)
38
+ Proxy::Collection.new(context, restrictions(context).request_scope(:fetch, self), options)
39
39
  end
40
40
  end
41
41
 
@@ -19,6 +19,7 @@ module Heimdallr
19
19
  @context, @scope, @options = context, scope, options
20
20
 
21
21
  @restrictions = @scope.restrictions(context)
22
+ @options[:eager_loaded] ||= {}
22
23
  end
23
24
 
24
25
  # Collections cannot be restricted with different context or options.
@@ -40,7 +41,7 @@ module Heimdallr
40
41
  def self.delegate_as_constructor(name, method)
41
42
  class_eval(<<-EOM, __FILE__, __LINE__)
42
43
  def #{name}(attributes={})
43
- record = @restrictions.request_scope(:fetch).new.restrict(@context, @options)
44
+ record = @restrictions.request_scope(:fetch).new.restrict(@context, options_with_escape)
44
45
  record.#{method}(attributes.merge(@restrictions.fixtures[:create]))
45
46
  record
46
47
  end
@@ -53,7 +54,7 @@ module Heimdallr
53
54
  def self.delegate_as_scope(name)
54
55
  class_eval(<<-EOM, __FILE__, __LINE__)
55
56
  def #{name}(*args)
56
- Proxy::Collection.new(@context, @scope.#{name}(*args), @options)
57
+ Proxy::Collection.new(@context, @scope.#{name}(*args), options_with_escape)
57
58
  end
58
59
  EOM
59
60
  end
@@ -75,7 +76,7 @@ module Heimdallr
75
76
  def self.delegate_as_record(name)
76
77
  class_eval(<<-EOM, __FILE__, __LINE__)
77
78
  def #{name}(*args)
78
- @scope.#{name}(*args).restrict(@context, @options)
79
+ @scope.#{name}(*args).restrict(@context, options_with_eager_load)
79
80
  end
80
81
  EOM
81
82
  end
@@ -87,7 +88,7 @@ module Heimdallr
87
88
  class_eval(<<-EOM, __FILE__, __LINE__)
88
89
  def #{name}(*args)
89
90
  @scope.#{name}(*args).map do |element|
90
- element.restrict(@context, @options)
91
+ element.restrict(@context, options_with_eager_load)
91
92
  end
92
93
  end
93
94
  EOM
@@ -113,9 +114,6 @@ module Heimdallr
113
114
  delegate_as_scope :uniq
114
115
  delegate_as_scope :where
115
116
  delegate_as_scope :joins
116
- delegate_as_scope :includes
117
- delegate_as_scope :eager_load
118
- delegate_as_scope :preload
119
117
  delegate_as_scope :lock
120
118
  delegate_as_scope :limit
121
119
  delegate_as_scope :offset
@@ -154,6 +152,57 @@ module Heimdallr
154
152
  delegate_as_records :to_a
155
153
  delegate_as_records :to_ary
156
154
 
155
+ # A proxy for +includes+ which adds Heimdallr conditions for eager loaded
156
+ # associations.
157
+ def includes(*associations)
158
+ # Normalize association list to strict nested hash.
159
+ normalize = ->(list) {
160
+ if list.is_a? Array
161
+ list.map(&normalize).reduce(:merge)
162
+ elsif list.is_a? Symbol
163
+ { list => {} }
164
+ elsif list.is_a? Hash
165
+ hash = {}
166
+ list.each do |key, value|
167
+ hash[key] = normalize.(value)
168
+ end
169
+ hash
170
+ end
171
+ }
172
+ associations = normalize.(associations)
173
+
174
+ current_scope = @scope.includes(associations)
175
+
176
+ add_conditions = ->(associations, scope) {
177
+ associations.each do |association, nested|
178
+ reflection = scope.reflect_on_association(association)
179
+ if reflection && !reflection.options[:polymorphic]
180
+ associated_klass = reflection.klass
181
+
182
+ if associated_klass.respond_to? :restrict
183
+ nested_scope = associated_klass.restrictions(@context).request_scope(:fetch)
184
+
185
+ where_values = nested_scope.where_values
186
+ if where_values.any?
187
+ current_scope = current_scope.where(*where_values)
188
+ end
189
+
190
+ add_conditions.(nested, associated_klass)
191
+ end
192
+ end
193
+ end
194
+ }
195
+
196
+ unless Heimdallr.skip_eager_condition_injection
197
+ add_conditions.(associations, current_scope)
198
+ end
199
+
200
+ options = @options.merge(eager_loaded:
201
+ @options[:eager_loaded].merge(associations))
202
+
203
+ Proxy::Collection.new(@context, current_scope, options)
204
+ end
205
+
157
206
  # A proxy for +find+ which restricts the returned record or records.
158
207
  #
159
208
  # @return [Proxy::Record, Array<Proxy::Record>]
@@ -162,10 +211,10 @@ module Heimdallr
162
211
 
163
212
  if result.is_a? Enumerable
164
213
  result.map do |element|
165
- element.restrict(@context, @options)
214
+ element.restrict(@context, options_with_eager_load)
166
215
  end
167
216
  else
168
- result.restrict(@context, @options)
217
+ result.restrict(@context, options_with_eager_load)
169
218
  end
170
219
  end
171
220
 
@@ -175,7 +224,7 @@ module Heimdallr
175
224
  # @yieldparam [Proxy::Record] record
176
225
  def each
177
226
  @scope.each do |record|
178
- yield record.restrict(@context, @options)
227
+ yield record.restrict(@context, options_with_eager_load)
179
228
  end
180
229
  end
181
230
 
@@ -183,12 +232,12 @@ module Heimdallr
183
232
  def method_missing(method, *args)
184
233
  if method =~ /^find_all_by/
185
234
  @scope.send(method, *args).map do |element|
186
- element.restrict(@context, @options)
235
+ element.restrict(@context, options_with_escape)
187
236
  end
188
237
  elsif method =~ /^find_by/
189
- @scope.send(method, *args).restrict(@context, @options)
238
+ @scope.send(method, *args).restrict(@context, options_with_escape)
190
239
  elsif @scope.heimdallr_scopes && @scope.heimdallr_scopes.include?(method)
191
- Proxy::Collection.new(@context, @scope.send(method, *args), @options)
240
+ Proxy::Collection.new(@context, @scope.send(method, *args), options_with_escape)
192
241
  elsif @scope.respond_to? method
193
242
  raise InsecureOperationError,
194
243
  "Potentially insecure method #{method} was called"
@@ -232,5 +281,18 @@ module Heimdallr
232
281
  def creatable?
233
282
  @restrictions.can? :create
234
283
  end
284
+
285
+ private
286
+
287
+ # Return options hash to pass to children proxies.
288
+ # Currently this checks only eagerly loaded collections, which
289
+ # shouldn't be passed around blindly.
290
+ def options_with_escape
291
+ @options.reject { |k,v| k == :eager_loaded }
292
+ end
293
+
294
+ def options_with_eager_load
295
+ @options
296
+ end
235
297
  end
236
298
  end
@@ -16,9 +16,10 @@ module Heimdallr
16
16
  # @param object proxified record
17
17
  # @option options [Boolean] implicit proxy type
18
18
  def initialize(context, record, options={})
19
- @context, @record, @options = context, record, options
19
+ @context, @record, @options = context, record, options.dup
20
20
 
21
21
  @restrictions = @record.class.restrictions(context, record)
22
+ @eager_loaded = @options.delete(:eager_loaded) || {}
22
23
  end
23
24
 
24
25
  # @method decrement(field, by=1)
@@ -40,6 +41,18 @@ module Heimdallr
40
41
  # and thus is not considered as a potential security threat.
41
42
  delegate :touch, :to => :@record
42
43
 
44
+ # @method model_name
45
+ # @macro delegate
46
+ delegate :model_name, :to => :@record
47
+
48
+ # @method to_key
49
+ # @macro delegate
50
+ delegate :to_key, :to => :@record
51
+
52
+ # @method to_param
53
+ # @macro delegate
54
+ delegate :to_param, :to => :@record
55
+
43
56
  # A proxy for +attributes+ method which removes all attributes
44
57
  # without +:view+ permission.
45
58
  def attributes
@@ -176,7 +189,7 @@ module Heimdallr
176
189
  suffix = nil
177
190
  end
178
191
 
179
- if (defined?(ActiveRecord) && @record.is_a?(ActiveRecord::Reflection) &&
192
+ if (@record.is_a?(ActiveRecord::Reflection) &&
180
193
  association = @record.class.reflect_on_association(method)) ||
181
194
  (!@record.class.heimdallr_relations.nil? &&
182
195
  @record.class.heimdallr_relations.include?(normalized_method))
@@ -185,7 +198,19 @@ module Heimdallr
185
198
  if referenced.nil?
186
199
  nil
187
200
  elsif referenced.respond_to? :restrict
188
- referenced.restrict(@context, @options)
201
+ if @eager_loaded.include?(method)
202
+ options = @options.merge(eager_loaded: @eager_loaded[method])
203
+ else
204
+ options = @options
205
+ end
206
+
207
+ if association.collection? && @eager_loaded.include?(method)
208
+ # Don't re-restrict eagerly loaded collections to not
209
+ # discard preloaded data.
210
+ Proxy::Collection.new(@context, referenced, options)
211
+ else
212
+ referenced.restrict(@context, @options)
213
+ end
189
214
  elsif Heimdallr.allow_insecure_associations
190
215
  referenced
191
216
  else
@@ -263,6 +288,11 @@ module Heimdallr
263
288
  }.merge(@restrictions.reflection)
264
289
  end
265
290
 
291
+ def visible?
292
+ scope = @restrictions.request_scope(:fetch)
293
+ scope.where({ @record.class.primary_key => @record.to_key }).any?
294
+ end
295
+
266
296
  def creatable?
267
297
  @restrictions.can? :create
268
298
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heimdallr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-04-13 00:00:00.000000000 Z
13
+ date: 2012-06-01 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
17
- requirement: &70171398246540 !ruby/object:Gem::Requirement
17
+ requirement: &70300668429320 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: 3.0.0
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *70171398246540
25
+ version_requirements: *70300668429320
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: activemodel
28
- requirement: &70171398246060 !ruby/object:Gem::Requirement
28
+ requirement: &70300668428840 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: 3.0.0
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *70171398246060
36
+ version_requirements: *70300668428840
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: rspec
39
- requirement: &70171398245680 !ruby/object:Gem::Requirement
39
+ requirement: &70300668428460 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: '0'
45
45
  type: :development
46
46
  prerelease: false
47
- version_requirements: *70171398245680
47
+ version_requirements: *70300668428460
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: activerecord
50
- requirement: &70171398245220 !ruby/object:Gem::Requirement
50
+ requirement: &70300668428000 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ! '>='
@@ -55,7 +55,7 @@ dependencies:
55
55
  version: '0'
56
56
  type: :development
57
57
  prerelease: false
58
- version_requirements: *70171398245220
58
+ version_requirements: *70300668428000
59
59
  description: ! "Heimdallr aims to provide an easy to configure and efficient object-
60
60
  and field-level access\n control solution, reusing proven patterns from gems like
61
61
  CanCan and allowing one to manage permissions in a very\n fine-grained manner."