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