mongoid 9.0.11 → 9.0.13

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +65 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/embedded/batchable.rb +8 -0
  5. data/lib/mongoid/association/nested/many.rb +34 -5
  6. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  7. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  8. data/lib/mongoid/association/relatable.rb +17 -0
  9. data/lib/mongoid/clients/factory.rb +2 -0
  10. data/lib/mongoid/collection_configurable.rb +8 -0
  11. data/lib/mongoid/config/encryption.rb +171 -22
  12. data/lib/mongoid/config.rb +56 -9
  13. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  14. data/lib/mongoid/contextual/memory.rb +18 -7
  15. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  16. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  17. data/lib/mongoid/encryptable.rb +73 -0
  18. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  19. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  20. data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
  21. data/lib/mongoid/errors.rb +3 -0
  22. data/lib/mongoid/field_readable.rb +70 -0
  23. data/lib/mongoid/indexable.rb +39 -27
  24. data/lib/mongoid/matchable.rb +6 -1
  25. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  26. data/lib/mongoid/matcher/regex.rb +9 -13
  27. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  28. data/lib/mongoid/matcher.rb +1 -0
  29. data/lib/mongoid/persistence_context.rb +77 -0
  30. data/lib/mongoid/search_indexable.rb +19 -8
  31. data/lib/mongoid/tasks/database.rb +33 -16
  32. data/lib/mongoid/threaded.rb +37 -0
  33. data/lib/mongoid/version.rb +1 -1
  34. data/lib/mongoid/warnings.rb +4 -0
  35. data/spec/integration/app_spec.rb +7 -0
  36. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  37. data/spec/integration/atomic/modifiers_spec.rb +57 -0
  38. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  39. data/spec/integration/encryption_spec.rb +298 -0
  40. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  41. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  42. data/spec/integration/query_operator_guard_spec.rb +89 -0
  43. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  44. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  45. data/spec/mongoid/attributes/nested_spec.rb +204 -10
  46. data/spec/mongoid/clients/factory_spec.rb +36 -0
  47. data/spec/mongoid/config/defaults_spec.rb +18 -0
  48. data/spec/mongoid/config/encryption_spec.rb +451 -0
  49. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  50. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  51. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  52. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  53. data/spec/mongoid/criteria_spec.rb +2 -0
  54. data/spec/mongoid/encryptable_spec.rb +115 -0
  55. data/spec/mongoid/matcher/regexp_budget_spec.rb +576 -0
  56. data/spec/mongoid/search_indexable_spec.rb +34 -1
  57. data/spec/mongoid/tasks/database_spec.rb +31 -0
  58. data/spec/support/crypt/models.rb +253 -0
  59. data/spec/support/models/modifier_models.rb +32 -0
  60. metadata +17 -2
@@ -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,442 @@ 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
379
+
380
+ # The map has one entry per namespace. Every model that writes to the
381
+ # namespace, including subclasses that are never registered in
382
+ # Mongoid.models, has to be covered by that entry, or its encrypted fields
383
+ # are written in plaintext.
384
+ context 'when more than one model writes to a namespace' do
385
+ let(:encrypt_metadata) do
386
+ {
387
+ 'keyId' => [ BSON::Binary.new(Base64.decode64('grolrnFVSSW9Gq04Q87R9Q=='), :uuid) ],
388
+ 'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
389
+ }
390
+ end
391
+
392
+ let(:encrypted_string) do
393
+ { 'encrypt' => { 'bsonType' => 'string' } }
394
+ end
395
+
396
+ context 'when two models are stored in one collection' do
397
+ let(:expected_entry) do
398
+ {
399
+ 'bsonType' => 'object',
400
+ 'encryptMetadata' => encrypt_metadata,
401
+ 'properties' => {
402
+ 'serial' => encrypted_string,
403
+ 'holder' => encrypted_string
404
+ }
405
+ }
406
+ end
407
+
408
+ context 'when the models are registered in one order' do
409
+ let(:models) do
410
+ [ Crypt::Badge, Crypt::BadgeScan ]
411
+ end
412
+
413
+ it 'maps the fields of both models' do
414
+ expect(encryption_schema_map['mongoid_test.crypt_badges']).to eq(expected_entry)
415
+ end
416
+ end
417
+
418
+ context 'when the models are registered in the other order' do
419
+ let(:models) do
420
+ [ Crypt::BadgeScan, Crypt::Badge ]
421
+ end
422
+
423
+ it 'maps the fields of both models' do
424
+ expect(encryption_schema_map['mongoid_test.crypt_badges']).to eq(expected_entry)
425
+ end
426
+ end
427
+ end
428
+
429
+ context 'when a subclass adds an encrypted field to an encrypted parent' do
430
+ let(:expected_entry) do
431
+ {
432
+ 'bsonType' => 'object',
433
+ 'encryptMetadata' => encrypt_metadata,
434
+ 'properties' => {
435
+ 'ssn' => encrypted_string,
436
+ 'salary' => encrypted_string,
437
+ 'rate' => encrypted_string
438
+ }
439
+ }
440
+ end
441
+
442
+ # Subclasses are not registered in Mongoid.models, so only the parent
443
+ # is passed in.
444
+ context 'when only the parent is registered' do
445
+ let(:models) do
446
+ [ Crypt::Member ]
447
+ end
448
+
449
+ it 'maps the fields of the parent and of every subclass' do
450
+ expect(encryption_schema_map['mongoid_test.crypt_members']).to eq(expected_entry)
451
+ end
452
+ end
453
+
454
+ context 'when a subclass is registered as well' do
455
+ let(:models) do
456
+ [ Crypt::Member, Crypt::Employee ]
457
+ end
458
+
459
+ it 'maps the fields of the parent and of every subclass' do
460
+ expect(encryption_schema_map['mongoid_test.crypt_members']).to eq(expected_entry)
461
+ end
462
+ end
463
+ end
464
+
465
+ context 'when only a subclass has encrypted fields' do
466
+ let(:models) do
467
+ [ Crypt::Listing ]
468
+ end
469
+
470
+ it 'maps the fields of the subclass' do
471
+ expect(encryption_schema_map['mongoid_test.crypt_listings']).to eq(
472
+ 'bsonType' => 'object',
473
+ 'encryptMetadata' => encrypt_metadata,
474
+ 'properties' => {
475
+ 'owner_ssn' => encrypted_string
476
+ }
477
+ )
478
+ end
479
+ end
480
+
481
+ context 'when a subclass of an embeds_one target adds an encrypted field' do
482
+ let(:models) do
483
+ [ Crypt::Box ]
484
+ end
485
+
486
+ it 'maps the fields of the target and of its subclass' do
487
+ expect(encryption_schema_map.dig('mongoid_test.crypt_boxes', 'properties', 'item')).to eq(
488
+ 'bsonType' => 'object',
489
+ 'properties' => {
490
+ 'label' => encrypted_string,
491
+ 'seal' => encrypted_string
492
+ }
493
+ )
494
+ end
495
+ end
496
+
497
+ context 'when only a subclass of an embeds_one target has encrypted fields' do
498
+ let(:models) do
499
+ [ Crypt::Locker ]
500
+ end
501
+
502
+ it 'maps the fields of the subclass' do
503
+ expect(encryption_schema_map.dig('mongoid_test.crypt_lockers', 'properties', 'content')).to eq(
504
+ 'bsonType' => 'object',
505
+ 'encryptMetadata' => encrypt_metadata,
506
+ 'properties' => {
507
+ 'code' => encrypted_string
508
+ }
509
+ )
510
+ end
511
+ end
512
+
513
+ # These models are not added to the shared support file: every client
514
+ # with automatic encryption builds the map over all registered models,
515
+ # and a conflict would make every such client fail to build.
516
+ context 'when models on one collection disagree on encryption' do
517
+ let(:defined_models) { [] }
518
+
519
+ after do
520
+ defined_models.each { |model| Mongoid.deregister_model(model) }
521
+ end
522
+
523
+ def define_model(&block)
524
+ model = Class.new do
525
+ include Mongoid::Document
526
+
527
+ store_in collection: 'crypt_conflicts'
528
+ end
529
+ model.class_eval(&block)
530
+ defined_models << model
531
+ model
532
+ end
533
+
534
+ context 'when the same field is encrypted the same way' do
535
+ let(:models) do
536
+ [
537
+ define_model { field :code, type: String, encrypt: { deterministic: true } },
538
+ define_model { field :code, type: String, encrypt: { deterministic: true } }
539
+ ]
540
+ end
541
+
542
+ it 'maps the field once' do
543
+ expect(encryption_schema_map['mongoid_test.crypt_conflicts']).to eq(
544
+ 'bsonType' => 'object',
545
+ 'properties' => {
546
+ 'code' => {
547
+ 'encrypt' => {
548
+ 'bsonType' => 'string',
549
+ 'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
550
+ }
551
+ }
552
+ }
553
+ )
554
+ end
555
+ end
556
+
557
+ context 'when the same field is encrypted differently' do
558
+ let(:models) do
559
+ [
560
+ define_model { field :code, type: String, encrypt: { deterministic: true } },
561
+ define_model { field :code, type: String, encrypt: { deterministic: false } }
562
+ ]
563
+ end
564
+
565
+ it 'raises' do
566
+ expect { encryption_schema_map }.to raise_error(Mongoid::Errors::InvalidEncryptionSchema)
567
+ end
568
+ end
569
+
570
+ context 'when the collection metadata differs' do
571
+ let(:models) do
572
+ [
573
+ define_model do
574
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
575
+ field :code, type: String, encrypt: true
576
+ end,
577
+ define_model do
578
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: false
579
+ field :name, type: String, encrypt: true
580
+ end
581
+ ]
582
+ end
583
+
584
+ it 'raises' do
585
+ expect { encryption_schema_map }.to raise_error(Mongoid::Errors::InvalidEncryptionSchema)
586
+ end
587
+ end
588
+
589
+ context 'when a subclass encrypts an inherited field differently' do
590
+ let(:models) do
591
+ parent = define_model { field :code, type: String, encrypt: { deterministic: true } }
592
+ Class.new(parent) { field :code, type: String, encrypt: { deterministic: false } }
593
+ [ parent ]
594
+ end
595
+
596
+ it 'raises' do
597
+ expect { encryption_schema_map }.to raise_error(Mongoid::Errors::InvalidEncryptionSchema)
598
+ end
599
+ end
600
+ end
601
+ end
151
602
  end
152
603
  end
@@ -582,4 +582,95 @@ describe Mongoid::Contextual::Aggregable::Memory do
582
582
  end
583
583
  end
584
584
  end
585
+
586
+ context 'when the field name is a method name' do
587
+ let!(:depeche) do
588
+ Band.create!(name: 'Depeche Mode', likes: 1000)
589
+ end
590
+
591
+ let(:criteria) do
592
+ Band.all.tap do |crit|
593
+ crit.documents = [ depeche ]
594
+ end
595
+ end
596
+
597
+ it 'does not call the method for sum' do
598
+ expect(depeche).not_to receive(:delete)
599
+ expect(context.sum(:delete)).to eq(0)
600
+ end
601
+
602
+ it 'does not call the method for min' do
603
+ expect(depeche).not_to receive(:delete)
604
+ expect(context.min(:delete)).to be_nil
605
+ end
606
+
607
+ it 'does not call the method for max' do
608
+ expect(depeche).not_to receive(:delete)
609
+ expect(context.max(:delete)).to be_nil
610
+ end
611
+
612
+ it 'does not call the method for avg' do
613
+ expect(depeche).not_to receive(:delete)
614
+ expect(context.avg(:delete)).to be_nil
615
+ end
616
+
617
+ it 'does not call the method for aggregates' do
618
+ expect(depeche).not_to receive(:delete)
619
+ expect(context.aggregates(:delete))
620
+ .to eq('count' => 0, 'avg' => nil, 'max' => nil, 'min' => nil, 'sum' => 0)
621
+ end
622
+
623
+ it 'does not call a private method for avg' do
624
+ expect(depeche).not_to receive(:exit)
625
+ expect(context.avg(:exit)).to be_nil
626
+ end
627
+ end
628
+
629
+ context 'when the field is not defined' do
630
+ let!(:depeche) do
631
+ Band.create!(name: 'Depeche Mode', likes: 1000)
632
+ end
633
+
634
+ let(:criteria) do
635
+ Band.all.tap do |crit|
636
+ crit.documents = [ depeche ]
637
+ end
638
+ end
639
+
640
+ it 'returns zero for sum' do
641
+ expect(context.sum(:not_a_field)).to eq(0)
642
+ end
643
+
644
+ it 'returns nil for min' do
645
+ expect(context.min(:not_a_field)).to be_nil
646
+ end
647
+
648
+ it 'returns nil for max' do
649
+ expect(context.max(:not_a_field)).to be_nil
650
+ end
651
+
652
+ it 'returns nil for avg' do
653
+ expect(context.avg(:not_a_field)).to be_nil
654
+ end
655
+ end
656
+
657
+ context 'when the field is the foreign key of a many to many association' do
658
+ let!(:preference) do
659
+ Preference.create!(name: 'nature')
660
+ end
661
+
662
+ let!(:person) do
663
+ Person.new(preferences: [ preference ])
664
+ end
665
+
666
+ let(:criteria) do
667
+ Person.all.tap do |crit|
668
+ crit.documents = [ person ]
669
+ end
670
+ end
671
+
672
+ it 'aggregates the stored ids' do
673
+ expect(context.max(:preference_ids)).to eq([ preference.id ])
674
+ end
675
+ end
585
676
  end