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 +9 -0
- data/heimdallr.gemspec +1 -1
- data/lib/heimdallr.rb +15 -0
- data/lib/heimdallr/model.rb +1 -1
- data/lib/heimdallr/proxy/collection.rb +75 -13
- data/lib/heimdallr/proxy/record.rb +33 -3
- metadata +10 -10
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
|
|
data/heimdallr.gemspec
CHANGED
@@ -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.
|
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"
|
data/lib/heimdallr.rb
CHANGED
@@ -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.
|
data/lib/heimdallr/model.rb
CHANGED
@@ -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,
|
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),
|
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,
|
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,
|
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,
|
214
|
+
element.restrict(@context, options_with_eager_load)
|
166
215
|
end
|
167
216
|
else
|
168
|
-
result.restrict(@context,
|
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,
|
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,
|
235
|
+
element.restrict(@context, options_with_escape)
|
187
236
|
end
|
188
237
|
elsif method =~ /^find_by/
|
189
|
-
@scope.send(method, *args).restrict(@context,
|
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),
|
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 (
|
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
|
-
|
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.
|
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-
|
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: &
|
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: *
|
25
|
+
version_requirements: *70300668429320
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: activemodel
|
28
|
-
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: *
|
36
|
+
version_requirements: *70300668428840
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: rspec
|
39
|
-
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: *
|
47
|
+
version_requirements: *70300668428460
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: activerecord
|
50
|
-
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: *
|
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."
|