Skip to content

Commit 871a4d9

Browse files
make models encryptable
1 parent 19c0132 commit 871a4d9

File tree

15 files changed

+2250
-165
lines changed

15 files changed

+2250
-165
lines changed

docs/field-level-encryption.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,13 @@ To declare a field as encrypted, you must:
151151
2. Choose an encryption type for the schema and configure the schema for the encryption type
152152

153153
Not all schematypes are supported for CSFLE and QE. For an overview of valid schema types, refer to MongoDB's documentation.
154+
155+
### Registering Models
156+
157+
Encrypted schemas must be registered on a connection, not the Mongoose global:
158+
159+
```javascript
160+
161+
const connection = mongoose.createConnection();
162+
const UserModel = connection.model('User', encryptedUserSchema);
163+
```

lib/collection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Collection.prototype.onOpen = function() {
8181
* @api private
8282
*/
8383

84-
Collection.prototype.onClose = function() {};
84+
Collection.prototype.onClose = function() { };
8585

8686
/**
8787
* Queues a method for later execution when its

lib/connection.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
609609

610610
Connection.prototype.createCollections = async function createCollections(options = {}) {
611611
const result = {};
612-
const errorsMap = { };
612+
const errorsMap = {};
613613

614614
const { continueOnError } = options;
615615
delete options.continueOnError;
@@ -736,7 +736,7 @@ Connection.prototype.transaction = function transaction(fn, options) {
736736
throw err;
737737
}).
738738
finally(() => {
739-
session.endSession().catch(() => {});
739+
session.endSession().catch(() => { });
740740
});
741741
});
742742
};
@@ -1080,7 +1080,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10801080

10811081
for (const model of Object.values(this.models)) {
10821082
// Errors handled internally, so safe to ignore error
1083-
model.init().catch(function $modelInitNoop() {});
1083+
model.init().catch(function $modelInitNoop() { });
10841084
}
10851085

10861086
// `createConnection()` calls this `openUri()` function without
@@ -1116,7 +1116,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
11161116
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
11171117
Connection.prototype.on = function on(event, callback) {
11181118
if (event === 'error' && this.$initialConnection) {
1119-
this.$initialConnection.catch(() => {});
1119+
this.$initialConnection.catch(() => { });
11201120
}
11211121
return EventEmitter.prototype.on.call(this, event, callback);
11221122
};
@@ -1138,7 +1138,7 @@ Connection.prototype.on = function on(event, callback) {
11381138
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
11391139
Connection.prototype.once = function on(event, callback) {
11401140
if (event === 'error' && this.$initialConnection) {
1141-
this.$initialConnection.catch(() => {});
1141+
this.$initialConnection.catch(() => { });
11421142
}
11431143
return EventEmitter.prototype.once.call(this, event, callback);
11441144
};
@@ -1471,7 +1471,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14711471
}
14721472

14731473
// Errors handled internally, so safe to ignore error
1474-
model.init().catch(function $modelInitNoop() {});
1474+
model.init().catch(function $modelInitNoop() { });
14751475

14761476
return model;
14771477
}
@@ -1498,7 +1498,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14981498
}
14991499

15001500
if (this === model.prototype.db
1501-
&& (!collection || collection === model.collection.name)) {
1501+
&& (!collection || collection === model.collection.name)) {
15021502
// model already uses this connection.
15031503

15041504
// only the first model with this name is cached to allow
@@ -1685,8 +1685,8 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanis
16851685
*/
16861686
Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) {
16871687
return (options) &&
1688-
(options.user) &&
1689-
((options.pass) || this.authMechanismDoesNotRequirePassword());
1688+
(options.user) &&
1689+
((options.pass) || this.authMechanismDoesNotRequirePassword());
16901690
};
16911691

16921692
/**
@@ -1748,7 +1748,7 @@ Connection.prototype.createClient = function createClient() {
17481748
*/
17491749
Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
17501750
const result = {};
1751-
const errorsMap = { };
1751+
const errorsMap = {};
17521752

17531753
const { continueOnError } = options;
17541754
delete options.continueOnError;

lib/drivers/node-mongodb-native/connection.js

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const pkg = require('../../../package.json');
1212
const processConnectionOptions = require('../../helpers/processConnectionOptions');
1313
const setTimeout = require('../../helpers/timers').setTimeout;
1414
const utils = require('../../utils');
15+
const { Schema } = require('../../mongoose');
1516

1617
/**
1718
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
@@ -320,6 +321,16 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
320321
};
321322
}
322323

324+
const { schemaMap, encryptedFieldsMap } = this._buildEncryptionSchemas();
325+
326+
if (Object.keys(schemaMap).length > 0) {
327+
options.autoEncryption.schemaMap = schemaMap;
328+
}
329+
330+
if (Object.keys(encryptedFieldsMap).length > 0) {
331+
options.autoEncryption.encryptedFieldsMap = encryptedFieldsMap;
332+
}
333+
323334
this.readyState = STATES.connecting;
324335
this._connectionString = uri;
325336

@@ -343,6 +354,55 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
343354
return this;
344355
};
345356

357+
/**
358+
* Given a connection, which may or may not have encrypted models, build
359+
* a schemaMap and/or an encryptedFieldsMap for the connection, combining all models
360+
* into a single schemaMap and encryptedFields map.
361+
*
362+
* @returns a copy of the options object with a schemaMap and/or an encryptedFieldsMap added to the options' autoEncryption
363+
* options.
364+
*/
365+
NativeConnection.prototype._buildEncryptionSchemas = function() {
366+
const qeMappings = {};
367+
const csfleMappings = {};
368+
369+
// If discriminators are configured for the collection, there might be multiple models
370+
// pointing to the same namespace. For this scenario, we merge all the schemas for each namespace
371+
// into a single schema.
372+
// Notably, this doesn't allow for discriminators to declare multiple values on the same fields.
373+
for (const model of Object.values(this.models)) {
374+
const { schema, collection: { collectionName } } = model;
375+
const namespace = `${this.$dbName}.${collectionName}`;
376+
if (schema.encryptionType() === 'csfle') {
377+
csfleMappings[namespace] ??= new Schema({}, { encryptionType: 'csfle' });
378+
csfleMappings[namespace].add(schema);
379+
} else if (schema.encryptionType() === 'queryableEncryption') {
380+
qeMappings[namespace] ??= new Schema({}, { encryptionType: 'queryableEncryption' });
381+
qeMappings[namespace].add(schema);
382+
}
383+
}
384+
385+
const schemaMap = Object.entries(csfleMappings).reduce(
386+
(schemaMap, [namespace, schema]) => {
387+
schemaMap[namespace] = schema._buildSchemaMap();
388+
return schemaMap;
389+
},
390+
{}
391+
);
392+
393+
const encryptedFieldsMap = Object.entries(qeMappings).reduce(
394+
(encryptedFieldsMap, [namespace, schema]) => {
395+
encryptedFieldsMap[namespace] = schema._buildEncryptedFields();
396+
return encryptedFieldsMap;
397+
},
398+
{}
399+
);
400+
401+
return {
402+
schemaMap, encryptedFieldsMap
403+
};
404+
};
405+
346406
/*!
347407
* ignore
348408
*/
@@ -363,7 +423,7 @@ NativeConnection.prototype.setClient = function setClient(client) {
363423

364424
for (const model of Object.values(this.models)) {
365425
// Errors handled internally, so safe to ignore error
366-
model.init().catch(function $modelInitNoop() {});
426+
model.init().catch(function $modelInitNoop() { });
367427
}
368428

369429
return this;
@@ -406,9 +466,9 @@ function _setClient(conn, client, options, dbName) {
406466
};
407467

408468
const type = client &&
409-
client.topology &&
410-
client.topology.description &&
411-
client.topology.description.type || '';
469+
client.topology &&
470+
client.topology.description &&
471+
client.topology.description.type || '';
412472

413473
if (type === 'Single') {
414474
client.on('serverDescriptionChanged', ev => {

lib/encryptionUtils.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
const schemaTypes = require('./schema/index.js');
4+
const SchemaBigInt = require('./schema/bigint');
5+
const SchemaBoolean = require('./schema/boolean');
6+
const SchemaBuffer = require('./schema/buffer');
7+
const SchemaDate = require('./schema/date');
8+
const SchemaDecimal128 = require('./schema/decimal128');
9+
const SchemaDouble = require('./schema/double');
10+
const SchemaInt32 = require('./schema/int32');
11+
const SchemaObjectId = require('./schema/objectId');
12+
const SchemaString = require('./schema/string');
13+
14+
/**
15+
* Given a schema and a path to a field in the schema, this returns the
16+
* BSON type of the field, if it can be determined. This method specifically
17+
* **only** handles BSON types that are used for CSFLE and QE - any other
18+
* BSON types will return `null`. (example: MinKey and MaxKey).
19+
*
20+
* @param {import('.').Schema} schema
21+
* @param {string} path
22+
* @returns {string}
23+
*/
24+
function inferBSONType(schema, path) {
25+
const type = schema.path(path);
26+
27+
if (type instanceof SchemaString) {
28+
return 'string';
29+
}
30+
31+
if (type instanceof SchemaInt32) {
32+
return 'int';
33+
}
34+
35+
if (type instanceof SchemaBigInt) {
36+
return 'long';
37+
}
38+
39+
if (type instanceof SchemaBoolean) {
40+
return 'bool';
41+
}
42+
43+
if (type instanceof SchemaDate) {
44+
return 'date';
45+
}
46+
47+
if (type instanceof SchemaBuffer) {
48+
return 'binData';
49+
}
50+
51+
if (type instanceof SchemaObjectId) {
52+
return 'objectId';
53+
}
54+
55+
if (type instanceof SchemaDecimal128) {
56+
return 'decimal';
57+
}
58+
59+
if (type instanceof SchemaDouble) {
60+
return 'double';
61+
}
62+
63+
if (type instanceof schemaTypes.Array) {
64+
return 'array';
65+
}
66+
67+
return null;
68+
}
69+
70+
module.exports = {
71+
inferBSONType
72+
};

lib/encryption_utils.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
const { Array } = require('./schema/index.js');
4+
const SchemaBigInt = require('./schema/bigint');
5+
const SchemaBoolean = require('./schema/boolean');
6+
const SchemaBuffer = require('./schema/buffer');
7+
const SchemaDate = require('./schema/date');
8+
const SchemaDecimal128 = require('./schema/decimal128');
9+
const SchemaDouble = require('./schema/double');
10+
const SchemaInt32 = require('./schema/int32');
11+
const SchemaObjectId = require('./schema/objectId');
12+
const SchemaString = require('./schema/string');
13+
14+
/**
15+
* Given a schema and a path to a field in the schema, this returns the
16+
* BSON type of the field, if it can be determined. This method specifically
17+
* **only** handles BSON types that are used for CSFLE and QE - any other
18+
* BSON types will return `null`. (example: MinKey and MaxKey).
19+
*
20+
* @param {import('.').Schema} schema
21+
* @param {string} path
22+
* @returns
23+
*/
24+
function inferBSONType(schema, path) {
25+
const type = schema.path(path);
26+
27+
if (type instanceof SchemaString) {
28+
return 'string';
29+
}
30+
31+
if (type instanceof SchemaInt32) {
32+
return 'int';
33+
}
34+
35+
if (type instanceof SchemaBigInt) {
36+
return 'long';
37+
}
38+
39+
if (type instanceof SchemaBoolean) {
40+
return 'bool';
41+
}
42+
43+
if (type instanceof SchemaDate) {
44+
return 'date';
45+
}
46+
47+
if (type instanceof SchemaBuffer) {
48+
return 'binData';
49+
}
50+
51+
if (type instanceof SchemaObjectId) {
52+
return 'objectId';
53+
}
54+
55+
if (type instanceof SchemaDecimal128) {
56+
return 'decimal';
57+
}
58+
59+
if (type instanceof SchemaDouble) {
60+
return 'double';
61+
}
62+
63+
if (type instanceof Array) {
64+
return 'array';
65+
}
66+
67+
return null;
68+
}
69+
70+
module.exports = {
71+
inferBSONType
72+
};

0 commit comments

Comments
 (0)