Skip to content

Commit 6d4be7b

Browse files
initial commit
1 parent 19c0132 commit 6d4be7b

File tree

8 files changed

+1523
-122
lines changed

8 files changed

+1523
-122
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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,16 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
320320
};
321321
}
322322

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

@@ -343,6 +353,55 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
343353
return this;
344354
};
345355

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

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/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+
};

scripts/configure-cluster-with-encryption.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export CWD=$(pwd);
88
export DRIVERS_TOOLS_PINNED_COMMIT=35d0592c76f4f3d25a5607895eb21b491dd52543;
99

1010
# install extra dependency
11-
npm install mongodb-client-encryption
11+
npm install --no-save mongodb-client-encryption
1212

1313
# set up mongodb cluster and encryption configuration if the data/ folder does not exist
1414
if [ ! -d "data" ]; then

scripts/run-encryption-tests.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
3+
# sets up mongodb cluster and encryption configuration, adds relevant variables to the environment, and runs encryption tests
4+
5+
export CWD=$(pwd);
6+
7+
# set up mongodb cluster and encryption configuration if the data/ folder does not exist
8+
# note: for tooling, cluster set-up and configuration look into the 'scripts/configure-cluster-with-encryption.sh' script
9+
10+
if [ -d "data" ]; then
11+
cd data
12+
else
13+
source $CWD/scripts/configure-cluster-with-encryption.sh
14+
fi
15+
16+
# extracts MONGOOSE_TEST_URI and CRYPT_SHARED_LIB_PATH from .yml file into environment variables for this test run
17+
read -r -d '' SOURCE_SCRIPT << EOM
18+
const fs = require('fs');
19+
const file = fs.readFileSync('mo-expansion.yml', { encoding: 'utf-8' })
20+
.trim().split('\\n');
21+
const regex = /^(?<key>.*): "(?<value>.*)"$/;
22+
const variables = file.map(
23+
(line) => regex.exec(line.trim()).groups
24+
).map(
25+
({key, value}) => \`export \${key}='\${value}'\`
26+
).join('\n');
27+
28+
process.stdout.write(variables);
29+
process.stdout.write('\n');
30+
EOM
31+
32+
node --eval "$SOURCE_SCRIPT" | tee expansions.sh
33+
source expansions.sh
34+
35+
export MONGOOSE_TEST_URI=$MONGODB_URI
36+
37+
# run encryption tests
38+
cd ..
39+
npx mocha --exit ./test/encryption/*.test.js

0 commit comments

Comments
 (0)