Skip to content

Commit ea70d8f

Browse files
committed
fix(NODE-2995): Add shared metadata MongoClient
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.
1 parent b94519b commit ea70d8f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+724
-747
lines changed

.evergreen/run-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ if [[ -z "${CLIENT_ENCRYPTION}" ]]; then
5252
unset AWS_ACCESS_KEY_ID;
5353
unset AWS_SECRET_ACCESS_KEY;
5454
else
55-
npm install mongodb-client-encryption@1.1.1-beta.0
55+
npm install mongodb-client-encryption@latest
5656
fi
5757

5858
MONGODB_UNIFIED_TOPOLOGY=${UNIFIED} MONGODB_URI=${MONGODB_URI} npm run ${TEST_NPM_SCRIPT}

lib/encrypter.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
'use strict';
2+
const MongoClient = require('./mongo_client');
3+
const BSON = require('./core/connection/utils').retrieveBSON();
4+
const MongoError = require('./core/error').MongoError;
5+
6+
try {
7+
require.resolve('mongodb-client-encryption');
8+
} catch (err) {
9+
throw new MongoError(
10+
'Auto-encryption requested, but the module is not installed. ' +
11+
'Please add `mongodb-client-encryption` as a dependency of your project'
12+
);
13+
}
14+
15+
const mongodbClientEncryption = require('mongodb-client-encryption');
16+
if (typeof mongodbClientEncryption.extension !== 'function') {
17+
throw new MongoError(
18+
'loaded version of `mongodb-client-encryption` does not have property `extension`. ' +
19+
'Please make sure you are loading the correct version of `mongodb-client-encryption`'
20+
);
21+
}
22+
const AutoEncrypter = mongodbClientEncryption.extension(require('../index')).AutoEncrypter;
23+
24+
const kInternalClient = Symbol('internalClient');
25+
26+
class Encrypter {
27+
/**
28+
* @param {MongoClient} client
29+
* @param {{autoEncryption: import('./mongo_client').AutoEncryptionOptions, bson: object}} options
30+
*/
31+
constructor(client, options) {
32+
this.bypassAutoEncryption = !!options.autoEncryption.bypassAutoEncryption;
33+
this.needsConnecting = false;
34+
35+
if (options.maxPoolSize === 0 && options.autoEncryption.keyVaultClient == null) {
36+
options.autoEncryption.keyVaultClient = client;
37+
} else if (options.autoEncryption.keyVaultClient == null) {
38+
options.autoEncryption.keyVaultClient = this.getInternalClient(client);
39+
}
40+
41+
if (this.bypassAutoEncryption) {
42+
options.autoEncryption.metadataClient = undefined;
43+
} else if (options.maxPoolSize === 0) {
44+
options.autoEncryption.metadataClient = client;
45+
} else {
46+
options.autoEncryption.metadataClient = this.getInternalClient(client);
47+
}
48+
49+
options.autoEncryption.bson = Encrypter.makeBSON(options);
50+
51+
this.autoEncrypter = new AutoEncrypter(client, options.autoEncryption);
52+
}
53+
54+
getInternalClient(client) {
55+
if (!this[kInternalClient]) {
56+
const clonedOptions = {};
57+
58+
for (const key of Object.keys(client.s.options)) {
59+
if (
60+
['autoEncryption', 'minPoolSize', 'servers', 'caseTranslate', 'dbName'].indexOf(key) !==
61+
-1
62+
)
63+
continue;
64+
clonedOptions[key] = client.s.options[key];
65+
}
66+
67+
clonedOptions.minPoolSize = 0;
68+
69+
const allEvents = [
70+
// APM
71+
'commandStarted',
72+
'commandSucceeded',
73+
'commandFailed',
74+
75+
// SDAM
76+
'serverOpening',
77+
'serverClosed',
78+
'serverDescriptionChanged',
79+
'serverHeartbeatStarted',
80+
'serverHeartbeatSucceeded',
81+
'serverHeartbeatFailed',
82+
'topologyOpening',
83+
'topologyClosed',
84+
'topologyDescriptionChanged',
85+
86+
// Legacy
87+
'joined',
88+
'left',
89+
'ping',
90+
'ha',
91+
92+
// CMAP
93+
'connectionPoolCreated',
94+
'connectionPoolClosed',
95+
'connectionCreated',
96+
'connectionReady',
97+
'connectionClosed',
98+
'connectionCheckOutStarted',
99+
'connectionCheckOutFailed',
100+
'connectionCheckedOut',
101+
'connectionCheckedIn',
102+
'connectionPoolCleared'
103+
];
104+
105+
this[kInternalClient] = new MongoClient(client.s.url, clonedOptions);
106+
107+
for (const eventName of allEvents) {
108+
for (const listener of client.listeners(eventName)) {
109+
this[kInternalClient].on(eventName, listener);
110+
}
111+
}
112+
113+
client.on('newListener', (eventName, listener) => {
114+
this[kInternalClient].on(eventName, listener);
115+
});
116+
117+
this.needsConnecting = true;
118+
}
119+
return this[kInternalClient];
120+
}
121+
122+
connectInternalClient(callback) {
123+
if (this.needsConnecting) {
124+
this.needsConnecting = false;
125+
return this[kInternalClient].connect(callback);
126+
}
127+
128+
return callback();
129+
}
130+
131+
close(client, force, callback) {
132+
this.autoEncrypter.teardown(e => {
133+
if (this[kInternalClient] && client !== this[kInternalClient]) {
134+
return this[kInternalClient].close(force, callback);
135+
}
136+
callback(e);
137+
});
138+
}
139+
140+
static makeBSON(options) {
141+
return (
142+
(options || {}).bson ||
143+
new BSON([
144+
BSON.Binary,
145+
BSON.Code,
146+
BSON.DBRef,
147+
BSON.Decimal128,
148+
BSON.Double,
149+
BSON.Int32,
150+
BSON.Long,
151+
BSON.Map,
152+
BSON.MaxKey,
153+
BSON.MinKey,
154+
BSON.ObjectId,
155+
BSON.BSONRegExp,
156+
BSON.Symbol,
157+
BSON.Timestamp
158+
])
159+
);
160+
}
161+
}
162+
163+
module.exports = { Encrypter };

lib/mongo_client.js

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ const validOptions = require('./operations/connect').validOptions;
7171
* @property {string} [platform] Optional platform information
7272
*/
7373

74+
/**
75+
* @public
76+
* @typedef AutoEncryptionOptions
77+
* @property {MongoClient} [keyVaultClient] A `MongoClient` used to fetch keys from a key vault
78+
* @property {string} [keyVaultNamespace] The namespace where keys are stored in the key vault
79+
* @property {object} [kmsProviders] Configuration options that are used by specific KMS providers during key generation, encryption, and decryption.
80+
* @property {object} [schemaMap] A map of namespaces to a local JSON schema for encryption
81+
*
82+
* > **NOTE**: Supplying options.schemaMap provides more security than relying on JSON Schemas obtained from the server.
83+
* > It protects against a malicious server advertising a false JSON Schema, which could trick the client into sending decrypted data that should be encrypted.
84+
* > Schemas supplied in the schemaMap only apply to configuring automatic encryption for client side encryption.
85+
* > Other validation rules in the JSON schema will not be enforced by the driver and will result in an error.
86+
*
87+
* @property {object} [options] An optional hook to catch logging messages from the underlying encryption engine
88+
* @property {object} [extraOptions]
89+
* @property {boolean} [bypassAutoEncryption]
90+
*/
91+
7492
/**
7593
* Creates a new MongoClient instance
7694
* @class
@@ -151,7 +169,18 @@ const validOptions = require('./operations/connect').validOptions;
151169
* @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.
152170
* @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.
153171
* @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.
154-
* @param {AutoEncrypter~AutoEncryptionOptions} [options.autoEncryption] Optionally enable client side auto encryption
172+
* @param {AutoEncryptionOptions} [options.autoEncryption] Optionally enable client side auto encryption.
173+
*
174+
* > 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
175+
* > (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.
176+
* >
177+
* > Automatic encryption requires the authenticated user to have the [listCollections privilege action](https://docs.mongodb.com/manual/reference/command/listCollections/#dbcmd.listCollections).
178+
* >
179+
* > 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:
180+
* > - AutoEncryptionOptions.keyVaultClient is not passed.
181+
* > - AutoEncryptionOptions.bypassAutomaticEncryption is false.
182+
* > 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.
183+
*
155184
* @param {DriverInfoOptions} [options.driverInfo] Allows a wrapping driver to amend the client metadata generated by the driver to include information about the wrapping driver
156185
* @param {boolean} [options.directConnection=false] Enable directConnection
157186
* @param {MongoClient~connectCallback} [callback] The command result callback
@@ -162,6 +191,8 @@ function MongoClient(url, options) {
162191
// Set up event emitter
163192
EventEmitter.call(this);
164193

194+
if (options && options.autoEncryption) require('./encrypter'); // Does CSFLE lib check
195+
165196
// The internal state
166197
this.s = {
167198
url: url,
@@ -268,13 +299,13 @@ MongoClient.prototype.close = function(force, callback) {
268299
}
269300

270301
client.topology.close(force, err => {
271-
const autoEncrypter = client.topology.s.options.autoEncrypter;
272-
if (!autoEncrypter) {
273-
completeClose(err);
274-
return;
302+
const encrypter = client.topology.s.options.encrypter;
303+
if (encrypter) {
304+
return encrypter.close(client, force, err2 => {
305+
completeClose(err || err2);
306+
});
275307
}
276-
277-
autoEncrypter.teardown(force, err2 => completeClose(err || err2));
308+
completeClose(err);
278309
});
279310
});
280311
};

lib/operations/connect.js

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const emitDeprecationWarning = require('../utils').emitDeprecationWarning;
1616
const emitWarningOnce = require('../utils').emitWarningOnce;
1717
const fs = require('fs');
1818
const WriteConcern = require('../write_concern');
19-
const BSON = require('../core/connection/utils').retrieveBSON();
2019
const CMAP_EVENT_NAMES = require('../cmap/events').CMAP_EVENT_NAMES;
2120

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

497496
// determine CSFLE support
498497
if (options.autoEncryption != null) {
499-
let AutoEncrypter;
500-
try {
501-
require.resolve('mongodb-client-encryption');
502-
} catch (err) {
503-
callback(
504-
new MongoError(
505-
'Auto-encryption requested, but the module is not installed. Please add `mongodb-client-encryption` as a dependency of your project'
506-
)
507-
);
508-
return;
509-
}
510-
511-
try {
512-
let mongodbClientEncryption = require('mongodb-client-encryption');
513-
if (typeof mongodbClientEncryption.extension !== 'function') {
514-
callback(
515-
new MongoError(
516-
'loaded version of `mongodb-client-encryption` does not have property `extension`. Please make sure you are loading the correct version of `mongodb-client-encryption`'
517-
)
518-
);
519-
}
520-
AutoEncrypter = mongodbClientEncryption.extension(require('../../index')).AutoEncrypter;
521-
} catch (err) {
522-
callback(err);
523-
return;
524-
}
525-
526-
const mongoCryptOptions = Object.assign(
527-
{
528-
bson:
529-
options.bson ||
530-
new BSON([
531-
BSON.Binary,
532-
BSON.Code,
533-
BSON.DBRef,
534-
BSON.Decimal128,
535-
BSON.Double,
536-
BSON.Int32,
537-
BSON.Long,
538-
BSON.Map,
539-
BSON.MaxKey,
540-
BSON.MinKey,
541-
BSON.ObjectId,
542-
BSON.BSONRegExp,
543-
BSON.Symbol,
544-
BSON.Timestamp
545-
])
546-
},
547-
options.autoEncryption
548-
);
549-
550-
options.autoEncrypter = new AutoEncrypter(mongoClient, mongoCryptOptions);
498+
const Encrypter = require('../encrypter').Encrypter;
499+
options.encrypter = new Encrypter(mongoClient, options);
500+
options.autoEncrypter = options.encrypter.autoEncrypter;
551501
}
552502

553503
// Create the topology
@@ -585,7 +535,10 @@ function createTopology(mongoClient, topologyType, options, callback) {
585535
return;
586536
}
587537

588-
callback(undefined, topology);
538+
options.encrypter.connectInternalClient(error => {
539+
if (error) return callback(error);
540+
callback(undefined, topology);
541+
});
589542
});
590543
});
591544

0 commit comments

Comments
 (0)