Skip to content

Commit b357c0b

Browse files
feat: drop collection using encrypted fields from collection list MONGOSH-1208 (#1277)
* feat(shell-api): ensure collection.drop() always drops FLE2 collections MONGOSH-1208 * test: add e2e tests for fle2 * fix: ignore ts error for CommandOperationOptions * test: update e2e test to check dropping collection functionality * test: update name * test: check that fle2 data encrypted on plain mongo
1 parent e5f0541 commit b357c0b

File tree

3 files changed

+164
-4
lines changed

3 files changed

+164
-4
lines changed

packages/cli-repl/test/e2e-fle.spec.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { inspect } from 'util';
1010
import path from 'path';
1111

1212
describe('FLE tests', () => {
13-
const testServer = startTestServer('shared');
13+
const testServer = startTestServer('not-shared', '--replicaset', '--nodes', '1');
1414
skipIfServerVersion(testServer, '< 4.2'); // FLE only available on 4.2+
1515
skipIfCommunityServer(testServer); // FLE is enterprise-only
1616
useBinaryPath(testServer); // Get mongocryptd in the PATH for this test
@@ -204,10 +204,9 @@ describe('FLE tests', () => {
204204
await shell.executeLine(`autoMongo = Mongo(${uri}, { \
205205
keyVaultNamespace: '${dbname}.keyVault', \
206206
kmsProviders: { local }, \
207-
schemaMap: schemaMap \
207+
schemaMap \
208208
});`);
209209

210-
211210
await shell.executeLine(`bypassMongo = Mongo(${uri}, { \
212211
keyVaultNamespace: '${dbname}.keyVault', \
213212
kmsProviders: { local }, \
@@ -237,6 +236,77 @@ describe('FLE tests', () => {
237236
expect(plainMongoResult).to.not.include("phoneNumber: '+12874627836445'");
238237
});
239238

239+
context('6.0+', () => {
240+
skipIfServerVersion(testServer, '< 6.0'); // FLE2 only available on 6.0+
241+
242+
it('drops fle2 collection with all helper collections when encryptedFields options are in listCollections', async() => {
243+
const shell = TestShell.start({
244+
args: ['--nodb'],
245+
env: {
246+
...process.env,
247+
MONGOSH_FLE2_SUPPORT: 'true'
248+
},
249+
});
250+
const uri = JSON.stringify(await testServer.connectionString());
251+
252+
await shell.waitForPrompt();
253+
254+
await shell.executeLine('local = { key: BinData(0, "kh4Gv2N8qopZQMQYMEtww/AkPsIrXNmEMxTrs3tUoTQZbZu4msdRUaR8U5fXD7A7QXYHcEvuu4WctJLoT+NvvV3eeIg3MD+K8H9SR794m/safgRHdIfy6PD+rFpvmFbY") }');
255+
256+
await shell.executeLine(`keyMongo = Mongo(${uri}, { \
257+
keyVaultNamespace: '${dbname}.keyVault', \
258+
kmsProviders: { local } \
259+
});`);
260+
261+
await shell.executeLine('keyVault = keyMongo.getKeyVault();');
262+
await shell.executeLine('keyId = keyVault.createKey("local");');
263+
264+
await shell.executeLine(`encryptedFieldsMap = { \
265+
'${dbname}.collfle2': { \
266+
fields: [{ path: 'phoneNumber', keyId, bsonType: 'string' }] \
267+
} \
268+
};`);
269+
270+
await shell.executeLine(`autoMongo = Mongo(${uri}, { \
271+
keyVaultNamespace: '${dbname}.keyVault', \
272+
kmsProviders: { local }, \
273+
encryptedFieldsMap \
274+
});`);
275+
276+
// Drivers will create the auxilliary FLE2 collections only when explicitly creating collections
277+
// via the createCollection() command.
278+
await shell.executeLine(`autoMongo.getDB('${dbname}').createCollection('collfle2');`);
279+
await shell.executeLine(`autoMongo.getDB('${dbname}').collfle2.insertOne({ \
280+
phoneNumber: '+12874627836445' \
281+
});`);
282+
283+
const autoMongoResult = await shell.executeLine(`autoMongo.getDB('${dbname}').collfle2.find()`);
284+
expect(autoMongoResult).to.include("phoneNumber: '+12874627836445'");
285+
286+
await shell.executeLine(`plainMongo = Mongo(${uri});`);
287+
288+
const plainMongoResult = await shell.executeLine(`plainMongo.getDB('${dbname}').collfle2.find()`);
289+
expect(plainMongoResult).to.include('phoneNumber: Binary(Buffer.from');
290+
expect(plainMongoResult).to.not.include("phoneNumber: '+12874627836445'");
291+
292+
let collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`);
293+
294+
expect(collections).to.include('enxcol_.collfle2.ecc');
295+
expect(collections).to.include('enxcol_.collfle2.esc');
296+
expect(collections).to.include('enxcol_.collfle2.ecoc');
297+
expect(collections).to.include('collfle2');
298+
299+
await shell.executeLine(`plainMongo.getDB('${dbname}').collfle2.drop();`);
300+
301+
collections = await shell.executeLine(`plainMongo.getDB('${dbname}').getCollectionNames()`);
302+
303+
expect(collections).to.not.include('enxcol_.collfle2.ecc');
304+
expect(collections).to.not.include('enxcol_.collfle2.esc');
305+
expect(collections).to.not.include('enxcol_.collfle2.ecoc');
306+
expect(collections).to.not.include('collfle2');
307+
});
308+
});
309+
240310
it('performs KeyVault data key management as expected', async() => {
241311
const shell = TestShell.start({
242312
args: [await testServer.connectionString()]

packages/shell-api/src/collection.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,7 @@ describe('Collection', () => {
12431243
});
12441244

12451245
it('passes through options', async() => {
1246+
serviceProvider.listCollections.resolves([{}]);
12461247
serviceProvider.dropCollection.resolves();
12471248
await collection.drop({ promoteValues: false });
12481249
expect(serviceProvider.dropCollection).to.have.been.calledWith(
@@ -1873,6 +1874,72 @@ describe('Collection', () => {
18731874
});
18741875
});
18751876
});
1877+
describe('fle2', () => {
1878+
let mongo1: Mongo;
1879+
let mongo2: Mongo;
1880+
let serviceProvider: StubbedInstance<ServiceProvider>;
1881+
let database: Database;
1882+
let bus: StubbedInstance<EventEmitter>;
1883+
let instanceState: ShellInstanceState;
1884+
let collection: Collection;
1885+
let keyId: any[]
1886+
;
1887+
beforeEach(() => {
1888+
bus = stubInterface<EventEmitter>();
1889+
serviceProvider = stubInterface<ServiceProvider>();
1890+
serviceProvider.runCommand.resolves({ ok: 1 });
1891+
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
1892+
serviceProvider.initialDb = 'test';
1893+
serviceProvider.bsonLibrary = bson;
1894+
instanceState = new ShellInstanceState(serviceProvider, bus);
1895+
keyId = [ { $binary: { base64: 'oh3caogGQ4Sf34ugKnZ7Xw==', subType: '04' } } ];
1896+
mongo1 = new Mongo(
1897+
instanceState,
1898+
undefined,
1899+
{
1900+
keyVaultNamespace: 'db1.keyvault',
1901+
kmsProviders: { local: { key: 'A'.repeat(128) } },
1902+
encryptedFieldsMap: {
1903+
'db1.collfle2': {
1904+
fields: [{ path: 'phoneNumber', keyId, bsonType: 'string' }],
1905+
}
1906+
}
1907+
},
1908+
undefined,
1909+
serviceProvider
1910+
);
1911+
database = new Database(mongo1, 'db1');
1912+
collection = new Collection(mongo1, database, 'collfle2');
1913+
mongo2 = new Mongo(
1914+
instanceState,
1915+
undefined,
1916+
undefined,
1917+
undefined,
1918+
serviceProvider
1919+
);
1920+
});
1921+
1922+
describe('drop', () => {
1923+
it('does not pass encryptedFields through options when collection is in encryptedFieldsMap', async() => {
1924+
serviceProvider.dropCollection.resolves();
1925+
await collection.drop();
1926+
expect(serviceProvider.dropCollection).to.have.been.calledWith(
1927+
'db1', 'collfle2', {}
1928+
);
1929+
});
1930+
1931+
it('passes encryptedFields through options when collection is not in encryptedFieldsMap', async() => {
1932+
serviceProvider.listCollections.resolves([{
1933+
options: { encryptedFields: { fields: [ { path: 'phoneNumber', keyId, bsonType: 'string' } ] } }
1934+
}]);
1935+
serviceProvider.dropCollection.resolves();
1936+
await mongo2.getDB('db1').getCollection('collfle2').drop();
1937+
expect(serviceProvider.dropCollection).to.have.been.calledWith(
1938+
'db1', 'collfle2', { encryptedFields: { fields: [ { path: 'phoneNumber', keyId, bsonType: 'string' } ] } }
1939+
);
1940+
});
1941+
});
1942+
});
18761943
describe('with session', () => {
18771944
let serviceProvider: StubbedInstance<ServiceProvider>;
18781945
let collection: Collection;

packages/shell-api/src/collection.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1340,11 +1340,34 @@ export default class Collection extends ShellApiWithMongoClass {
13401340
async drop(options: DropCollectionOptions = {}): Promise<boolean> {
13411341
this._emitCollectionApiCall('drop');
13421342

1343+
let encryptedFieldsOptions = {};
1344+
1345+
// @ts-expect-error waiting for driver release
1346+
const encryptedFieldsMap = this._mongo._fleOptions?.encryptedFieldsMap;
1347+
const encryptedFields: Document | undefined = encryptedFieldsMap?.[`${this._database._name}.${ this._name}`];
1348+
1349+
// @ts-expect-error waiting for driver release
1350+
if (!encryptedFields && !options.encryptedFields) {
1351+
const collectionInfos = await this._mongo._serviceProvider.listCollections(
1352+
this._database._name,
1353+
{
1354+
name: this._name
1355+
},
1356+
await this._database._baseOptions()
1357+
);
1358+
1359+
const encryptedFields: Document | undefined = collectionInfos?.[0]?.options?.encryptedFields;
1360+
1361+
if (encryptedFields) {
1362+
encryptedFieldsOptions = { encryptedFields };
1363+
}
1364+
}
1365+
13431366
try {
13441367
return await this._mongo._serviceProvider.dropCollection(
13451368
this._database._name,
13461369
this._name,
1347-
{ ...await this._database._baseOptions(), ...options }
1370+
{ ...await this._database._baseOptions(), ...options, ...encryptedFieldsOptions }
13481371
);
13491372
} catch (error: any) {
13501373
if (error?.codeName === 'NamespaceNotFound') {

0 commit comments

Comments
 (0)