diff --git a/packages/cli-repl/test/e2e-fle.spec.ts b/packages/cli-repl/test/e2e-fle.spec.ts index 2b1a53d860..bc0a425bdb 100644 --- a/packages/cli-repl/test/e2e-fle.spec.ts +++ b/packages/cli-repl/test/e2e-fle.spec.ts @@ -10,7 +10,7 @@ import { inspect } from 'util'; import path from 'path'; describe('FLE tests', () => { - const testServer = startTestServer('shared'); + const testServer = startTestServer('not-shared', '--replicaset', '--nodes', '1'); skipIfServerVersion(testServer, '< 4.2'); // FLE only available on 4.2+ skipIfCommunityServer(testServer); // FLE is enterprise-only useBinaryPath(testServer); // Get mongocryptd in the PATH for this test @@ -204,10 +204,9 @@ describe('FLE tests', () => { await shell.executeLine(`autoMongo = Mongo(${uri}, { \ keyVaultNamespace: '${dbname}.keyVault', \ kmsProviders: { local }, \ - schemaMap: schemaMap \ + schemaMap \ });`); - await shell.executeLine(`bypassMongo = Mongo(${uri}, { \ keyVaultNamespace: '${dbname}.keyVault', \ kmsProviders: { local }, \ @@ -237,6 +236,77 @@ describe('FLE tests', () => { expect(plainMongoResult).to.not.include("phoneNumber: '+12874627836445'"); }); + context('6.0+', () => { + skipIfServerVersion(testServer, '< 6.0'); // FLE2 only available on 6.0+ + + it('drops fle2 collection with all helper collections when encryptedFields options are in listCollections', async() => { + const shell = TestShell.start({ + args: ['--nodb'], + env: { + ...process.env, + MONGOSH_FLE2_SUPPORT: 'true' + }, + }); + const uri = JSON.stringify(await testServer.connectionString()); + + await shell.waitForPrompt(); + + await shell.executeLine('local = { key: BinData(0, "kh4Gv2N8qopZQMQYMEtww/AkPsIrXNmEMxTrs3tUoTQZbZu4msdRUaR8U5fXD7A7QXYHcEvuu4WctJLoT+NvvV3eeIg3MD+K8H9SR794m/safgRHdIfy6PD+rFpvmFbY") }'); + + await shell.executeLine(`keyMongo = Mongo(${uri}, { \ + keyVaultNamespace: '${dbname}.keyVault', \ + kmsProviders: { local } \ + });`); + + await shell.executeLine('keyVault = keyMongo.getKeyVault();'); + await shell.executeLine('keyId = keyVault.createKey("local");'); + + await shell.executeLine(`encryptedFieldsMap = { \ + '${dbname}.collfle2': { \ + fields: [{ path: 'phoneNumber', keyId, bsonType: 'string' }] \ + } \ + };`); + + await shell.executeLine(`autoMongo = Mongo(${uri}, { \ + keyVaultNamespace: '${dbname}.keyVault', \ + kmsProviders: { local }, \ + encryptedFieldsMap \ + });`); + + // Drivers will create the auxilliary FLE2 collections only when explicitly creating collections + // via the createCollection() command. + await shell.executeLine(`autoMongo.getDB('${dbname}').createCollection('collfle2');`); + await shell.executeLine(`autoMongo.getDB('${dbname}').collfle2.insertOne({ \ + phoneNumber: '+12874627836445' \ + });`); + + const autoMongoResult = await shell.executeLine(`autoMongo.getDB('${dbname}').collfle2.find()`); + expect(autoMongoResult).to.include("phoneNumber: '+12874627836445'"); + + await shell.executeLine(`plainMongo = Mongo(${uri});`); + + const plainMongoResult = await shell.executeLine(`plainMongo.getDB('${dbname}').collfle2.find()`); + expect(plainMongoResult).to.include('phoneNumber: Binary(Buffer.from'); + expect(plainMongoResult).to.not.include("phoneNumber: '+12874627836445'"); + + let collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`); + + expect(collections).to.include('enxcol_.collfle2.ecc'); + expect(collections).to.include('enxcol_.collfle2.esc'); + expect(collections).to.include('enxcol_.collfle2.ecoc'); + expect(collections).to.include('collfle2'); + + await shell.executeLine(`plainMongo.getDB('${dbname}').collfle2.drop();`); + + collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`); + + expect(collections).to.not.include('enxcol_.collfle2.ecc'); + expect(collections).to.not.include('enxcol_.collfle2.esc'); + expect(collections).to.not.include('enxcol_.collfle2.ecoc'); + expect(collections).to.not.include('collfle2'); + }); + }); + it('performs KeyVault data key management as expected', async() => { const shell = TestShell.start({ args: [await testServer.connectionString()] diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index c1041d00a8..dd61db8f6d 100644 --- a/packages/shell-api/src/collection.spec.ts +++ b/packages/shell-api/src/collection.spec.ts @@ -1243,6 +1243,7 @@ describe('Collection', () => { }); it('passes through options', async() => { + serviceProvider.listCollections.resolves([{}]); serviceProvider.dropCollection.resolves(); await collection.drop({ promoteValues: false }); expect(serviceProvider.dropCollection).to.have.been.calledWith( @@ -1873,6 +1874,72 @@ describe('Collection', () => { }); }); }); + describe('fle2', () => { + let mongo1: Mongo; + let mongo2: Mongo; + let serviceProvider: StubbedInstance; + let database: Database; + let bus: StubbedInstance; + let instanceState: ShellInstanceState; + let collection: Collection; + let keyId: any[] +; + beforeEach(() => { + bus = stubInterface(); + serviceProvider = stubInterface(); + serviceProvider.runCommand.resolves({ ok: 1 }); + serviceProvider.runCommandWithCheck.resolves({ ok: 1 }); + serviceProvider.initialDb = 'test'; + serviceProvider.bsonLibrary = bson; + instanceState = new ShellInstanceState(serviceProvider, bus); + keyId = [ { $binary: { base64: 'oh3caogGQ4Sf34ugKnZ7Xw==', subType: '04' } } ]; + mongo1 = new Mongo( + instanceState, + undefined, + { + keyVaultNamespace: 'db1.keyvault', + kmsProviders: { local: { key: 'A'.repeat(128) } }, + encryptedFieldsMap: { + 'db1.collfle2': { + fields: [{ path: 'phoneNumber', keyId, bsonType: 'string' }], + } + } + }, + undefined, + serviceProvider + ); + database = new Database(mongo1, 'db1'); + collection = new Collection(mongo1, database, 'collfle2'); + mongo2 = new Mongo( + instanceState, + undefined, + undefined, + undefined, + serviceProvider + ); + }); + + describe('drop', () => { + it('does not pass encryptedFields through options when collection is in encryptedFieldsMap', async() => { + serviceProvider.dropCollection.resolves(); + await collection.drop(); + expect(serviceProvider.dropCollection).to.have.been.calledWith( + 'db1', 'collfle2', {} + ); + }); + + it('passes encryptedFields through options when collection is not in encryptedFieldsMap', async() => { + serviceProvider.listCollections.resolves([{ + options: { encryptedFields: { fields: [ { path: 'phoneNumber', keyId, bsonType: 'string' } ] } } + }]); + serviceProvider.dropCollection.resolves(); + await mongo2.getDB('db1').getCollection('collfle2').drop(); + expect(serviceProvider.dropCollection).to.have.been.calledWith( + 'db1', 'collfle2', { encryptedFields: { fields: [ { path: 'phoneNumber', keyId, bsonType: 'string' } ] } } + ); + }); + }); + }); describe('with session', () => { let serviceProvider: StubbedInstance; let collection: Collection; diff --git a/packages/shell-api/src/collection.ts b/packages/shell-api/src/collection.ts index 9853b32877..c31ced9097 100644 --- a/packages/shell-api/src/collection.ts +++ b/packages/shell-api/src/collection.ts @@ -1340,11 +1340,34 @@ export default class Collection extends ShellApiWithMongoClass { async drop(options: DropCollectionOptions = {}): Promise { this._emitCollectionApiCall('drop'); + let encryptedFieldsOptions = {}; + + // @ts-expect-error waiting for driver release + const encryptedFieldsMap = this._mongo._fleOptions?.encryptedFieldsMap; + const encryptedFields: Document | undefined = encryptedFieldsMap?.[`${this._database._name}.${ this._name}`]; + + // @ts-expect-error waiting for driver release + if (!encryptedFields && !options.encryptedFields) { + const collectionInfos = await this._mongo._serviceProvider.listCollections( + this._database._name, + { + name: this._name + }, + await this._database._baseOptions() + ); + + const encryptedFields: Document | undefined = collectionInfos?.[0]?.options?.encryptedFields; + + if (encryptedFields) { + encryptedFieldsOptions = { encryptedFields }; + } + } + try { return await this._mongo._serviceProvider.dropCollection( this._database._name, this._name, - { ...await this._database._baseOptions(), ...options } + { ...await this._database._baseOptions(), ...options, ...encryptedFieldsOptions } ); } catch (error: any) { if (error?.codeName === 'NamespaceNotFound') {