From 8382916fffc8348c98deb40b483804139faebb8f Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Thu, 30 Jul 2026 13:37:50 +0200 Subject: [PATCH] Consult server-side encryptedFields when dropping QE collections Dropping a queryable encryption collection through a client that has auto encryption configured without an encryptedFieldsMap left the enxcol_..esc/.ecoc metadata collections behind: the listCollections fallback ran only when the map was present. Per the client-side-encryption spec (GetEncryptedFields with askDb=true) and the reference implementation, the server lookup must happen whenever auto encryption is configured. Also add the spec-mandated documentation note that supplying an encrypted_fields_map protects against a malicious server advertising false encryptedFields. --- lib/mongo/client.rb | 4 + lib/mongo/collection/queryable_encryption.rb | 13 ++-- .../qe_drop_collection_spec.rb | 76 +++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 spec/integration/client_side_encryption/qe_drop_collection_spec.rb diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index 4fc3250a83..893e3a8b5d 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -513,6 +513,10 @@ def hash # (this part of the API is subject to change). # - :encrypted_fields_map => Hash | nil, maps a collection namespace to # a hash describing encrypted fields for queryable encryption. + # - Note: Supplying an encrypted_fields_map provides more security + # than relying on an encryptedFields obtained from the server. It + # protects against a malicious server advertising a false + # encryptedFields. # - Note: If a collection is present on both the encryptedFieldsMap # and schemaMap, an error will be raised. # - :bypass_query_analysis => Boolean | nil, when true disables automatic diff --git a/lib/mongo/collection/queryable_encryption.rb b/lib/mongo/collection/queryable_encryption.rb index 8beb17c45f..e911463c4b 100644 --- a/lib/mongo/collection/queryable_encryption.rb +++ b/lib/mongo/collection/queryable_encryption.rb @@ -62,8 +62,8 @@ def maybe_create_qe_collections(encrypted_fields, client, session) def maybe_drop_emm_collections(encrypted_fields, client, session) encrypted_fields = if encrypted_fields encrypted_fields - elsif encrypted_fields_map - encrypted_fields_for_drop_from_map + elsif client.options[:auto_encryption_options] + encrypted_fields_for_drop else {} end @@ -127,12 +127,13 @@ def encrypted_fields_from(fields) {} end - # Tries to return the encrypted fields from the {{encrypted_fields_map}} - # value, for the current namespace. + # Tries to return the encrypted fields for the current namespace from + # the {{encrypted_fields_map}} value, falling back to the encrypted + # fields advertised by the server for the collection. # # @return [ Hash | nil ] the encrypted fields, if found - def encrypted_fields_for_drop_from_map - encrypted_fields_map[namespace] || + def encrypted_fields_for_drop + (encrypted_fields_map && encrypted_fields_map[namespace]) || database.list_collections(filter: { name: name }) .first &.fetch(:options, {}) diff --git a/spec/integration/client_side_encryption/qe_drop_collection_spec.rb b/spec/integration/client_side_encryption/qe_drop_collection_spec.rb new file mode 100644 index 0000000000..1544945553 --- /dev/null +++ b/spec/integration/client_side_encryption/qe_drop_collection_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Queryable encryption drop collection' do + require_libmongocrypt + min_server_version '7.0.0-rc0' + require_topology :replica_set, :sharded, :load_balanced + + include_context 'define shared FLE helpers' + + let(:encrypted_coll) { 'qe_drop_lookup' } + + # The shared fixture hardcodes metadata collection names for a collection + # named "default"; remove them so the default enxcol_..esc/.ecoc + # names for the collection under test apply. + let(:encrypted_fields) do + BSON::ExtJSON.parse( + File.read('spec/support/crypt/encrypted_fields/encryptedFields.json') + ).tap do |fields| + fields.delete('escCollection') + fields.delete('ecocCollection') + end + end + + let(:auto_encryption_options) do + { + key_vault_namespace: key_vault_namespace, + kms_providers: local_kms_providers, + bypass_query_analysis: true + } + end + + let(:encrypted_client) do + ClientRegistry.instance.new_local_client( + SpecConfig.instance.addresses, + auto_encryption_options: auto_encryption_options, + database: SpecConfig.instance.test_db + ) + end + + before do + authorized_client[encrypted_coll].drop(encrypted_fields: encrypted_fields) + authorized_client[encrypted_coll].create(encrypted_fields: encrypted_fields) + end + + after do + authorized_client[encrypted_coll].drop(encrypted_fields: encrypted_fields) + end + + shared_examples 'drops the metadata collections' do + it 'looks up encryptedFields on the server and drops the metadata collections' do + expect(authorized_client.database.collection_names) + .to include("enxcol_.#{encrypted_coll}.esc", "enxcol_.#{encrypted_coll}.ecoc") + + encrypted_client[encrypted_coll].drop + + collection_names = authorized_client.database.collection_names + expect(collection_names).not_to include("enxcol_.#{encrypted_coll}.esc") + expect(collection_names).not_to include("enxcol_.#{encrypted_coll}.ecoc") + expect(collection_names).not_to include(encrypted_coll) + end + end + + context 'when auto encryption is configured without encrypted_fields_map' do + include_examples 'drops the metadata collections' + end + + context 'when encrypted_fields_map has no entry for the collection' do + let(:auto_encryption_options) do + super().merge(encrypted_fields_map: {}) + end + + include_examples 'drops the metadata collections' + end +end