mongoid 9.0.12 → 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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +31 -11
- data/lib/mongoid/association/embedded/batchable.rb +8 -0
- data/lib/mongoid/clients/factory.rb +2 -0
- data/lib/mongoid/config/encryption.rb +148 -17
- data/lib/mongoid/encryptable.rb +42 -15
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/persistence_context.rb +46 -4
- data/lib/mongoid/search_indexable.rb +5 -1
- data/lib/mongoid/tasks/database.rb +10 -7
- data/lib/mongoid/threaded.rb +1 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/atomic/modifiers_spec.rb +57 -0
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/encryption_spec.rb +223 -0
- data/spec/mongoid/encryptable_spec.rb +54 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +6 -0
- data/spec/mongoid/search_indexable_spec.rb +34 -1
- data/spec/mongoid/tasks/database_spec.rb +13 -0
- data/spec/support/crypt/models.rb +96 -0
- data/spec/support/models/modifier_models.rb +32 -0
- metadata +5 -2
|
@@ -114,4 +114,61 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
+
context 'when assigning embeds_many attributes on a new embedded document' do
|
|
118
|
+
let(:cabinet) do
|
|
119
|
+
ModifierModels::Cabinet.create!
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
before do
|
|
123
|
+
cabinet.reload
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
context 'when the nested documents are assigned on the built document' do
|
|
127
|
+
before do
|
|
128
|
+
cabinet.build_drawer(matcher: 'all')
|
|
129
|
+
cabinet.drawer.assign_attributes(items: [ { name: 'a' } ])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'saves without conflicting $set paths' do
|
|
133
|
+
expect { cabinet.save! }.not_to raise_error
|
|
134
|
+
|
|
135
|
+
cabinet.reload
|
|
136
|
+
expect(cabinet.drawer.matcher).to eq('all')
|
|
137
|
+
expect(cabinet.drawer.items.map(&:name)).to eq([ 'a' ])
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
context 'when the nested documents are assigned via the parent' do
|
|
142
|
+
before do
|
|
143
|
+
cabinet.assign_attributes(drawer: { matcher: 'all', items: [ { name: 'a' } ] })
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'saves without conflicting $set paths' do
|
|
147
|
+
expect { cabinet.save! }.not_to raise_error
|
|
148
|
+
|
|
149
|
+
cabinet.reload
|
|
150
|
+
expect(cabinet.drawer.matcher).to eq('all')
|
|
151
|
+
expect(cabinet.drawer.items.map(&:name)).to eq([ 'a' ])
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context 'when an assigned child has its own embedded collection' do
|
|
156
|
+
let(:item) { ModifierModels::Item.new(name: 'a') }
|
|
157
|
+
|
|
158
|
+
before do
|
|
159
|
+
item.assign_attributes(widgets: [ { label: 'w' } ])
|
|
160
|
+
cabinet.build_drawer(matcher: 'all')
|
|
161
|
+
cabinet.drawer.assign_attributes(items: [ item ])
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'does not write the child collection to the root document' do
|
|
165
|
+
expect { cabinet.save! }.not_to raise_error
|
|
166
|
+
|
|
167
|
+
stored = ModifierModels::Cabinet.collection.find(_id: cabinet.id).first
|
|
168
|
+
expect(stored.keys).to contain_exactly('_id', 'drawer')
|
|
169
|
+
expect(stored['drawer']['items'].first['widgets'].first['label']).to eq('w')
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
117
174
|
end
|
|
@@ -246,4 +246,153 @@ describe 'Encryption' do
|
|
|
246
246
|
end
|
|
247
247
|
end
|
|
248
248
|
end
|
|
249
|
+
|
|
250
|
+
# The schema map has one entry per namespace, and it has to cover every model
|
|
251
|
+
# that writes there: models sharing a collection, subclasses, and subclasses
|
|
252
|
+
# of embedded models. Documents are read back through a client without
|
|
253
|
+
# automatic encryption, to see what is actually stored.
|
|
254
|
+
describe 'when more than one model writes to a namespace' do
|
|
255
|
+
let(:root_models) do
|
|
256
|
+
[ Crypt::Badge, Crypt::BadgeScan, Crypt::Member, Crypt::Listing, Crypt::Box, Crypt::Locker ]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
let(:keyed_models) do
|
|
260
|
+
[ Crypt::Badge, Crypt::BadgeScan, Crypt::Member, Crypt::PrivateListing, Crypt::Box, Crypt::SecretContent ]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
around do |example|
|
|
264
|
+
original_options = root_models.to_h { |model| [ model, model.storage_options ] }
|
|
265
|
+
existing_key_ids = keyed_models.to_h { |model| [ model, model.encrypt_metadata[:key_id] ] }
|
|
266
|
+
keyed_models.each { |model| model.set_key_id(data_key_id) }
|
|
267
|
+
root_models.each do |model|
|
|
268
|
+
unencrypted_client[model.collection_name.to_s].drop
|
|
269
|
+
model.store_in(client: :encrypted)
|
|
270
|
+
end
|
|
271
|
+
# The schema map is generated when the client is built, so the client has
|
|
272
|
+
# to be built after the key ids above are in place.
|
|
273
|
+
Mongoid::Clients.clear
|
|
274
|
+
|
|
275
|
+
example.run
|
|
276
|
+
|
|
277
|
+
original_options.each { |model, options| model.storage_options = options }
|
|
278
|
+
existing_key_ids.each { |model, key_id| model.set_key_id(key_id) }
|
|
279
|
+
Mongoid::Clients.clear
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def stored_document(model, id)
|
|
283
|
+
unencrypted_client[model.collection_name.to_s].find(_id: id).first
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def expect_ciphertext(value)
|
|
287
|
+
expect(value).to be_a(BSON::Binary).and(have_attributes(type: :ciphertext))
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
context 'when two models are stored in one collection' do
|
|
291
|
+
it 'stores the fields of both models encrypted', :aggregate_failures do
|
|
292
|
+
badge = Crypt::Badge.create!(serial: 'SERIAL-1')
|
|
293
|
+
scan = Crypt::BadgeScan.create!(holder: 'HOLDER-1')
|
|
294
|
+
|
|
295
|
+
expect_ciphertext(stored_document(Crypt::Badge, badge.id)['serial'])
|
|
296
|
+
expect_ciphertext(stored_document(Crypt::BadgeScan, scan.id)['holder'])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
context 'when a subclass adds an encrypted field to an encrypted parent' do
|
|
301
|
+
it 'stores the fields of the parent and of the subclass encrypted', :aggregate_failures do
|
|
302
|
+
employee = Crypt::Employee.create!(ssn: 'SSN-1', salary: 'SALARY-1')
|
|
303
|
+
|
|
304
|
+
stored_document(Crypt::Member, employee.id).tap do |doc|
|
|
305
|
+
expect_ciphertext(doc['ssn'])
|
|
306
|
+
expect_ciphertext(doc['salary'])
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it 'stores the fields of a sibling subclass encrypted' do
|
|
311
|
+
contractor = Crypt::Contractor.create!(ssn: 'SSN-2', rate: 'RATE-1')
|
|
312
|
+
|
|
313
|
+
expect_ciphertext(stored_document(Crypt::Member, contractor.id)['rate'])
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it 'reads the fields back' do
|
|
317
|
+
employee = Crypt::Employee.create!(ssn: 'SSN-3', salary: 'SALARY-3')
|
|
318
|
+
|
|
319
|
+
expect(Crypt::Member.find(employee.id).salary).to eq('SALARY-3')
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
context 'when only a subclass has encrypted fields' do
|
|
324
|
+
it 'stores the fields of the subclass encrypted' do
|
|
325
|
+
listing = Crypt::PrivateListing.create!(title: 'TITLE-1', owner_ssn: 'SSN-4')
|
|
326
|
+
|
|
327
|
+
expect_ciphertext(stored_document(Crypt::Listing, listing.id)['owner_ssn'])
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
context 'when a subclass of an embeds_one target adds an encrypted field' do
|
|
332
|
+
it 'stores the fields of the target and of the subclass encrypted', :aggregate_failures do
|
|
333
|
+
box = Crypt::Box.create!(item: Crypt::SealedItem.new(label: 'LABEL-1', seal: 'SEAL-1'))
|
|
334
|
+
|
|
335
|
+
stored_document(Crypt::Box, box.id)['item'].tap do |item|
|
|
336
|
+
expect_ciphertext(item['label'])
|
|
337
|
+
expect_ciphertext(item['seal'])
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
context 'when only a subclass of an embeds_one target has encrypted fields' do
|
|
343
|
+
it 'stores the fields of the subclass encrypted' do
|
|
344
|
+
locker = Crypt::Locker.create!(content: Crypt::SecretContent.new(label: 'LABEL-2', code: 'CODE-1'))
|
|
345
|
+
|
|
346
|
+
expect_ciphertext(stored_document(Crypt::Locker, locker.id)['content']['code'])
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# An entry for the namespace exists, but it does not cover every encrypted
|
|
351
|
+
# field of the model. The driver encrypts what the entry lists and writes
|
|
352
|
+
# the rest in plaintext.
|
|
353
|
+
context 'when the schema entry for the namespace misses an encrypted field' do
|
|
354
|
+
let(:config) do
|
|
355
|
+
super().merge(
|
|
356
|
+
partial: {
|
|
357
|
+
hosts: SpecConfig.instance.addresses,
|
|
358
|
+
database: database_id,
|
|
359
|
+
options: {
|
|
360
|
+
auto_encryption_options: {
|
|
361
|
+
key_vault_client: :key_vault,
|
|
362
|
+
kms_providers: kms_providers,
|
|
363
|
+
key_vault_namespace: key_vault_namespace,
|
|
364
|
+
extra_options: extra_options,
|
|
365
|
+
schema_map: partial_schema_map
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
let(:partial_schema_map) do
|
|
373
|
+
{
|
|
374
|
+
"#{database_id}.#{Crypt::Member.collection_name}" => {
|
|
375
|
+
'bsonType' => 'object',
|
|
376
|
+
'encryptMetadata' => {
|
|
377
|
+
'keyId' => [ BSON::Binary.new(Base64.decode64(data_key_id), :uuid) ],
|
|
378
|
+
'algorithm' => 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
|
|
379
|
+
},
|
|
380
|
+
'properties' => {
|
|
381
|
+
'ssn' => { 'encrypt' => { 'bsonType' => 'string' } }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it 'does not store the field in plaintext', :aggregate_failures do
|
|
388
|
+
expect do
|
|
389
|
+
Crypt::Employee.with(client: :partial) { |klass| klass.create!(ssn: 'SSN-5', salary: 'PARTIAL-1') }
|
|
390
|
+
end.to raise_error(Mongoid::Errors::NoEncryptionSchema)
|
|
391
|
+
|
|
392
|
+
expect(
|
|
393
|
+
unencrypted_client[Crypt::Member.collection_name.to_s].find(salary: 'PARTIAL-1').to_a
|
|
394
|
+
).to be_empty
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
249
398
|
end
|
|
@@ -354,6 +354,42 @@ describe Mongoid::Clients::Factory do
|
|
|
354
354
|
end
|
|
355
355
|
end
|
|
356
356
|
end
|
|
357
|
+
|
|
358
|
+
context 'when bypass_auto_encryption is set' do
|
|
359
|
+
it 'warns that encryption is bypassed' do
|
|
360
|
+
expect(Mongoid::Warnings).to receive(:warn_bypass_auto_encryption)
|
|
361
|
+
|
|
362
|
+
described_class.send(
|
|
363
|
+
:build_auto_encryption_options,
|
|
364
|
+
{ auto_encryption_options: { bypass_auto_encryption: true, schema_map: {} } },
|
|
365
|
+
database_id
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
context 'when bypass_auto_encryption is not set' do
|
|
371
|
+
it 'does not warn' do
|
|
372
|
+
expect(Mongoid::Warnings).not_to receive(:warn_bypass_auto_encryption)
|
|
373
|
+
|
|
374
|
+
described_class.send(
|
|
375
|
+
:build_auto_encryption_options,
|
|
376
|
+
{ auto_encryption_options: { schema_map: {} } },
|
|
377
|
+
database_id
|
|
378
|
+
)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
context 'when bypass_auto_encryption is false' do
|
|
383
|
+
it 'does not warn' do
|
|
384
|
+
expect(Mongoid::Warnings).not_to receive(:warn_bypass_auto_encryption)
|
|
385
|
+
|
|
386
|
+
described_class.send(
|
|
387
|
+
:build_auto_encryption_options,
|
|
388
|
+
{ auto_encryption_options: { bypass_auto_encryption: false, schema_map: {} } },
|
|
389
|
+
database_id
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
357
393
|
end
|
|
358
394
|
|
|
359
395
|
context "when no name is provided" do
|
|
@@ -376,5 +376,228 @@ describe Mongoid::Config::Encryption do
|
|
|
376
376
|
)
|
|
377
377
|
end
|
|
378
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
|
|
379
602
|
end
|
|
380
603
|
end
|
|
@@ -29,6 +29,14 @@ describe Mongoid::Encryptable do
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Any subclass of the declared class can be assigned to an embeds_one
|
|
33
|
+
# association, so the check has to look at the subclasses too.
|
|
34
|
+
context 'when only a subclass of an embedded model is encrypted' do
|
|
35
|
+
it 'returns true' do
|
|
36
|
+
expect(Crypt::Locker.requires_encryption_schema?).to be true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
32
40
|
context 'when the model declares no encryption anywhere' do
|
|
33
41
|
it 'returns false' do
|
|
34
42
|
expect(Person.requires_encryption_schema?).to be false
|
|
@@ -57,5 +65,51 @@ describe Mongoid::Encryptable do
|
|
|
57
65
|
expect(Crypt::Drawer.requires_encryption_schema?).to be false
|
|
58
66
|
end
|
|
59
67
|
end
|
|
68
|
+
|
|
69
|
+
context 'when every embedded association resolves' do
|
|
70
|
+
it 'memoizes a false answer' do
|
|
71
|
+
expect(Truck.requires_encryption_schema?).to be false
|
|
72
|
+
expect(Truck.instance_variable_defined?(:@requires_encryption_schema)).to be true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
context 'when an embedded association does not resolve' do
|
|
77
|
+
it 'does not memoize a false answer' do
|
|
78
|
+
expect(Crypt::Drawer.requires_encryption_schema?).to be false
|
|
79
|
+
expect(Crypt::Drawer.instance_variable_defined?(:@requires_encryption_schema)).to be false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context 'when an embedded encrypted class is defined after the first check' do
|
|
84
|
+
after do
|
|
85
|
+
if Crypt.const_defined?(:MissingNote, false)
|
|
86
|
+
Mongoid.deregister_model(Crypt::MissingNote)
|
|
87
|
+
Crypt.send(:remove_const, :MissingNote)
|
|
88
|
+
end
|
|
89
|
+
# While the class above existed, resolving it through the association
|
|
90
|
+
# memoized the class on the association, and a true answer on the
|
|
91
|
+
# model. Drop both, so the other examples see the model as it was
|
|
92
|
+
# before the class was defined.
|
|
93
|
+
relation = Crypt::Drawer.relations['missing_note']
|
|
94
|
+
relation.remove_instance_variable(:@klass) if relation.instance_variable_defined?(:@klass)
|
|
95
|
+
if Crypt::Drawer.instance_variable_defined?(:@requires_encryption_schema)
|
|
96
|
+
Crypt::Drawer.remove_instance_variable(:@requires_encryption_schema)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns true once the class is loaded' do
|
|
101
|
+
expect(Crypt::Drawer.requires_encryption_schema?).to be false
|
|
102
|
+
|
|
103
|
+
missing_note = Class.new do
|
|
104
|
+
include Mongoid::Document
|
|
105
|
+
|
|
106
|
+
embedded_in :drawer, class_name: 'Crypt::Drawer'
|
|
107
|
+
field :text, type: String, encrypt: true
|
|
108
|
+
end
|
|
109
|
+
Crypt.const_set(:MissingNote, missing_note)
|
|
110
|
+
|
|
111
|
+
expect(Crypt::Drawer.requires_encryption_schema?).to be true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
60
114
|
end
|
|
61
115
|
end
|
|
@@ -32,6 +32,12 @@ describe Mongoid::Matcher::RegexpBudget do
|
|
|
32
32
|
# exercising one has to hand .open something to bound.
|
|
33
33
|
let(:regexp_selector) { { 'name' => /\Aabc\z/ } }
|
|
34
34
|
|
|
35
|
+
describe 'REGEXP_BUDGET_KEY' do
|
|
36
|
+
it 'is namespaced so a foreign thread-local cannot collide with it' do
|
|
37
|
+
expect(Mongoid::Threaded::REGEXP_BUDGET_KEY).to start_with('[mongoid]:')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
35
41
|
describe '.open' do
|
|
36
42
|
context 'when no limit is configured' do
|
|
37
43
|
config_override :in_memory_regexp_time_limit, nil
|
|
@@ -62,7 +62,7 @@ class SearchIndexHelper
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
|
65
|
-
describe Mongoid::SearchIndexable do
|
|
65
|
+
describe Mongoid::SearchIndexable do # rubocop:disable RSpec/MultipleDescribes
|
|
66
66
|
before do
|
|
67
67
|
skip "#{described_class} requires at Atlas environment (set ATLAS_URI)" if ENV['ATLAS_URI'].nil?
|
|
68
68
|
end
|
|
@@ -145,3 +145,36 @@ describe Mongoid::SearchIndexable do
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
# rubocop:enable RSpec/MultipleMemoizedHelpers
|
|
148
|
+
|
|
149
|
+
# A unit test that does not need an Atlas connection.
|
|
150
|
+
describe Mongoid::SearchIndexable, '.wait_for_search_indexes' do
|
|
151
|
+
# Mongo::Collection#search_indexes was added in driver 2.19; the
|
|
152
|
+
# driver-min and driver-oldstable CI jobs pin the driver to 2.18-stable.
|
|
153
|
+
min_driver_version '2.19'
|
|
154
|
+
|
|
155
|
+
context 'when an index is ready' do
|
|
156
|
+
let(:model) do
|
|
157
|
+
Class.new do
|
|
158
|
+
include Mongoid::Document
|
|
159
|
+
|
|
160
|
+
store_in collection: BSON::ObjectId.new.to_s
|
|
161
|
+
search_index :idx, mappings: { dynamic: false }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'reads the index status inside collection management' do
|
|
166
|
+
managing = nil
|
|
167
|
+
search_indexes = [ { 'name' => 'idx', 'queryable' => true, 'status' => 'READY' } ]
|
|
168
|
+
collection = instance_double(Mongo::Collection)
|
|
169
|
+
allow(collection).to receive(:search_indexes) do
|
|
170
|
+
managing = Mongoid::Threaded.managing_collection?
|
|
171
|
+
search_indexes
|
|
172
|
+
end
|
|
173
|
+
allow(model).to receive(:collection).and_return(collection)
|
|
174
|
+
|
|
175
|
+
model.wait_for_search_indexes([ 'idx' ])
|
|
176
|
+
|
|
177
|
+
expect(managing).to be true
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -265,6 +265,19 @@ describe Mongoid::Tasks::Database do
|
|
|
265
265
|
described_class.create_search_indexes([searchable_model_spy], wait: false)
|
|
266
266
|
end
|
|
267
267
|
end
|
|
268
|
+
|
|
269
|
+
context 'when waiting for the indexes' do
|
|
270
|
+
it 'runs the wait inside collection management' do
|
|
271
|
+
managing = nil
|
|
272
|
+
allow(described_class).to receive(:wait_for_search_indexes) do
|
|
273
|
+
managing = Mongoid::Threaded.managing_collection?
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
described_class.create_search_indexes([ searchable_model_spy ], wait: true)
|
|
277
|
+
|
|
278
|
+
expect(managing).to be true
|
|
279
|
+
end
|
|
280
|
+
end
|
|
268
281
|
end
|
|
269
282
|
|
|
270
283
|
describe '.remove_search_indexes' do
|