heimdallr 1.0.3 → 1.0.4

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.
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."