Skip to content

Commit

Permalink
fix(NODE-2995): Add shared metadata MongoClient
Browse files Browse the repository at this point in the history
Automatic client side encryption needs to perform metadata look ups
like listCollections. In situations where the connection pool size
is constrained or in full use it can be impossible for an operation
to proceed. Adding a separate client in these situations permits the
metadata look ups to proceed unblocking operations.
  • Loading branch information
nbbeeken committed Mar 19, 2021
1 parent b94519b commit 3e098ea
Show file tree
Hide file tree
Showing 55 changed files with 716 additions and 747 deletions.
2 changes: 1 addition & 1 deletion .evergreen/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ if [[ -z "${CLIENT_ENCRYPTION}" ]]; then
unset AWS_ACCESS_KEY_ID;
unset AWS_SECRET_ACCESS_KEY;
else
npm install mongodb-client-encryption@1.1.1-beta.0
npm install mongodb-client-encryption@latest
fi

MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} npm run ${TEST_NPM_SCRIPT}
163 changes: 163 additions & 0 deletions lib/encrypter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
'use strict';
const MongoClient = require('./mongo_client');
const BSON = require('./core/connection/utils').retrieveBSON();
const MongoError = require('./core/error').MongoError;

try {
require.resolve('mongodb-client-encryption');
} catch (err) {
throw new MongoError(
'Auto-encryption requested, but the module is not installed. ' +
'Please add `mongodb-client-encryption` as a dependency of your project'
);
}

const mongodbClientEncryption = require('mongodb-client-encryption');
if (typeof mongodbClientEncryption.extension !== 'function') {
throw new MongoError(
'loaded version of `mongodb-client-encryption` does not have property `extension`. ' +
'Please make sure you are loading the correct version of `mongodb-client-encryption`'
);
}
const AutoEncrypter = mongodbClientEncryption.extension(require('../index')).AutoEncrypter;

const kInternalClient = Symbol('internalClient');

class Encrypter {
/**
* @param {MongoClient} client
* @param {{autoEncryption: import('./mongo_client').AutoEncryptionOptions, bson: object}} options
*/
constructor(client, options) {
this.bypassAutoEncryption = !!options.autoEncryption.bypassAutoEncryption;
this.needsConnecting = false;

if (options.maxPoolSize === 0 && options.autoEncryption.keyVaultClient == null) {
options.autoEncryption.keyVaultClient = client;
} else if (options.autoEncryption.keyVaultClient == null) {
options.autoEncryption.keyVaultClient = this.getInternalClient(client);
}

if (this.bypassAutoEncryption) {
options.autoEncryption.metadataClient = undefined;
} else if (options.maxPoolSize === 0) {
options.autoEncryption.metadataClient = client;
} else {
options.autoEncryption.metadataClient = this.getInternalClient(client);
}

options.autoEncryption.bson = Encrypter.makeBSON(options);

this.autoEncrypter = new AutoEncrypter(client, options.autoEncryption);
}

getInternalClient(client) {
if (!this[kInternalClient]) {
const clonedOptions = {};

for (const key of Object.keys(client.s.options)) {
if (
['autoEncryption', 'minPoolSize', 'servers', 'caseTranslate', 'dbName'].indexOf(key) !==
-1
)
continue;
clonedOptions[key] = client.s.options[key];
}

clonedOptions.minPoolSize = 0;

const allEvents = [
// APM
'commandStarted',
'commandSucceeded',
'commandFailed',

// SDAM
'serverOpening',
'serverClosed',
'serverDescriptionChanged',
'serverHeartbeatStarted',
'serverHeartbeatSucceeded',
'serverHeartbeatFailed',
'topologyOpening',
'topologyClosed',
'topologyDescriptionChanged',

// Legacy
'joined',
'left',
'ping',
'ha',

// CMAP
'connectionPoolCreated',
'connectionPoolClosed',
'connectionCreated',
'connectionReady',
'connectionClosed',
'connectionCheckOutStarted',
'connectionCheckOutFailed',
'connectionCheckedOut',
'connectionCheckedIn',
'connectionPoolCleared'
];

this[kInternalClient] = new MongoClient(client.s.url, clonedOptions);

for (const eventName of allEvents) {
for (const listener of client.listeners(eventName)) {
this[kInternalClient].on(eventName, listener);
}
}

client.on('newListener', (eventName, listener) => {
this[kInternalClient].on(eventName, listener);
});

this.needsConnecting = true;
}
return this[kInternalClient];
}

connectInternalClient(callback) {
if (this.needsConnecting) {
this.needsConnecting = false;
return this[kInternalClient].connect(callback);
}

return callback();
}

close(client, force, callback) {
this.autoEncrypter.teardown(e => {
if (this[kInternalClient] && client !== this[kInternalClient]) {
return this[kInternalClient].close(force, callback);
}
callback(e);
});
}

static makeBSON(options) {
return (
(options || {}).bson ||
new BSON([
BSON.Binary,
BSON.Code,
BSON.DBRef,
BSON.Decimal128,
BSON.Double,
BSON.Int32,
BSON.Long,
BSON.Map,
BSON.MaxKey,
BSON.MinKey,
BSON.ObjectId,
BSON.BSONRegExp,
BSON.Symbol,
BSON.Timestamp
])
);
}
}

module.exports = { Encrypter };
45 changes: 38 additions & 7 deletions lib/mongo_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ const validOptions = require('./operations/connect').validOptions;
* @property {string} [platform] Optional platform information
*/

/**
* @public
* @typedef AutoEncryptionOptions
* @property {MongoClient} [keyVaultClient] A `MongoClient` used to fetch keys from a key vault
* @property {string} [keyVaultNamespace] The namespace where keys are stored in the key vault
* @property {object} [kmsProviders] Configuration options that are used by specific KMS providers during key generation, encryption, and decryption.
* @property {object} [schemaMap] A map of namespaces to a local JSON schema for encryption
*
* > **NOTE**: Supplying options.schemaMap provides more security than relying on JSON Schemas obtained from the server.
* > It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending decrypted data that should be encrypted.
* > Schemas supplied in the schemaMap only apply to configuring automatic encryption for client side encryption.
* > Other validation rules in the JSON schema will not be enforced by the driver and will result in an error.
*
* @property {object} [options] An optional hook to catch logging messages from the underlying encryption engine
* @property {object} [extraOptions]
* @property {boolean} [bypassAutoEncryption]
*/

/**
* Creates a new MongoClient instance
* @class
Expand Down Expand Up @@ -151,7 +169,18 @@ const validOptions = require('./operations/connect').validOptions;
* @param {number} [options.minPoolSize=0] **Only applies to the unified topology** The minimum number of connections that MUST exist at any moment in a single connection pool.
* @param {number} [options.maxIdleTimeMS] **Only applies to the unified topology** The maximum amount of time a connection should remain idle in the connection pool before being marked idle. The default is infinity.
* @param {number} [options.waitQueueTimeoutMS=0] **Only applies to the unified topology** The maximum amount of time operation execution should wait for a connection to become available. The default is 0 which means there is no limit.
* @param {AutoEncrypter~AutoEncryptionOptions} [options.autoEncryption] Optionally enable client side auto encryption
* @param {AutoEncryptionOptions} [options.autoEncryption] Optionally enable client side auto encryption.
*
* > Automatic encryption is an enterprise only feature that only applies to operations on a collection. Automatic encryption is not supported for operations on a database or view, and operations that are not bypassed will result in error
* > (see [libmongocrypt: Auto Encryption Allow-List](https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#libmongocrypt-auto-encryption-allow-list)). To bypass automatic encryption for all operations, set bypassAutoEncryption=true in AutoEncryptionOpts.
* >
* > Automatic encryption requires the authenticated user to have the [listCollections privilege action](https://docs.mongodb.com/manual/reference/command/listCollections/#dbcmd.listCollections).
* >
* > If a MongoClient with a limited connection pool size (i.e a non-zero maxPoolSize) is configured with AutoEncryptionOptions, a separate internal MongoClient is created if any of the following are true:
* > - AutoEncryptionOptions.keyVaultClient is not passed.
* > - AutoEncryptionOptions.bypassAutomaticEncryption is false.
* > If an internal MongoClient is created, it is configured with the same options as the parent MongoClient except minPoolSize is set to 0 and AutoEncryptionOptions is omitted.
*
* @param {DriverInfoOptions} [options.driverInfo] Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver
* @param {boolean} [options.directConnection=false] Enable directConnection
* @param {MongoClient~connectCallback} [callback] The command result callback
Expand All @@ -162,6 +191,8 @@ function MongoClient(url, options) {
// Set up event emitter
EventEmitter.call(this);

if (options && options.autoEncryption) require('./encrypter'); // Does CSFLE lib check

// The internal state
this.s = {
url: url,
Expand Down Expand Up @@ -268,13 +299,13 @@ MongoClient.prototype.close = function(force, callback) {
}

client.topology.close(force, err => {
const autoEncrypter = client.topology.s.options.autoEncrypter;
if (!autoEncrypter) {
completeClose(err);
return;
const encrypter = client.topology.s.options.encrypter;
if (encrypter) {
return encrypter.close(client, force, err2 => {
completeClose(err || err2);
});
}

autoEncrypter.teardown(force, err2 => completeClose(err || err2));
completeClose(err);
});
});
};
Expand Down
61 changes: 7 additions & 54 deletions lib/operations/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const emitDeprecationWarning = require('../utils').emitDeprecationWarning;
const emitWarningOnce = require('../utils').emitWarningOnce;
const fs = require('fs');
const WriteConcern = require('../write_concern');
const BSON = require('../core/connection/utils').retrieveBSON();
const CMAP_EVENT_NAMES = require('../cmap/events').CMAP_EVENT_NAMES;

let client;
Expand Down Expand Up @@ -496,58 +495,9 @@ function createTopology(mongoClient, topologyType, options, callback) {

// determine CSFLE support
if (options.autoEncryption != null) {
let AutoEncrypter;
try {
require.resolve('mongodb-client-encryption');
} catch (err) {
callback(
new MongoError(
'Auto-encryption requested, but the module is not installed. Please add `mongodb-client-encryption` as a dependency of your project'
)
);
return;
}

try {
let mongodbClientEncryption = require('mongodb-client-encryption');
if (typeof mongodbClientEncryption.extension !== 'function') {
callback(
new MongoError(
'loaded version of `mongodb-client-encryption` does not have property `extension`. Please make sure you are loading the correct version of `mongodb-client-encryption`'
)
);
}
AutoEncrypter = mongodbClientEncryption.extension(require('../../index')).AutoEncrypter;
} catch (err) {
callback(err);
return;
}

const mongoCryptOptions = Object.assign(
{
bson:
options.bson ||
new BSON([
BSON.Binary,
BSON.Code,
BSON.DBRef,
BSON.Decimal128,
BSON.Double,
BSON.Int32,
BSON.Long,
BSON.Map,
BSON.MaxKey,
BSON.MinKey,
BSON.ObjectId,
BSON.BSONRegExp,
BSON.Symbol,
BSON.Timestamp
])
},
options.autoEncryption
);

options.autoEncrypter = new AutoEncrypter(mongoClient, mongoCryptOptions);
const Encrypter = require('../encrypter').Encrypter;
options.encrypter = new Encrypter(mongoClient, options);
options.autoEncrypter = options.encrypter.autoEncrypter;
}

// Create the topology
Expand Down Expand Up @@ -585,7 +535,10 @@ function createTopology(mongoClient, topologyType, options, callback) {
return;
}

callback(undefined, topology);
options.encrypter.connectInternalClient(error => {
if (error) return callback(error);
callback(undefined, topology);
});
});
});

Expand Down

0 comments on commit 3e098ea

Please sign in to comment.