mongoid 9.0.10 → 9.0.12
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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/Rakefile +31 -0
- data/lib/config/locales/en.yml +45 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/nested/many.rb +38 -3
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/auto_save.rb +48 -6
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/relatable.rb +17 -0
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +9 -5
- data/lib/mongoid/config/encryption.rb +36 -18
- data/lib/mongoid/config.rb +72 -0
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/encryptable.rb +46 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
- data/lib/mongoid/errors.rb +2 -0
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/indexable.rb +39 -27
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
- data/lib/mongoid/matcher/regex.rb +9 -13
- data/lib/mongoid/matcher/regexp_budget.rb +383 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +14 -7
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +37 -0
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +201 -36
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/auto_save_spec.rb +98 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/attributes/nested_spec.rb +250 -30
- data/spec/mongoid/config/defaults_spec.rb +18 -0
- data/spec/mongoid/config/encryption_spec.rb +228 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/encryptable_spec.rb +61 -0
- data/spec/mongoid/errors/document_not_found_spec.rb +10 -3
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/support/crypt/models.rb +157 -0
- metadata +28 -2
|
@@ -8,11 +8,38 @@ require_relative '../association/referenced/has_and_belongs_to_many_models'
|
|
|
8
8
|
require_relative './nested_spec_models'
|
|
9
9
|
|
|
10
10
|
describe Mongoid::Attributes::Nested do
|
|
11
|
+
# Expects the enclosing context to define `build_with_id`, taking an id
|
|
12
|
+
# value and returning the document built with it in nested attributes.
|
|
13
|
+
shared_examples 'rejects non-scalar ids' do |klass_name|
|
|
14
|
+
not_found = /not found for class #{klass_name}/
|
|
15
|
+
|
|
16
|
+
context 'when the id is an operator hash' do
|
|
17
|
+
it 'raises a document not found error' do
|
|
18
|
+
expect { build_with_id({ '$ne' => nil }) }
|
|
19
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context 'when the id is an array' do
|
|
24
|
+
it 'raises a document not found error' do
|
|
25
|
+
expect { build_with_id([ BSON::ObjectId.new ]) }
|
|
26
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'when the id is a malformed object id hash' do
|
|
31
|
+
it 'raises a document not found error' do
|
|
32
|
+
expect { build_with_id({ '$oid' => 'junk' }) }
|
|
33
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound, not_found)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
11
37
|
|
|
12
38
|
describe ".accepts_nested_attributes_for" do
|
|
13
39
|
|
|
14
40
|
context "when the autosave option is not defined" do
|
|
15
41
|
|
|
42
|
+
|
|
16
43
|
let(:person) do
|
|
17
44
|
Person.new
|
|
18
45
|
end
|
|
@@ -164,24 +191,71 @@ describe Mongoid::Attributes::Nested do
|
|
|
164
191
|
end
|
|
165
192
|
end
|
|
166
193
|
|
|
167
|
-
context
|
|
168
|
-
|
|
194
|
+
context 'when the relation is a has-many' do
|
|
169
195
|
before do
|
|
170
196
|
Person.send(:undef_method, :posts_attributes=)
|
|
171
197
|
Person.accepts_nested_attributes_for :posts
|
|
172
198
|
end
|
|
173
199
|
|
|
174
|
-
|
|
175
|
-
|
|
200
|
+
context 'when adding a new document to a relation' do
|
|
201
|
+
let(:person) do
|
|
202
|
+
Person.new(posts_attributes: { '1' => { title: 'First' } })
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'sets the nested attributes' do
|
|
206
|
+
expect(person.posts.first.title).to eq('First')
|
|
207
|
+
end
|
|
176
208
|
end
|
|
177
209
|
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
context 'when adding an existing document to a relation' do
|
|
211
|
+
let(:person1) { Person.create! }
|
|
212
|
+
let(:post) { person1.posts.create!(title: 'Sample Post') }
|
|
213
|
+
|
|
214
|
+
let(:person2) { Person.create!(posts_attributes: { '0' => { id: post.id, title: 'Reparented!' } }) }
|
|
215
|
+
|
|
216
|
+
context 'when allow_reparenting_via_nested_attributes is false' do
|
|
217
|
+
config_override :allow_reparenting_via_nested_attributes, false
|
|
218
|
+
|
|
219
|
+
it 'raises a document not found error' do
|
|
220
|
+
expect { person2 }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
225
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
226
|
+
|
|
227
|
+
it 'sets the nested attributes' do
|
|
228
|
+
expect(person2.posts.map(&:title)).to eq([ 'Reparented!' ])
|
|
229
|
+
end
|
|
230
|
+
end
|
|
180
231
|
end
|
|
181
|
-
end
|
|
182
232
|
|
|
183
|
-
|
|
233
|
+
context 'when the id in the attributes is not a scalar' do
|
|
234
|
+
let(:person) { Person.create! }
|
|
235
|
+
|
|
236
|
+
def build_with_id(id)
|
|
237
|
+
person.update!(posts_attributes: { '0' => { id: id, title: 'Crafted' } })
|
|
238
|
+
end
|
|
184
239
|
|
|
240
|
+
context 'when the association is empty' do
|
|
241
|
+
include_examples 'rejects non-scalar ids', 'Post'
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
context 'when the association is not empty' do
|
|
245
|
+
before { person.posts.create!(title: 'Existing') }
|
|
246
|
+
|
|
247
|
+
include_examples 'rejects non-scalar ids', 'Post'
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
251
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
252
|
+
|
|
253
|
+
include_examples 'rejects non-scalar ids', 'Post'
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
context 'when the relation is a has-and-belongs-to-many' do
|
|
185
259
|
before do
|
|
186
260
|
Person.send(:undef_method, :preferences_attributes=)
|
|
187
261
|
Person.accepts_nested_attributes_for :preferences
|
|
@@ -203,16 +277,100 @@ describe Mongoid::Attributes::Nested do
|
|
|
203
277
|
)
|
|
204
278
|
end
|
|
205
279
|
|
|
206
|
-
|
|
207
|
-
|
|
280
|
+
context 'when allow_reparenting_via_nested_attributes is false' do
|
|
281
|
+
config_override :allow_reparenting_via_nested_attributes, false
|
|
282
|
+
|
|
283
|
+
it 'raises a document not found error' do
|
|
284
|
+
expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
285
|
+
/Document\(s\) not found for class Preference/)
|
|
286
|
+
end
|
|
208
287
|
end
|
|
209
288
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
289
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
290
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
291
|
+
|
|
292
|
+
it 'sets the nested attributes' do
|
|
293
|
+
expect(person.preferences.map(&:name)).to eq([ preference.name ])
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'updates attributes of existing document which is added to relation' do
|
|
297
|
+
preference_name = 'updated preference'
|
|
298
|
+
person = Person.new(
|
|
299
|
+
preferences_attributes: { 0 => { id: preference.id, name: preference_name } }
|
|
300
|
+
)
|
|
301
|
+
expect(person.preferences.map(&:name)).to eq([ preference_name ])
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
context 'when the id does not correspond to an existing document' do
|
|
305
|
+
let(:person) do
|
|
306
|
+
Person.new(
|
|
307
|
+
preferences_attributes: { 0 => { id: BSON::ObjectId.new, name: 'Ghost' } }
|
|
308
|
+
)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
context 'when raise_not_found_error is true' do
|
|
312
|
+
config_override :raise_not_found_error, true
|
|
313
|
+
|
|
314
|
+
it 'raises a document not found error' do
|
|
315
|
+
expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
316
|
+
/Document\(s\) not found for class Preference/)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context 'when raise_not_found_error is false' do
|
|
321
|
+
config_override :raise_not_found_error, false
|
|
322
|
+
|
|
323
|
+
it 'raises a document not found error' do
|
|
324
|
+
expect { person }.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
325
|
+
/Document\(s\) not found for class Preference/)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
context 'when _destroy is true for a document not in the relation' do
|
|
332
|
+
before do
|
|
333
|
+
Person.send(:undef_method, :preferences_attributes=)
|
|
334
|
+
Person.accepts_nested_attributes_for :preferences, allow_destroy: true
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
let(:person) do
|
|
338
|
+
Person.new(
|
|
339
|
+
preferences_attributes: { 0 => { id: preference.id, _destroy: '1' } }
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'does not raise UnknownAttribute' do
|
|
344
|
+
expect { person }.not_to raise_error
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it 'does not add the document to the relation' do
|
|
348
|
+
expect(person.preferences).to be_empty
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
context 'when the id in the attributes is not a scalar' do
|
|
354
|
+
let(:target) { Person.create! }
|
|
355
|
+
|
|
356
|
+
def build_with_id(id)
|
|
357
|
+
target.update!(preferences_attributes: { 0 => { id: id, name: 'Crafted' } })
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
context 'when the association is empty' do
|
|
361
|
+
include_examples 'rejects non-scalar ids', 'Preference'
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
context 'when the association is not empty' do
|
|
365
|
+
before { target.preferences.create!(name: 'Existing') }
|
|
366
|
+
|
|
367
|
+
include_examples 'rejects non-scalar ids', 'Preference'
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
371
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
372
|
+
|
|
373
|
+
include_examples 'rejects non-scalar ids', 'Preference'
|
|
216
374
|
end
|
|
217
375
|
end
|
|
218
376
|
end
|
|
@@ -1355,10 +1513,11 @@ describe Mongoid::Attributes::Nested do
|
|
|
1355
1513
|
context "when the ids do not match" do
|
|
1356
1514
|
|
|
1357
1515
|
it "raises an error" do
|
|
1358
|
-
expect
|
|
1516
|
+
expect do
|
|
1359
1517
|
person.addresses_attributes =
|
|
1360
|
-
{
|
|
1361
|
-
|
|
1518
|
+
{ 'foo' => { 'id' => 'test', 'street' => 'Test' } }
|
|
1519
|
+
end.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
1520
|
+
/Document\(s\) not found for class Address/)
|
|
1362
1521
|
end
|
|
1363
1522
|
end
|
|
1364
1523
|
end
|
|
@@ -3012,12 +3171,12 @@ describe Mongoid::Attributes::Nested do
|
|
|
3012
3171
|
end
|
|
3013
3172
|
|
|
3014
3173
|
it "raises a document not found error" do
|
|
3015
|
-
expect
|
|
3174
|
+
expect do
|
|
3016
3175
|
person.posts_attributes =
|
|
3017
|
-
{
|
|
3018
|
-
{
|
|
3019
|
-
|
|
3020
|
-
|
|
3176
|
+
{ '0' =>
|
|
3177
|
+
{ 'id' => BSON::ObjectId.new.to_s, 'title' => 'Rogue' } }
|
|
3178
|
+
end.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
3179
|
+
/Document not found for class Post/)
|
|
3021
3180
|
end
|
|
3022
3181
|
end
|
|
3023
3182
|
end
|
|
@@ -3048,10 +3207,11 @@ describe Mongoid::Attributes::Nested do
|
|
|
3048
3207
|
context "when the ids do not match" do
|
|
3049
3208
|
|
|
3050
3209
|
it "raises an error" do
|
|
3051
|
-
expect
|
|
3210
|
+
expect do
|
|
3052
3211
|
person.posts_attributes =
|
|
3053
|
-
{
|
|
3054
|
-
|
|
3212
|
+
{ 'foo' => { 'id' => 'test', 'title' => 'Test' } }
|
|
3213
|
+
end.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
3214
|
+
/Document not found for class Post/)
|
|
3055
3215
|
end
|
|
3056
3216
|
end
|
|
3057
3217
|
end
|
|
@@ -3765,10 +3925,11 @@ describe Mongoid::Attributes::Nested do
|
|
|
3765
3925
|
context "when the ids do not match" do
|
|
3766
3926
|
|
|
3767
3927
|
it "raises an error" do
|
|
3768
|
-
expect
|
|
3928
|
+
expect do
|
|
3769
3929
|
person.preferences_attributes =
|
|
3770
|
-
{
|
|
3771
|
-
|
|
3930
|
+
{ 'foo' => { 'id' => 'test', 'name' => 'Test' } }
|
|
3931
|
+
end.to raise_error(Mongoid::Errors::DocumentNotFound,
|
|
3932
|
+
/Document\(s\) not found for class Preference/)
|
|
3772
3933
|
end
|
|
3773
3934
|
end
|
|
3774
3935
|
end
|
|
@@ -4999,4 +5160,63 @@ describe Mongoid::Attributes::Nested do
|
|
|
4999
5160
|
end
|
|
5000
5161
|
end
|
|
5001
5162
|
end
|
|
5163
|
+
|
|
5164
|
+
context 'when an id in nested attributes belongs to another parent' do
|
|
5165
|
+
let(:victim) { Person.create! }
|
|
5166
|
+
let(:attacker) { Person.create! }
|
|
5167
|
+
|
|
5168
|
+
context 'when the association is a has-many' do
|
|
5169
|
+
before do
|
|
5170
|
+
Person.send(:undef_method, :posts_attributes=)
|
|
5171
|
+
Person.accepts_nested_attributes_for :posts
|
|
5172
|
+
end
|
|
5173
|
+
|
|
5174
|
+
let!(:victim_post) { victim.posts.create!(title: 'Private') }
|
|
5175
|
+
|
|
5176
|
+
it 'does not update the other parent\'s document' do
|
|
5177
|
+
expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
|
|
5178
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
5179
|
+
expect(victim_post.reload.title).to eq('Private')
|
|
5180
|
+
end
|
|
5181
|
+
|
|
5182
|
+
it 'does not add the other parent\'s document to the association' do
|
|
5183
|
+
expect { attacker.update!(posts_attributes: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
|
|
5184
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
5185
|
+
expect(attacker.reload.posts).to be_empty
|
|
5186
|
+
end
|
|
5187
|
+
|
|
5188
|
+
it 'does not update via a direct association assignment' do
|
|
5189
|
+
expect { attacker.update!(posts: { '0' => { id: victim_post.id, title: 'Overwritten' } }) }
|
|
5190
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
5191
|
+
expect(victim_post.reload.title).to eq('Private')
|
|
5192
|
+
end
|
|
5193
|
+
end
|
|
5194
|
+
|
|
5195
|
+
context 'when the association is a has-and-belongs-to-many' do
|
|
5196
|
+
before do
|
|
5197
|
+
Person.send(:undef_method, :preferences_attributes=)
|
|
5198
|
+
Person.accepts_nested_attributes_for :preferences
|
|
5199
|
+
end
|
|
5200
|
+
|
|
5201
|
+
let!(:victim_preference) { victim.preferences.create!(name: 'Private') }
|
|
5202
|
+
|
|
5203
|
+
it 'does not update the other parent\'s document' do
|
|
5204
|
+
expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
|
|
5205
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
5206
|
+
expect(victim_preference.reload.name).to eq('Private')
|
|
5207
|
+
end
|
|
5208
|
+
|
|
5209
|
+
it 'does not add the other parent\'s document to the association' do
|
|
5210
|
+
expect { attacker.update!(preferences_attributes: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
|
|
5211
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
5212
|
+
expect(attacker.reload.preferences).to be_empty
|
|
5213
|
+
end
|
|
5214
|
+
|
|
5215
|
+
it 'does not update via a direct association assignment' do
|
|
5216
|
+
expect { attacker.update!(preferences: { '0' => { id: victim_preference.id, name: 'Overwritten' } }) }
|
|
5217
|
+
.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
5218
|
+
expect(victim_preference.reload.name).to eq('Private')
|
|
5219
|
+
end
|
|
5220
|
+
end
|
|
5221
|
+
end
|
|
5002
5222
|
end
|
|
@@ -41,6 +41,18 @@ describe Mongoid::Config::Defaults do
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
shared_examples 'uses settings for 9.0' do
|
|
45
|
+
it 'uses settings for 9.0' do
|
|
46
|
+
expect(Mongoid.autosave_saves_unchanged_documents).to be true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
shared_examples 'never allows reparenting via nested attributes' do
|
|
51
|
+
it 'leaves allow_reparenting_via_nested_attributes false' do
|
|
52
|
+
expect(Mongoid.allow_reparenting_via_nested_attributes).to be false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
context "when giving a valid version" do
|
|
45
57
|
|
|
46
58
|
before do
|
|
@@ -57,6 +69,8 @@ describe Mongoid::Config::Defaults do
|
|
|
57
69
|
|
|
58
70
|
it_behaves_like "uses settings for 8.0"
|
|
59
71
|
it_behaves_like "uses settings for 8.1"
|
|
72
|
+
it_behaves_like 'uses settings for 9.0'
|
|
73
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
60
74
|
end
|
|
61
75
|
|
|
62
76
|
context "when the given version is 8.1" do
|
|
@@ -65,6 +79,8 @@ describe Mongoid::Config::Defaults do
|
|
|
65
79
|
|
|
66
80
|
it_behaves_like "does not use settings for 8.0"
|
|
67
81
|
it_behaves_like "uses settings for 8.1"
|
|
82
|
+
it_behaves_like 'uses settings for 9.0'
|
|
83
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
68
84
|
end
|
|
69
85
|
|
|
70
86
|
context "when the given version is 9.0" do
|
|
@@ -73,6 +89,8 @@ describe Mongoid::Config::Defaults do
|
|
|
73
89
|
|
|
74
90
|
it_behaves_like "does not use settings for 8.0"
|
|
75
91
|
it_behaves_like "does not use settings for 8.1"
|
|
92
|
+
it_behaves_like 'uses settings for 9.0'
|
|
93
|
+
it_behaves_like 'never allows reparenting via nested attributes'
|
|
76
94
|
end
|
|
77
95
|
end
|
|
78
96
|
|
|
@@ -75,6 +75,19 @@ describe Mongoid::Config::Encryption do
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Models are registered in class load order, and Rails eager loads
|
|
79
|
+
# app/models alphabetically, so a child whose file name sorts before
|
|
80
|
+
# its parent's arrives first.
|
|
81
|
+
context 'when the embedded model comes before its parent' do
|
|
82
|
+
let(:models) do
|
|
83
|
+
[ Crypt::Insurance, Crypt::Patient ]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns a map of encryption schemas' do
|
|
87
|
+
expect(encryption_schema_map).to eq(expected_schema_map)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
78
91
|
context 'and fields do not have encryption options' do
|
|
79
92
|
let(:models) do
|
|
80
93
|
[Crypt::Car]
|
|
@@ -109,6 +122,7 @@ describe Mongoid::Config::Encryption do
|
|
|
109
122
|
let(:expected_schema_map) do
|
|
110
123
|
{
|
|
111
124
|
"mongoid_test.crypt_users" => {
|
|
125
|
+
'bsonType' => 'object',
|
|
112
126
|
"properties" => {
|
|
113
127
|
"name" => {
|
|
114
128
|
"encrypt" => {
|
|
@@ -148,5 +162,219 @@ describe Mongoid::Config::Encryption do
|
|
|
148
162
|
expect(encryption_schema_map).to eq({})
|
|
149
163
|
end
|
|
150
164
|
end
|
|
165
|
+
|
|
166
|
+
# A field left out of the schema map is written in plaintext, with no
|
|
167
|
+
# exception and nothing logged, so an omission here is silent data
|
|
168
|
+
# exposure rather than a broken feature.
|
|
169
|
+
context 'when the encrypted fields are on an embedded model' do
|
|
170
|
+
let(:token_properties) do
|
|
171
|
+
{
|
|
172
|
+
'bsonType' => 'object',
|
|
173
|
+
'properties' => {
|
|
174
|
+
'value' => {
|
|
175
|
+
'encrypt' => {
|
|
176
|
+
'bsonType' => 'string',
|
|
177
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context 'when two models embed the same encrypted model' do
|
|
185
|
+
let(:models) do
|
|
186
|
+
[ Crypt::Vault, Crypt::Chest ]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'maps the embedded model for the first parent' do
|
|
190
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_vaults', 'properties', 'token'))
|
|
191
|
+
.to eq(token_properties)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'maps the embedded model for the second parent' do
|
|
195
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_chests', 'properties', 'token'))
|
|
196
|
+
.to eq(token_properties)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
context 'when one model embeds the same encrypted model twice' do
|
|
201
|
+
let(:models) do
|
|
202
|
+
[ Crypt::Ledger ]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'maps the first relation' do
|
|
206
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_ledgers', 'properties', 'primary_token'))
|
|
207
|
+
.to eq(token_properties)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'maps the second relation' do
|
|
211
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_ledgers', 'properties', 'backup_token'))
|
|
212
|
+
.to eq(token_properties)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# relation_class constantizes, and a polymorphic embedded_in has no class
|
|
217
|
+
# to resolve, so the walk has to look at the relation type first.
|
|
218
|
+
context 'when the embedded model is embedded polymorphically' do
|
|
219
|
+
let(:models) do
|
|
220
|
+
[ Crypt::Vault ]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'does not raise' do
|
|
224
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
context 'when the embedded model comes before its parent' do
|
|
229
|
+
let(:models) do
|
|
230
|
+
[ Crypt::Token, Crypt::Vault ]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'maps the embedded model' do
|
|
234
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_vaults', 'properties', 'token'))
|
|
235
|
+
.to eq(token_properties)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
context 'when the parent has no encrypted field of its own' do
|
|
240
|
+
let(:models) do
|
|
241
|
+
[ Crypt::Wallet, Crypt::Token ]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'maps the embedded model' do
|
|
245
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_wallets', 'properties', 'token'))
|
|
246
|
+
.to eq(token_properties)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
context 'when the encrypted model is nested three levels deep' do
|
|
251
|
+
let(:account_properties) do
|
|
252
|
+
{
|
|
253
|
+
'bsonType' => 'object',
|
|
254
|
+
'properties' => {
|
|
255
|
+
'credential' => {
|
|
256
|
+
'bsonType' => 'object',
|
|
257
|
+
'properties' => {
|
|
258
|
+
'secret' => {
|
|
259
|
+
'encrypt' => {
|
|
260
|
+
'bsonType' => 'string',
|
|
261
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
context 'when the models are listed outermost first' do
|
|
271
|
+
let(:models) do
|
|
272
|
+
[ Crypt::Owner, Crypt::Account, Crypt::Credential ]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'maps the whole subtree' do
|
|
276
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_owners', 'properties', 'account'))
|
|
277
|
+
.to eq(account_properties)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
context 'when the middle model comes first' do
|
|
282
|
+
let(:models) do
|
|
283
|
+
[ Crypt::Account, Crypt::Owner, Crypt::Credential ]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it 'maps the whole subtree' do
|
|
287
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_owners', 'properties', 'account'))
|
|
288
|
+
.to eq(account_properties)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The walk has to keep stopping at a model it is already inside of,
|
|
294
|
+
# otherwise a self-embedding model recurses forever.
|
|
295
|
+
context 'when a model embeds itself' do
|
|
296
|
+
let(:models) do
|
|
297
|
+
[ Crypt::Article ]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it 'terminates' do
|
|
301
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
it 'maps the embedded model without descending into the cycle' do
|
|
305
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_articles', 'properties', 'comment')).to eq(
|
|
306
|
+
'bsonType' => 'object',
|
|
307
|
+
'properties' => {
|
|
308
|
+
'body' => {
|
|
309
|
+
'encrypt' => {
|
|
310
|
+
'bsonType' => 'string',
|
|
311
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context 'when an encrypted model has a callable database name' do
|
|
321
|
+
let(:models) do
|
|
322
|
+
[ Crypt::DynamicCar ]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it 'does not build a key from the unresolved callable' do
|
|
326
|
+
expect(encryption_schema_map.keys.grep(/Proc/)).to be_empty
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
it 'does not build a key that the model can never write to' do
|
|
330
|
+
expect(encryption_schema_map.keys).to all(satisfy { |key| key.count('.') == 1 })
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
context 'when an encrypted model has a callable database name that needs a tenant' do
|
|
335
|
+
let(:models) do
|
|
336
|
+
[ Crypt::TenantCar ]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Resolving the callable while the map is built is not a fix: the
|
|
340
|
+
# documented multi-tenant idiom has no correct value at client
|
|
341
|
+
# construction time. Generating the map must not depend on it.
|
|
342
|
+
it 'does not raise' do
|
|
343
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it 'does not pin the map to whichever tenant happens to be current' do
|
|
347
|
+
Thread.current[:tenant_database] = 'tenant_a'
|
|
348
|
+
expect(encryption_schema_map.keys).not_to include('tenant_a.crypt_tenant_cars')
|
|
349
|
+
ensure
|
|
350
|
+
Thread.current[:tenant_database] = nil
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The map is generated over every model in the application, and a model may
|
|
355
|
+
# name an embedded class that is never defined. Resolving that name raises,
|
|
356
|
+
# which would take down every client build.
|
|
357
|
+
context 'when a model names an embedded class that is not defined' do
|
|
358
|
+
let(:models) do
|
|
359
|
+
[ Crypt::Drawer, Crypt::Cabinet ]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it 'does not raise' do
|
|
363
|
+
expect { encryption_schema_map }.not_to raise_error
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it 'leaves out the model with nothing to encrypt' do
|
|
367
|
+
expect(encryption_schema_map.keys).not_to include('mongoid_test.crypt_drawers')
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it 'still maps the encrypted fields of the model itself' do
|
|
371
|
+
expect(encryption_schema_map.dig('mongoid_test.crypt_cabinets', 'properties', 'label')).to eq(
|
|
372
|
+
'encrypt' => {
|
|
373
|
+
'bsonType' => 'string',
|
|
374
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
151
379
|
end
|
|
152
380
|
end
|