Skip to content

Commit db8eef7

Browse files
Add support for encrypted models and discriminators
1 parent 19c0132 commit db8eef7

File tree

14 files changed

+1530
-128
lines changed

14 files changed

+1530
-128
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/drivers/node-mongodb-native/connection.js

Lines changed: 60 additions & 0 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('../../schema');
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
*/

lib/schema.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,63 @@ Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() {
914914
return Object.keys(this.encryptedFields).length > 0;
915915
};
916916

917+
Schema.prototype._buildEncryptedFields = function() {
918+
const fields = Object.entries(this.encryptedFields).map(
919+
([path, config]) => {
920+
const bsonType = this.path(path).autoEncryptionType();
921+
// { path, bsonType, keyId, queries? }
922+
return { path, bsonType, ...config };
923+
});
924+
925+
return { fields };
926+
};
927+
928+
Schema.prototype._buildSchemaMap = function() {
929+
/**
930+
* `schemaMap`s are JSON schemas, which use the following structure to represent objects:
931+
* { field: { bsonType: 'object', properties: { ... } } }
932+
*
933+
* for example, a schema that looks like this `{ a: { b: int32 } }` would be encoded as
934+
* `{ a: { bsonType: 'object', properties: { b: < encryption configuration > } } }`
935+
*
936+
* This function takes an array of path segments, an output object (that gets mutated) and
937+
* a value to associated with the full path, and constructs a valid CSFLE JSON schema path for
938+
* the object. This works for deeply nested properties as well.
939+
*
940+
* @param {string[]} path array of path components
941+
* @param {object} object the object in which to build a JSON schema of `path`'s properties
942+
* @param {object} value the value to associate with the path in object
943+
*/
944+
function buildNestedPath(path, object, value) {
945+
let i = 0, component = path[i];
946+
for (; i < path.length - 1; ++i, component = path[i]) {
947+
object[component] = object[component] == null ? {
948+
bsonType: 'object',
949+
properties: {}
950+
} : object[component];
951+
object = object[component].properties;
952+
}
953+
object[component] = value;
954+
}
955+
956+
const schemaMapPropertyReducer = (accum, [path, propertyConfig]) => {
957+
const bsonType = this.path(path).autoEncryptionType();
958+
const pathComponents = path.split('.');
959+
const configuration = { encrypt: { ...propertyConfig, bsonType } };
960+
buildNestedPath(pathComponents, accum, configuration);
961+
return accum;
962+
};
963+
964+
const properties = Object.entries(this.encryptedFields).reduce(
965+
schemaMapPropertyReducer,
966+
{});
967+
968+
return {
969+
bsonType: 'object',
970+
properties
971+
};
972+
};
973+
917974
/**
918975
* Add an alias for `path`. This means getting or setting the `alias`
919976
* is equivalent to getting or setting the `path`.

lib/schema/bigint.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ SchemaBigInt.prototype.toJSONSchema = function toJSONSchema(options) {
255255
};
256256

257257
SchemaBigInt.prototype.autoEncryptionType = function autoEncryptionType() {
258-
return 'int64';
258+
return 'long';
259259
};
260260

261261
/*!

lib/schema/boolean.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ SchemaBoolean.prototype.toJSONSchema = function toJSONSchema(options) {
305305
};
306306

307307
SchemaBoolean.prototype.autoEncryptionType = function autoEncryptionType() {
308-
return 'boolean';
308+
return 'bool';
309309
};
310310

311311
/*!

lib/schema/buffer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ SchemaBuffer.prototype.toJSONSchema = function toJSONSchema(options) {
315315
};
316316

317317
SchemaBuffer.prototype.autoEncryptionType = function autoEncryptionType() {
318-
return 'binary';
318+
return 'binData';
319319
};
320320

321321
/*!

lib/schema/decimal128.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ SchemaDecimal128.prototype.toJSONSchema = function toJSONSchema(options) {
236236
};
237237

238238
SchemaDecimal128.prototype.autoEncryptionType = function autoEncryptionType() {
239-
return 'decimal128';
239+
return 'decimal';
240240
};
241241

242242
/*!

lib/schema/int32.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ SchemaInt32.prototype.toJSONSchema = function toJSONSchema(options) {
261261
};
262262

263263
SchemaInt32.prototype.autoEncryptionType = function autoEncryptionType() {
264-
return 'int32';
264+
return 'int';
265265
};
266266

267267

lib/schema/objectId.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ SchemaObjectId.prototype.toJSONSchema = function toJSONSchema(options) {
305305
};
306306

307307
SchemaObjectId.prototype.autoEncryptionType = function autoEncryptionType() {
308-
return 'objectid';
308+
return 'objectId';
309309
};
310310

311311
/*!

lib/utils.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,3 +1029,13 @@ exports.injectTimestampsOption = function injectTimestampsOption(writeOperation,
10291029
}
10301030
writeOperation.timestamps = timestampsOption;
10311031
};
1032+
1033+
exports.print = function(...args) {
1034+
const { inspect } = require('util');
1035+
console.error(
1036+
inspect(
1037+
...args,
1038+
{ depth: Infinity }
1039+
)
1040+
);
1041+
};

0 commit comments

Comments
 (0)