mongoid 9.0.11 → 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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +45 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/nested/many.rb +34 -5
  5. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  6. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  7. data/lib/mongoid/association/relatable.rb +17 -0
  8. data/lib/mongoid/collection_configurable.rb +8 -0
  9. data/lib/mongoid/config/encryption.rb +36 -18
  10. data/lib/mongoid/config.rb +56 -9
  11. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  12. data/lib/mongoid/contextual/memory.rb +18 -7
  13. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  14. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  15. data/lib/mongoid/encryptable.rb +46 -0
  16. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  17. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  18. data/lib/mongoid/errors.rb +2 -0
  19. data/lib/mongoid/field_readable.rb +70 -0
  20. data/lib/mongoid/indexable.rb +39 -27
  21. data/lib/mongoid/matchable.rb +6 -1
  22. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  23. data/lib/mongoid/matcher/regex.rb +9 -13
  24. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  25. data/lib/mongoid/matcher.rb +1 -0
  26. data/lib/mongoid/persistence_context.rb +35 -0
  27. data/lib/mongoid/search_indexable.rb +14 -7
  28. data/lib/mongoid/tasks/database.rb +23 -9
  29. data/lib/mongoid/threaded.rb +37 -0
  30. data/lib/mongoid/version.rb +1 -1
  31. data/spec/integration/app_spec.rb +7 -0
  32. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  33. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  34. data/spec/integration/encryption_spec.rb +149 -0
  35. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  36. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  37. data/spec/integration/query_operator_guard_spec.rb +89 -0
  38. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  39. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  40. data/spec/mongoid/attributes/nested_spec.rb +204 -10
  41. data/spec/mongoid/config/defaults_spec.rb +18 -0
  42. data/spec/mongoid/config/encryption_spec.rb +228 -0
  43. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  44. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  45. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  46. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  47. data/spec/mongoid/criteria_spec.rb +2 -0
  48. data/spec/mongoid/encryptable_spec.rb +61 -0
  49. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  50. data/spec/mongoid/tasks/database_spec.rb +18 -0
  51. data/spec/support/crypt/models.rb +157 -0
  52. metadata +14 -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,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
@@ -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
@@ -588,6 +588,27 @@ describe Mongoid::Contextual::Memory do
588
588
  expect(context.distinct("label.sales")).to eq([ BigDecimal("1E2") ])
589
589
  end
590
590
  end
591
+
592
+ context 'when the field name is a method name' do
593
+ let!(:person) do
594
+ Person.create!(ssn: 'secret-ssn')
595
+ end
596
+
597
+ let!(:address) do
598
+ person.addresses.create!(street: 'hobrecht')
599
+ end
600
+
601
+ let(:criteria) do
602
+ Address.all.tap do |crit|
603
+ crit.documents = [ address ]
604
+ end
605
+ end
606
+
607
+ it 'does not call the method' do
608
+ expect(address).not_to receive(:destroy)
609
+ expect(context.distinct(:destroy)).to eq([ nil ])
610
+ end
611
+ end
591
612
  end
592
613
 
593
614
  describe "#each" do
@@ -1826,6 +1847,116 @@ describe Mongoid::Contextual::Memory do
1826
1847
  ])
1827
1848
  end
1828
1849
  end
1850
+
1851
+ context 'when plucking a dynamic attribute' do
1852
+ let(:criteria) do
1853
+ Band.all.tap do |crit|
1854
+ crit.documents = [ Band.create!(name: 'Depeche Mode', mood: 'dark') ]
1855
+ end
1856
+ end
1857
+
1858
+ it 'returns the value from the attributes' do
1859
+ expect(context.pluck(:mood)).to eq([ 'dark' ])
1860
+ end
1861
+ end
1862
+
1863
+ context 'when the field name is a method name' do
1864
+ let!(:person) do
1865
+ Person.create!(ssn: 'secret-ssn')
1866
+ end
1867
+
1868
+ let!(:address) do
1869
+ person.addresses.create!(street: 'hobrecht')
1870
+ end
1871
+
1872
+ let(:criteria) do
1873
+ Address.all.tap do |crit|
1874
+ crit.documents = [ address ]
1875
+ end
1876
+ end
1877
+
1878
+ it 'does not call the method' do
1879
+ expect(address).not_to receive(:destroy)
1880
+ expect(context.pluck(:destroy)).to eq([ nil ])
1881
+ end
1882
+
1883
+ it 'does not return the attributes hash' do
1884
+ expect(context.pluck(:attributes)).to eq([ nil ])
1885
+ end
1886
+
1887
+ it 'does not traverse to the parent document' do
1888
+ expect(context.pluck('_root.ssn')).to eq([ nil ])
1889
+ end
1890
+
1891
+ it 'does not destroy the parent document' do
1892
+ expect(context.pluck('_root.destroy')).to eq([ nil ])
1893
+ expect(Person.where(_id: person.id).count).to eq(1)
1894
+ end
1895
+ end
1896
+
1897
+ context 'when plucking a belongs_to association' do
1898
+ let!(:band) do
1899
+ Band.create!(name: 'Depeche Mode')
1900
+ end
1901
+
1902
+ let!(:artist) do
1903
+ Artist.create!(band: band)
1904
+ end
1905
+
1906
+ let(:criteria) do
1907
+ Artist.all.tap do |crit|
1908
+ crit.documents = [ artist ]
1909
+ end
1910
+ end
1911
+
1912
+ it 'traverses to the associated document' do
1913
+ expect(context.pluck('band.name')).to eq([ 'Depeche Mode' ])
1914
+ end
1915
+
1916
+ it 'returns the document for the association name' do
1917
+ expect(context.pluck(:band)).to eq([ band ])
1918
+ end
1919
+
1920
+ it 'returns the id for the foreign key' do
1921
+ expect(context.pluck(:band_id)).to eq([ band.id ])
1922
+ end
1923
+ end
1924
+
1925
+ context 'when plucking the foreign key of a many to many association' do
1926
+ let!(:preference) do
1927
+ Preference.create!(name: 'nature')
1928
+ end
1929
+
1930
+ let!(:person) do
1931
+ Person.create!(preferences: [ preference ])
1932
+ end
1933
+
1934
+ let(:criteria) do
1935
+ Person.all.tap do |crit|
1936
+ crit.documents = [ person ]
1937
+ end
1938
+ end
1939
+
1940
+ it 'returns the stored ids' do
1941
+ expect(context.pluck(:preference_ids)).to eq([ [ preference.id ] ])
1942
+ end
1943
+ end
1944
+
1945
+ context 'when the field path has an empty segment' do
1946
+ let!(:band) do
1947
+ Band.create!(name: 'Depeche Mode', label: Label.new(name: 'Mute'))
1948
+ end
1949
+
1950
+ let(:criteria) do
1951
+ Band.all.tap do |crit|
1952
+ crit.documents = [ band ]
1953
+ end
1954
+ end
1955
+
1956
+ it 'returns nil' do
1957
+ expect(context.pluck('label..name')).to eq([ nil ])
1958
+ end
1959
+ end
1829
1960
  end
1830
1961
 
1831
1962
  describe "#pick" do
@@ -1886,6 +2017,27 @@ describe Mongoid::Contextual::Memory do
1886
2017
  expect(picked).to be_nil
1887
2018
  end
1888
2019
  end
2020
+
2021
+ context 'when the field name is a method name' do
2022
+ let!(:person) do
2023
+ Person.create!(ssn: 'secret-ssn')
2024
+ end
2025
+
2026
+ let!(:address) do
2027
+ person.addresses.create!(street: 'hobrecht')
2028
+ end
2029
+
2030
+ let(:criteria) do
2031
+ Address.all.tap do |crit|
2032
+ crit.documents = [ address ]
2033
+ end
2034
+ end
2035
+
2036
+ it 'does not call the method' do
2037
+ expect(address).not_to receive(:destroy)
2038
+ expect(context.pick(:destroy)).to be_nil
2039
+ end
2040
+ end
1889
2041
  end
1890
2042
 
1891
2043
  describe "#tally" do
@@ -2340,6 +2492,31 @@ describe Mongoid::Contextual::Memory do
2340
2492
  )
2341
2493
  end
2342
2494
  end
2495
+
2496
+ context 'when the field name is a method name' do
2497
+ let!(:person) do
2498
+ Person.create!(ssn: 'secret-ssn')
2499
+ end
2500
+
2501
+ let!(:address) do
2502
+ person.addresses.create!(street: 'hobrecht')
2503
+ end
2504
+
2505
+ let(:criteria) do
2506
+ Address.all.tap do |crit|
2507
+ crit.documents = [ address ]
2508
+ end
2509
+ end
2510
+
2511
+ it 'does not call the method' do
2512
+ expect(address).not_to receive(:destroy)
2513
+ expect(context.tally(:destroy)).to eq(nil => 1)
2514
+ end
2515
+
2516
+ it 'does not disclose the parent document fields' do
2517
+ expect(context.tally('_root.ssn')).to eq(nil => 1)
2518
+ end
2519
+ end
2343
2520
  end
2344
2521
 
2345
2522
  describe '#inc' do
@@ -1830,6 +1830,8 @@ describe Mongoid::Criteria::Queryable::Selectable do
1830
1830
  end
1831
1831
 
1832
1832
  context 'when the following criteria uses string were form' do
1833
+ # String criteria compile to $where, which requires the opt-in.
1834
+ config_override :allow_unsafe_query_operators, true
1833
1835
 
1834
1836
  let(:selection) do
1835
1837
  query.not.where('hello world')