Skip to content

Commit ebe3d06

Browse files
bunch o extra changes?
1 parent 42d9607 commit ebe3d06

File tree

13 files changed

+624
-72
lines changed

13 files changed

+624
-72
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Encryption Tests
2+
3+
on:
4+
push:
5+
branches: ['master']
6+
pull_request:
7+
branches: [ 'master' ]
8+
workflow_dispatch: {}
9+
10+
permissions:
11+
contents: write
12+
pull-requests: write
13+
id-token: write
14+
15+
jobs:
16+
run-tests:
17+
permissions:
18+
# required for all workflows
19+
security-events: write
20+
id-token: write
21+
contents: write
22+
runs-on: ubuntu-latest
23+
name: Encryption tests
24+
env:
25+
FORCE_COLOR: true
26+
steps:
27+
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
28+
- name: Setup node
29+
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
30+
with:
31+
node-version: latest
32+
- name: Install Dependencies
33+
run: npm install
34+
- name: Install mongodb-client-encryption
35+
run: npm install mongodb-client-encryption
36+
- name: Run Tests
37+
run: npm run test-encryption

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,6 @@ examples/ecommerce-netlify-functions/.netlify/state.json
6767

6868
notes.md
6969
list.out
70+
71+
data
72+
*.pid

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ If you have a question about Mongoose (not a bug report) please post it to eithe
4646
* execute `npm run test-tsd` to run the typescript tests
4747
* execute `npm run ts-benchmark` to run the typescript benchmark "performance test" for a single time.
4848
* execute `npm run ts-benchmark-watch` to run the typescript benchmark "performance test" while watching changes on types folder. Note: Make sure to commit all changes before executing this command.
49+
* in order to run tests that require an cluster with encryption locally, run `npm run test-encryption`. Alternatively, you can start an encrypted cluster using the `scripts/configure-cluster-with-encryption.sh` file.
4950

5051
## Documentation
5152

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
@@ -607,7 +607,7 @@ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
607607

608608
Connection.prototype.createCollections = async function createCollections(options = {}) {
609609
const result = {};
610-
const errorsMap = { };
610+
const errorsMap = {};
611611

612612
const { continueOnError } = options;
613613
delete options.continueOnError;
@@ -734,7 +734,7 @@ Connection.prototype.transaction = function transaction(fn, options) {
734734
throw err;
735735
}).
736736
finally(() => {
737-
session.endSession().catch(() => {});
737+
session.endSession().catch(() => { });
738738
});
739739
});
740740
};
@@ -1025,7 +1025,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10251025

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

10311031
// `createConnection()` calls this `openUri()` function without
@@ -1061,7 +1061,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
10611061
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
10621062
Connection.prototype.on = function on(event, callback) {
10631063
if (event === 'error' && this.$initialConnection) {
1064-
this.$initialConnection.catch(() => {});
1064+
this.$initialConnection.catch(() => { });
10651065
}
10661066
return EventEmitter.prototype.on.call(this, event, callback);
10671067
};
@@ -1083,7 +1083,7 @@ Connection.prototype.on = function on(event, callback) {
10831083
// to avoid uncaught exceptions when using `on('error')`. See gh-14377.
10841084
Connection.prototype.once = function on(event, callback) {
10851085
if (event === 'error' && this.$initialConnection) {
1086-
this.$initialConnection.catch(() => {});
1086+
this.$initialConnection.catch(() => { });
10871087
}
10881088
return EventEmitter.prototype.once.call(this, event, callback);
10891089
};
@@ -1412,7 +1412,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14121412
}
14131413

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

14171417
return model;
14181418
}
@@ -1439,7 +1439,7 @@ Connection.prototype.model = function model(name, schema, collection, options) {
14391439
}
14401440

14411441
if (this === model.prototype.db
1442-
&& (!collection || collection === model.collection.name)) {
1442+
&& (!collection || collection === model.collection.name)) {
14431443
// model already uses this connection.
14441444

14451445
// only the first model with this name is cached to allow
@@ -1626,8 +1626,8 @@ Connection.prototype.authMechanismDoesNotRequirePassword = function authMechanis
16261626
*/
16271627
Connection.prototype.optionsProvideAuthenticationData = function optionsProvideAuthenticationData(options) {
16281628
return (options) &&
1629-
(options.user) &&
1630-
((options.pass) || this.authMechanismDoesNotRequirePassword());
1629+
(options.user) &&
1630+
((options.pass) || this.authMechanismDoesNotRequirePassword());
16311631
};
16321632

16331633
/**
@@ -1689,7 +1689,7 @@ Connection.prototype.createClient = function createClient() {
16891689
*/
16901690
Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
16911691
const result = {};
1692-
const errorsMap = { };
1692+
const errorsMap = {};
16931693

16941694
const { continueOnError } = options;
16951695
delete options.continueOnError;

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

Lines changed: 113 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 { inferBSONType } = require('../../encryption_utils');
1516

1617
/**
1718
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
@@ -304,6 +305,17 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
304305
};
305306
}
306307

308+
309+
const { schemaMap, encryptedFieldsMap } = this._buildEncryptionSchemas(options);
310+
311+
if (Object.keys(schemaMap).length > 0) {
312+
options.autoEncryption.schemaMap = schemaMap;
313+
}
314+
315+
if (Object.keys(encryptedFieldsMap).length > 0) {
316+
options.autoEncryption.encryptedFieldsMap = encryptedFieldsMap;
317+
}
318+
307319
this.readyState = STATES.connecting;
308320
this._connectionString = uri;
309321

@@ -327,6 +339,103 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
327339
return this;
328340
};
329341

342+
/**
343+
* Given a connection, which may or may not have encrypted models, build
344+
* a schemaMap and/or an encryptedFieldsMap for the connection, combining all models
345+
* into a single schemaMap and encryptedFields map.
346+
*
347+
* @returns a copy of the options object with a schemaMap and/or an encryptedFieldsMap added to the options' autoEncryption
348+
* options.
349+
*/
350+
NativeConnection.prototype._buildEncryptionSchemas = function() {
351+
const schemaMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'csfle').reduce(
352+
schemaMapReducer.bind(this),
353+
{}
354+
);
355+
const encryptedFieldsMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'qe').reduce(
356+
encryptedFieldsMapReducer.bind(this),
357+
{}
358+
);
359+
360+
return {
361+
schemaMap, encryptedFieldsMap
362+
};
363+
364+
/**
365+
* `schemaMap`s are JSON schemas, which use the following structure to represent objects:
366+
* { field: { bsonType: 'object', properties: { ... } } }
367+
*
368+
* for example, a schema that looks like this `{ a: { b: int32 } }` would be encoded as
369+
* `{ a: { bsonType: 'object', properties: { b: < encryption configuration > } } }`
370+
*
371+
* This function takes an array of path segments, an output object (that gets mutated) and
372+
* a value to associated with the full path, and constructs a valid CSFLE JSON schema path for
373+
* the object. This works for deeply nested properties as well.
374+
*
375+
* @param {string[]} path array of path components
376+
* @param {object} object the object in which to build a JSON schema of `path`'s properties
377+
* @param {object} value the value to associate with the path in object
378+
*/
379+
function buildNestedPath(path, object, value) {
380+
let i = 0, component = path[i];
381+
for (; i < path.length - 1; ++i, component = path[i]) {
382+
object[component] = object[component] == null ? {
383+
bsonType: 'object',
384+
properties: {}
385+
} : object[component];
386+
object = object[component].properties;
387+
}
388+
object[component] = value;
389+
}
390+
391+
/**
392+
* @param {object} schemaMap the accumulation schemaMap
393+
* @param {Model} the model
394+
* @returns
395+
*/
396+
function schemaMapReducer(schemaMap, model) {
397+
const { schema, collection: { collectionName } } = model;
398+
const namespace = `${this.$dbName}.${collectionName}`;
399+
400+
function schemaMapPropertyReducer(accum, [path, propertyConfig]) {
401+
const bsonType = inferBSONType(schema, path);
402+
const pathComponents = path.split('.');
403+
const configuration = { encrypt: { ...propertyConfig, bsonType } };
404+
buildNestedPath(pathComponents, accum, configuration);
405+
return accum;
406+
}
407+
const properties = Object.entries(schema.encryptedFields).reduce(
408+
schemaMapPropertyReducer,
409+
{});
410+
411+
schemaMap[namespace] = {
412+
bsonType: 'object',
413+
properties
414+
};
415+
return schemaMap;
416+
}
417+
418+
/**
419+
*
420+
* @param {object} encryptedFieldsMap the accumulation encryptedFieldsMap
421+
* @param {Model} the model
422+
* @returns
423+
*/
424+
function encryptedFieldsMapReducer(encryptedFieldsMap, { schema, collection: { collectionName } }) {
425+
const namespace = `${this.$dbName}.${collectionName}`;
426+
const fields = Object.entries(schema.encryptedFields).map(
427+
([path, config]) => {
428+
const bsonType = inferBSONType(schema, path);
429+
// { path, bsonType, keyId, queries? }
430+
return { path, bsonType, ...config };
431+
});
432+
433+
encryptedFieldsMap[namespace] = { fields };
434+
435+
return encryptedFieldsMap;
436+
}
437+
};
438+
330439
/*!
331440
* ignore
332441
*/
@@ -347,7 +456,7 @@ NativeConnection.prototype.setClient = function setClient(client) {
347456

348457
for (const model of Object.values(this.models)) {
349458
// Errors handled internally, so safe to ignore error
350-
model.init().catch(function $modelInitNoop() {});
459+
model.init().catch(function $modelInitNoop() { });
351460
}
352461

353462
return this;
@@ -390,9 +499,9 @@ function _setClient(conn, client, options, dbName) {
390499
};
391500

392501
const type = client &&
393-
client.topology &&
394-
client.topology.description &&
395-
client.topology.description.type || '';
502+
client.topology &&
503+
client.topology.description &&
504+
client.topology.description.type || '';
396505

397506
if (type === 'Single') {
398507
client.on('serverDescriptionChanged', ev => {

lib/utils.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,19 @@ exports.deepEqual = function deepEqual(a, b) {
8989
}
9090

9191
if ((isBsonType(a, 'ObjectId') && isBsonType(b, 'ObjectId')) ||
92-
(isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) {
92+
(isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) {
9393
return a.toString() === b.toString();
9494
}
9595

9696
if (a instanceof RegExp && b instanceof RegExp) {
9797
return a.source === b.source &&
98-
a.ignoreCase === b.ignoreCase &&
99-
a.multiline === b.multiline &&
100-
a.global === b.global &&
101-
a.dotAll === b.dotAll &&
102-
a.unicode === b.unicode &&
103-
a.sticky === b.sticky &&
104-
a.hasIndices === b.hasIndices;
98+
a.ignoreCase === b.ignoreCase &&
99+
a.multiline === b.multiline &&
100+
a.global === b.global &&
101+
a.dotAll === b.dotAll &&
102+
a.unicode === b.unicode &&
103+
a.sticky === b.sticky &&
104+
a.hasIndices === b.hasIndices;
105105
}
106106

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

998-
exports.noop = function() {};
998+
exports.noop = function() { };
999999

10001000
exports.errorToPOJO = function errorToPOJO(error) {
10011001
const isError = error instanceof Error;
@@ -1025,3 +1025,13 @@ exports.injectTimestampsOption = function injectTimestampsOption(writeOperation,
10251025
}
10261026
writeOperation.timestamps = timestampsOption;
10271027
};
1028+
1029+
exports.print = function(...args) {
1030+
const { inspect } = require('util');
1031+
console.error(
1032+
inspect(
1033+
...args,
1034+
{ depth: Infinity }
1035+
)
1036+
);
1037+
};

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
"test-deno": "deno run --allow-env --allow-read --allow-net --allow-run --allow-sys --allow-write ./test/deno.js",
103103
"test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js",
104104
"test-tsd": "node ./test/types/check-types-filename && tsd",
105+
"test-encryption": "bash scripts/run-encryption-tests.sh",
105106
"tdd": "mocha ./test/*.test.js --inspect --watch --recursive --watch-files ./**/*.{js,ts}",
106107
"test-coverage": "nyc --reporter=html --reporter=text npm test",
107108
"ts-benchmark": "cd ./benchmarks/typescript/simple && npm install && npm run benchmark | node ../../../scripts/tsc-diagnostics-check"
@@ -142,4 +143,4 @@
142143
"target": "ES2017"
143144
}
144145
}
145-
}
146+
}

0 commit comments

Comments
 (0)