Skip to content

Commit

Permalink
feat(NODE-4202): add FLE 2 behavior for create/drop collection (#3218)
Browse files Browse the repository at this point in the history
  • Loading branch information
addaleax authored May 3, 2022
1 parent c54df3f commit 6d3947b
Show file tree
Hide file tree
Showing 8 changed files with 3,493 additions and 32 deletions.
5 changes: 4 additions & 1 deletion src/encrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export class Encrypter {
if (internalClient == null) {
const clonedOptions: MongoClientOptions = {};

for (const key of Object.keys(options)) {
for (const key of [
...Object.getOwnPropertyNames(options),
...Object.getOwnPropertySymbols(options)
] as string[]) {
if (['autoEncryption', 'minPoolSize', 'servers', 'caseTranslate', 'dbName'].includes(key))
continue;
Reflect.set(clonedOptions, key, Reflect.get(options, key));
Expand Down
91 changes: 71 additions & 20 deletions src/operations/create_collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import type { Callback } from '../utils';
import { CommandOperation, CommandOperationOptions } from './command';
import { CreateIndexOperation } from './indexes';
import { Aspect, defineAspects } from './operation';

const ILLEGAL_COMMAND_FIELDS = new Set([
Expand Down Expand Up @@ -75,6 +76,8 @@ export interface CreateCollectionOptions extends CommandOperationOptions {
timeseries?: TimeSeriesCollectionOptions;
/** The number of seconds after which a document in a timeseries collection expires. */
expireAfterSeconds?: number;
/** @experimental */
encryptedFields?: Document;
}

/** @internal */
Expand All @@ -96,31 +99,79 @@ export class CreateCollectionOperation extends CommandOperation<Collection> {
session: ClientSession | undefined,
callback: Callback<Collection>
): void {
const db = this.db;
const name = this.name;
const options = this.options;
(async () => {
const db = this.db;
const name = this.name;
const options = this.options;

const done: Callback = err => {
if (err) {
return callback(err);
const encryptedFields: Document | undefined =
options.encryptedFields ??
db.s.client.options.autoEncryption?.encryptedFieldsMap?.[`${db.databaseName}.${name}`];

if (encryptedFields) {
// Create auxilliary collections for FLE2 support.
const escCollection = encryptedFields.escCollection ?? `enxcol_.${name}.esc`;
const eccCollection = encryptedFields.eccCollection ?? `enxcol_.${name}.ecc`;
const ecocCollection = encryptedFields.ecocCollection ?? `enxcol_.${name}.ecoc`;

for (const collectionName of [escCollection, eccCollection, ecocCollection]) {
const createOp = new CreateCollectionOperation(db, collectionName);
await createOp.executeWithoutEncryptedFieldsCheck(server, session);
}

if (!options.encryptedFields) {
this.options = { ...this.options, encryptedFields };
}
}

const coll = await this.executeWithoutEncryptedFieldsCheck(server, session);

if (encryptedFields) {
// Create the required index for FLE2 support.
const createIndexOp = new CreateIndexOperation(db, name, { __safeContent__: 1 }, {});
await new Promise<void>((resolve, reject) => {
createIndexOp.execute(server, session, err => (err ? reject(err) : resolve()));
});
}

callback(undefined, new Collection(db, name, options));
};

const cmd: Document = { create: name };
for (const n in options) {
if (
(options as any)[n] != null &&
typeof (options as any)[n] !== 'function' &&
!ILLEGAL_COMMAND_FIELDS.has(n)
) {
cmd[n] = (options as any)[n];
return coll;
})().then(
coll => callback(undefined, coll),
err => callback(err)
);
}

private executeWithoutEncryptedFieldsCheck(
server: Server,
session: ClientSession | undefined
): Promise<Collection> {
return new Promise<Collection>((resolve, reject) => {
const db = this.db;
const name = this.name;
const options = this.options;

const done: Callback = err => {
if (err) {
return reject(err);
}

resolve(new Collection(db, name, options));
};

const cmd: Document = { create: name };
for (const n in options) {
if (
(options as any)[n] != null &&
typeof (options as any)[n] !== 'function' &&
!ILLEGAL_COMMAND_FIELDS.has(n)
) {
cmd[n] = (options as any)[n];
}
}
}

// otherwise just execute the command
super.executeCommand(server, session, cmd, done);
// otherwise just execute the command
super.executeCommand(server, session, cmd, done);
});
}
}

Expand Down
94 changes: 88 additions & 6 deletions src/operations/drop.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import type { Document } from '../bson';
import type { Db } from '../db';
import { MONGODB_ERROR_CODES, MongoServerError } from '../error';
import type { Server } from '../sdam/server';
import type { ClientSession } from '../sessions';
import type { Callback } from '../utils';
import { CommandOperation, CommandOperationOptions } from './command';
import { Aspect, defineAspects } from './operation';

/** @public */
export type DropCollectionOptions = CommandOperationOptions;
export interface DropCollectionOptions extends CommandOperationOptions {
/** @experimental */
encryptedFields?: Document;
}

/** @internal */
export class DropCollectionOperation extends CommandOperation<boolean> {
override options: DropCollectionOptions;
db: Db;
name: string;

constructor(db: Db, name: string, options: DropCollectionOptions) {
constructor(db: Db, name: string, options: DropCollectionOptions = {}) {
super(db, options);
this.db = db;
this.options = options;
this.name = name;
}
Expand All @@ -24,10 +31,85 @@ export class DropCollectionOperation extends CommandOperation<boolean> {
session: ClientSession | undefined,
callback: Callback<boolean>
): void {
super.executeCommand(server, session, { drop: this.name }, (err, result) => {
if (err) return callback(err);
if (result.ok) return callback(undefined, true);
callback(undefined, false);
(async () => {
const db = this.db;
const options = this.options;
const name = this.name;

const encryptedFieldsMap = db.s.client.options.autoEncryption?.encryptedFieldsMap;
let encryptedFields: Document | undefined =
options.encryptedFields ?? encryptedFieldsMap?.[`${db.databaseName}.${name}`];

if (!encryptedFields && encryptedFieldsMap) {
// If the MongoClient was configued with an encryptedFieldsMap,
// and no encryptedFields config was available in it or explicitly
// passed as an argument, the spec tells us to look one up using
// listCollections().
const listCollectionsResult = await db
.listCollections({ name }, { nameOnly: false })
.toArray();
encryptedFields = listCollectionsResult?.[0]?.options?.encryptedFields;
}

let result;
let errorForMainOperation;
try {
result = await this.executeWithoutEncryptedFieldsCheck(server, session);
} catch (err) {
if (
!encryptedFields ||
!(err instanceof MongoServerError) ||
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
) {
throw err;
}
// Save a possible NamespaceNotFound error for later
// in the encryptedFields case, so that the auxilliary
// collections will still be dropped.
errorForMainOperation = err;
}

if (encryptedFields) {
const escCollection = encryptedFields.escCollection || `enxcol_.${name}.esc`;
const eccCollection = encryptedFields.eccCollection || `enxcol_.${name}.ecc`;
const ecocCollection = encryptedFields.ecocCollection || `enxcol_.${name}.ecoc`;

for (const collectionName of [escCollection, eccCollection, ecocCollection]) {
// Drop auxilliary collections, ignoring potential NamespaceNotFound errors.
const dropOp = new DropCollectionOperation(db, collectionName);
try {
await dropOp.executeWithoutEncryptedFieldsCheck(server, session);
} catch (err) {
if (
!(err instanceof MongoServerError) ||
err.code !== MONGODB_ERROR_CODES.NamespaceNotFound
) {
throw err;
}
}
}

if (errorForMainOperation) {
throw errorForMainOperation;
}
}

return result;
})().then(
result => callback(undefined, result),
err => callback(err)
);
}

private executeWithoutEncryptedFieldsCheck(
server: Server,
session: ClientSession | undefined
): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
super.executeCommand(server, session, { drop: this.name }, (err, result) => {
if (err) return reject(err);
resolve(!!result.ok);
});
});
}
}
Expand Down
Loading

0 comments on commit 6d3947b

Please sign in to comment.