diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index 4fc3250a83..77d9767a99 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -625,7 +625,11 @@ def initialize(addresses_or_uri, options = nil) build_encrypter end end - rescue StandardError + # ScriptError is rescued in addition to StandardError because + # Mongo::Crypt::Binding raises a LoadError when libmongocrypt cannot be + # found. Without it the cluster built above would be left open, leaking + # its monitoring and connection pool threads. + rescue StandardError, ScriptError begin @cluster.close rescue StandardError => e diff --git a/spec/mongo/client_construction_spec.rb b/spec/mongo/client_construction_spec.rb index fc88be7fcb..582eef5dbe 100644 --- a/spec/mongo/client_construction_spec.rb +++ b/spec/mongo/client_construction_spec.rb @@ -393,6 +393,32 @@ end end + # Mongo::Crypt::Binding raises a LoadError, which is a ScriptError and + # not a StandardError, when libmongocrypt is not available. The cluster + # is built before the encrypter, so it must be closed before the error + # propagates out of the constructor. This context deliberately sits + # outside the require_libmongocrypt guard above: the scenario it covers + # only happens when libmongocrypt is missing. + context 'when building the encrypter raises a LoadError' do + before do + allow(Mongo::Crypt::AutoEncrypter).to receive(:new).and_raise(LoadError, 'no libmongocrypt') + end + + it 'closes the cluster and propagates the error' do + expect_any_instance_of(Mongo::Cluster).to receive(:close).and_call_original + + expect do + new_local_client_nmio( + SpecConfig.instance.addresses, + auto_encryption_options: { + key_vault_namespace: 'keyvault.datakeys', + kms_providers: { local: { key: 'x' * 96 } }, + } + ) + end.to raise_error(LoadError, /no libmongocrypt/) + end + end + context 'timeout options' do let(:client) do new_local_client( diff --git a/spec/runners/crud/requirement.rb b/spec/runners/crud/requirement.rb index cb3ee755cb..4099312f38 100644 --- a/spec/runners/crud/requirement.rb +++ b/spec/runners/crud/requirement.rb @@ -114,7 +114,11 @@ def satisfied? ok &&= !SpecConfig.instance.auth? end if @csfle - ok &&= !!(ENV['LIBMONGOCRYPT_PATH'] || ENV['FLE']) + # Evergreen exports FLE="" on variants that do not exercise FLE, and + # an empty string is truthy in Ruby. Treat an empty value as + # "libmongocrypt is not available", the same way + # Mrss::LiteConstraints#require_libmongocrypt does. + ok &&= !(ENV['LIBMONGOCRYPT_PATH'] || '').empty? || !(ENV['FLE'] || '').empty? if ok && @csfle.is_a?(Hash) && (min_version = @csfle['minLibmongocryptVersion']) actual_version = Mongo::Crypt::Binding.mongocrypt_version(nil) ok &&= Mongo::Crypt::Binding.parse_version(actual_version) >= Gem::Version.new(min_version) diff --git a/spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml b/spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml new file mode 100644 index 0000000000..797904ee95 --- /dev/null +++ b/spec/spec_tests/data/unified/valid-pass/poc-queryable-encryption.yml @@ -0,0 +1,86 @@ +description: poc-queryable-encryption + +schemaVersion: "1.23" + +runOnRequirements: + - minServerVersion: "7.0" + csfle: true + # QE is not supported on standalone servers + topologies: [ replicaset, load-balanced, sharded ] + +createEntities: + - client: + id: &client0 client0 + autoEncryptOpts: + keyVaultNamespace: keyvault.datakeys + kmsProviders: + local: + key: Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk + - database: + id: &encryptedDB encryptedDB + client: *client0 + databaseName: &encryptedDBName poc-queryable-encryption + - collection: + id: &encryptedColl encryptedColl + database: *encryptedDB + collectionName: &encryptedCollName encrypted + - client: + id: &client1 client1 + - database: + id: &unencryptedDB unencryptedDB + client: *client1 + databaseName: *encryptedDBName + - collection: + id: &unencryptedColl unencryptedColl + database: *unencryptedDB + collectionName: *encryptedCollName + +initialData: + - databaseName: keyvault + collectionName: datakeys + documents: + - _id: &keyid { $binary: { base64: EjRWeBI0mHYSNBI0VniQEg==, subType: "04" } } + keyMaterial: { $binary: { base64: sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==, subType: "00" } } + creationDate: { $date: { $numberLong: "1641024000000" } } + updateDate: { $date: { $numberLong: "1641024000000" } } + status: 1 + masterKey: + provider: local + - databaseName: *encryptedDBName + collectionName: *encryptedCollName + documents: [] + createOptions: + encryptedFields: + fields: + - keyId: *keyid + path: 'encryptedInt' + bsonType: 'int' + queries: {'queryType': 'equality', 'contention': {'$numberLong': '0'}} + +tests: + - description: insert, replace, and find with queryable encryption + operations: + - object: *encryptedColl + name: insertOne + arguments: + document: + _id: 1 + encryptedInt: 11 + - object: *encryptedColl + name: replaceOne + arguments: + filter: { encryptedInt: 11 } + replacement: { encryptedInt: 22 } + - object: *encryptedColl + name: find + arguments: + filter: { encryptedInt: 22 } + expectResult: + - _id: 1 + encryptedInt: 22 + - object: *unencryptedColl + name: find + arguments: + filter: {} + expectResult: + - { _id: 1, encryptedInt: { $$type: binData }, __safeContent__: [ { "$binary" : { "base64" : "rhS16TJojgDDBtbluxBokvcotP1mQTGeYpNt8xd3MJQ=", "subType" : "00" } } ] } \ No newline at end of file