Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6507): generate encryption configuration on mongoose connect #6

Draft
wants to merge 1 commit into
base: NODE-6506-add-support-for-encrypted-schemas
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/field-level-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,13 @@ To declare a field as encrypted, you must:
2. Choose an encryption type for the schema and configure the schema for the encryption type

Not all schematypes are supported for CSFLE and QE. For an overview of valid schema types, refer to MongoDB's documentation.

### Registering Models

Encrypted schemas must be registered on a connection, not the Mongoose global:

```javascript

const connection = mongoose.createConnection();
const UserModel = connection.model('User', encryptedUserSchema);
```
2 changes: 1 addition & 1 deletion lib/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Collection.prototype.onOpen = function() {
* @api private
*/

Collection.prototype.onClose = function() {};
Collection.prototype.onClose = function() { };

/**
* Queues a method for later execution when its
Expand Down
20 changes: 10 additions & 10 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {

Connection.prototype.createCollections = async function createCollections(options = {}) {
const result = {};
const errorsMap = { };
const errorsMap = {};

const { continueOnError } = options;
delete options.continueOnError;
Expand Down Expand Up @@ -734,7 +734,7 @@ Connection.prototype.transaction = function transaction(fn, options) {
throw err;
}).
finally(() => {
session.endSession().catch(() => {});
session.endSession().catch(() => { });
});
});
};
Expand Down Expand Up @@ -1025,7 +1025,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {

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

// `createConnection()` calls this `openUri()` function without
Expand Down Expand Up @@ -1061,7 +1061,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
Connection.prototype.on = function on(event, callback) {
if (event === 'error' && this.$initialConnection) {
this.$initialConnection.catch(() => {});
this.$initialConnection.catch(() => { });
}
return EventEmitter.prototype.on.call(this, event, callback);
};
Expand All @@ -1083,7 +1083,7 @@ Connection.prototype.on = function on(event, callback) {
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
Connection.prototype.once = function on(event, callback) {
if (event === 'error' && this.$initialConnection) {
this.$initialConnection.catch(() => {});
this.$initialConnection.catch(() => { });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid making these sort of minor style changes, they make review difficult and may cause linter issues

}
return EventEmitter.prototype.once.call(this, event, callback);
};
Expand Down Expand Up @@ -1412,7 +1412,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
}

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

return model;
}
Expand All @@ -1439,7 +1439,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
}

if (this === model.prototype.db
&& (!collection || collection === model.collection.name)) {
&& (!collection || collection === model.collection.name)) {
// model already uses this connection.

// only the first model with this name is cached to allow
Expand Down Expand Up @@ -1626,8 +1626,8 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanis
*/
Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) {
return (options) &&
(options.user) &&
((options.pass) || this.authMechanismDoesNotRequirePassword());
(options.user) &&
((options.pass) || this.authMechanismDoesNotRequirePassword());
};

/**
Expand Down Expand Up @@ -1689,7 +1689,7 @@ Connection.prototype.createClient = function createClient() {
*/
Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
const result = {};
const errorsMap = { };
const errorsMap = {};

const { continueOnError } = options;
delete options.continueOnError;
Expand Down
52 changes: 48 additions & 4 deletions lib/drivers/node-mongodb-native/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,16 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
};
}

const { schemaMap, encryptedFieldsMap } = this._buildEncryptionSchemas();

if (Object.keys(schemaMap).length > 0) {
options.autoEncryption.schemaMap = schemaMap;
}

if (Object.keys(encryptedFieldsMap).length > 0) {
options.autoEncryption.encryptedFieldsMap = encryptedFieldsMap;
}

this.readyState = STATES.connecting;
this._connectionString = uri;

Expand All @@ -338,6 +348,40 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
return this;
};

/**
* Given a connection, which may or may not have encrypted models, build
* a schemaMap and/or an encryptedFieldsMap for the connection, combining all models
* into a single schemaMap and encryptedFields map.
*
* @returns a copy of the options object with a schemaMap and/or an encryptedFieldsMap added to the options' autoEncryption
* options.
*/
NativeConnection.prototype._buildEncryptionSchemas = function() {
const schemaMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'csfle').reduce(
(schemaMap, model) => {
const { schema, collection: { collectionName } } = model;
const namespace = `${this.$dbName}.${collectionName}`;
schemaMap[namespace] = schema._buildSchemaMap();
return schemaMap;
},
{}
);

const encryptedFieldsMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'qe').reduce(
(encryptedFieldsMap, model) => {
const { schema, collection: { collectionName } } = model;
const namespace = `${this.$dbName}.${collectionName}`;
encryptedFieldsMap[namespace] = schema._buildEncryptedFields();
return encryptedFieldsMap;
},
{}
);

return {
schemaMap, encryptedFieldsMap
};
};

/*!
* ignore
*/
Expand All @@ -358,7 +402,7 @@ NativeConnection.prototype.setClient = function setClient(client) {

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

return this;
Expand Down Expand Up @@ -401,9 +445,9 @@ function _setClient(conn, client, options, dbName) {
};

const type = client &&
client.topology &&
client.topology.description &&
client.topology.description.type || '';
client.topology &&
client.topology.description &&
client.topology.description.type || '';

if (type === 'Single') {
client.on('serverDescriptionChanged', ev => {
Expand Down
72 changes: 72 additions & 0 deletions lib/encryption_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

const { Array } = require('./schema/index.js');
const SchemaBigInt = require('./schema/bigint');
const SchemaBoolean = require('./schema/boolean');
const SchemaBuffer = require('./schema/buffer');
const SchemaDate = require('./schema/date');
const SchemaDecimal128 = require('./schema/decimal128');
const SchemaDouble = require('./schema/double');
const SchemaInt32 = require('./schema/int32');
const SchemaObjectId = require('./schema/objectId');
const SchemaString = require('./schema/string');

/**
* Given a schema and a path to a field in the schema, this returns the
* BSON type of the field, if it can be determined. This method specifically
* **only** handles BSON types that are used for CSFLE and QE - any other
* BSON types will return `null`. (example: MinKey and MaxKey).
*
* @param {import('.').Schema} schema
* @param {string} path
* @returns
*/
function inferBSONType(schema, path) {
const type = schema.path(path);

if (type instanceof SchemaString) {
return 'string';
}

if (type instanceof SchemaInt32) {
return 'int';
}

if (type instanceof SchemaBigInt) {
return 'long';
}

if (type instanceof SchemaBoolean) {
return 'bool';
}

if (type instanceof SchemaDate) {
return 'date';
}

if (type instanceof SchemaBuffer) {
return 'binData';
}

if (type instanceof SchemaObjectId) {
return 'objectId';
}

if (type instanceof SchemaDecimal128) {
return 'decimal';
}

if (type instanceof SchemaDouble) {
return 'double';
}

if (type instanceof Array) {
return 'array';
}

return null;
}

module.exports = {
inferBSONType
};
57 changes: 57 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,63 @@ Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() {
return Object.keys(this.encryptedFields).length > 0;
};

Schema.prototype._buildEncryptedFields = function() {
const fields = Object.entries(this.encryptedFields).map(
([path, config]) => {
const bsonType = inferBSONType(this, path);
// { path, bsonType, keyId, queries? }
return { path, bsonType, ...config };
});

return { fields };
};

Schema.prototype._buildSchemaMap = function() {
/**
* `schemaMap`s are JSON schemas, which use the following structure to represent objects:
* { field: { bsonType: 'object', properties: { ... } } }
*
* for example, a schema that looks like this `{ a: { b: int32 } }` would be encoded as
* `{ a: { bsonType: 'object', properties: { b: < encryption configuration > } } }`
*
* This function takes an array of path segments, an output object (that gets mutated) and
* a value to associated with the full path, and constructs a valid CSFLE JSON schema path for
* the object. This works for deeply nested properties as well.
*
* @param {string[]} path array of path components
* @param {object} object the object in which to build a JSON schema of `path`'s properties
* @param {object} value the value to associate with the path in object
*/
function buildNestedPath(path, object, value) {
let i = 0, component = path[i];
for (; i < path.length - 1; ++i, component = path[i]) {
object[component] = object[component] == null ? {
bsonType: 'object',
properties: {}
} : object[component];
object = object[component].properties;
}
object[component] = value;
}

const schemaMapPropertyReducer = (accum, [path, propertyConfig]) => {
const bsonType = inferBSONType(this, path);
const pathComponents = path.split('.');
const configuration = { encrypt: { ...propertyConfig, bsonType } };
buildNestedPath(pathComponents, accum, configuration);
return accum;
};

const properties = Object.entries(this.encryptedFields).reduce(
schemaMapPropertyReducer,
{});

return {
bsonType: 'object',
properties
};
};

/**
* Add an alias for `path`. This means getting or setting the `alias`
* is equivalent to getting or setting the `path`.
Expand Down
32 changes: 21 additions & 11 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,19 @@ exports.deepEqual = function deepEqual(a, b) {
}

if ((isBsonType(a, 'ObjectId') && isBsonType(b, 'ObjectId')) ||
(isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) {
(isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) {
return a.toString() === b.toString();
}

if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source &&
a.ignoreCase === b.ignoreCase &&
a.multiline === b.multiline &&
a.global === b.global &&
a.dotAll === b.dotAll &&
a.unicode === b.unicode &&
a.sticky === b.sticky &&
a.hasIndices === b.hasIndices;
a.ignoreCase === b.ignoreCase &&
a.multiline === b.multiline &&
a.global === b.global &&
a.dotAll === b.dotAll &&
a.unicode === b.unicode &&
a.sticky === b.sticky &&
a.hasIndices === b.hasIndices;
}

if (a == null || b == null) {
Expand Down Expand Up @@ -287,8 +287,8 @@ exports.merge = function merge(to, from, options, path) {
// base schema has a given path as a single nested but discriminator schema
// has the path as a document array, or vice versa (gh-9534)
if (options.isDiscriminatorSchemaMerge &&
(from[key].$isSingleNested && to[key].$isMongooseDocumentArray) ||
(from[key].$isMongooseDocumentArray && to[key].$isSingleNested)) {
(from[key].$isSingleNested && to[key].$isMongooseDocumentArray) ||
(from[key].$isMongooseDocumentArray && to[key].$isSingleNested)) {
continue;
} else if (from[key].instanceOfSchema) {
if (to[key].instanceOfSchema) {
Expand Down Expand Up @@ -995,7 +995,7 @@ exports.getOption = function(name) {
* ignore
*/

exports.noop = function() {};
exports.noop = function() { };

exports.errorToPOJO = function errorToPOJO(error) {
const isError = error instanceof Error;
Expand Down Expand Up @@ -1025,3 +1025,13 @@ exports.injectTimestampsOption = function injectTimestampsOption(writeOperation,
}
writeOperation.timestamps = timestampsOption;
};

exports.print = function(...args) {
const { inspect } = require('util');
console.error(
inspect(
...args,
{ depth: Infinity }
)
);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,4 @@
"target": "ES2017"
}
}
}
}
2 changes: 1 addition & 1 deletion scripts/configure-cluster-with-encryption.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ if [ ! -d "data" ]; then
echo 'Cluster Configuration Finished!'

cd ..
fi
fi
Loading
Loading