From 411e69ddc795f5db1eb9fcb5bba63ed91db489b6 Mon Sep 17 00:00:00 2001 From: bailey Date: Mon, 13 Jan 2025 20:00:09 -0700 Subject: [PATCH] fix tests --- lib/drivers/node-mongodb-native/connection.js | 23 +- lib/utils.js | 82 ++-- test/encryption/encryption.test.js | 352 ++++++++---------- 3 files changed, 219 insertions(+), 238 deletions(-) diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 286988cc04..6bb2c96ba1 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -61,7 +61,7 @@ Object.setPrototypeOf(NativeConnection.prototype, MongooseConnection.prototype); * @api public */ -NativeConnection.prototype.useDb = function(name, options) { +NativeConnection.prototype.useDb = function (name, options) { // Return immediately if cached options = options || {}; if (options.useCache && this.relatedDbs[name]) { @@ -345,7 +345,7 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio * @returns a copy of the options object with a schemaMap and/or an encryptedFieldsMap added to the options' autoEncryption * options. */ -NativeConnection.prototype._buildEncryptionSchemas = function(options) { +NativeConnection.prototype._buildEncryptionSchemas = function (options) { const schemaMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'csfle').reduce( schemaMapReducer.bind(this), {} @@ -355,14 +355,17 @@ NativeConnection.prototype._buildEncryptionSchemas = function(options) { {} ); - return Object.assign( - clone(options), { - autoEncryption: { - ...options.autoEncryption, - schemaMap, - encryptedFieldsMap - } - }); + options = clone(options); + + if (Object.keys(schemaMap).length > 0) { + options.autoEncryption.schemaMap = schemaMap; + } + + if (Object.keys(encryptedFieldsMap).length > 0) { + options.autoEncryption.encryptedFieldsMap = encryptedFieldsMap; + } + + return options; /** * @param {object} schemaMap the accumulation schemaMap diff --git a/lib/utils.js b/lib/utils.js index 6fc5c335ef..afc8cb3386 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -45,7 +45,7 @@ const manySpaceRE = /\s+/; * @api private */ -exports.toCollectionName = function(name, pluralize) { +exports.toCollectionName = function (name, pluralize) { if (name === 'system.profile') { return name; } @@ -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) { @@ -190,7 +190,7 @@ exports.deepEqual = function deepEqual(a, b) { * @param {Array} arr */ -exports.last = function(arr) { +exports.last = function (arr) { if (arr.length > 0) { return arr[arr.length - 1]; } @@ -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) { @@ -397,7 +397,7 @@ exports.isNonBuiltinObject = function isNonBuiltinObject(val) { * @param {Any} arg */ -exports.isNativeObject = function(arg) { +exports.isNativeObject = function (arg) { return Array.isArray(arg) || arg instanceof Date || arg instanceof Boolean || @@ -410,7 +410,7 @@ exports.isNativeObject = function(arg) { * @param {Any} val */ -exports.isEmptyObject = function(val) { +exports.isEmptyObject = function (val) { return val != null && typeof val === 'object' && Object.keys(val).length === 0; @@ -451,13 +451,13 @@ exports.tick = function tick(callback) { if (typeof callback !== 'function') { return; } - return function() { + return function () { try { callback.apply(this, arguments); } catch (err) { // only nextTick on err to get out of // the event loop and avoid state corruption. - immediate(function() { + immediate(function () { throw err; }); } @@ -470,7 +470,7 @@ exports.tick = function tick(callback) { * @param {Any} v */ -exports.isMongooseType = function(v) { +exports.isMongooseType = function (v) { return isBsonType(v, 'ObjectId') || isBsonType(v, 'Decimal128') || v instanceof Buffer; }; @@ -562,10 +562,10 @@ exports.populate = function populate(path, select, model, match, options, subPop // an array, string, or object literal). function makeSingles(arr) { const ret = []; - arr.forEach(function(obj) { + arr.forEach(function (obj) { if (oneSpaceRE.test(obj.path)) { const paths = obj.path.split(manySpaceRE); - paths.forEach(function(p) { + paths.forEach(function (p) { const copy = Object.assign({}, obj); copy.path = p; ret.push(copy); @@ -582,11 +582,11 @@ exports.populate = function populate(path, select, model, match, options, subPop function _populateObj(obj) { if (Array.isArray(obj.populate)) { const ret = []; - obj.populate.forEach(function(obj) { + obj.populate.forEach(function (obj) { if (oneSpaceRE.test(obj.path)) { const copy = Object.assign({}, obj); const paths = copy.path.split(manySpaceRE); - paths.forEach(function(p) { + paths.forEach(function (p) { copy.path = p; ret.push(exports.populate(copy)[0]); }); @@ -620,7 +620,7 @@ function _populateObj(obj) { * @param {Any} map */ -exports.getValue = function(path, obj, map) { +exports.getValue = function (path, obj, map) { return mpath.get(path, obj, getValueLookup, map); }; @@ -653,7 +653,7 @@ function getValueLookup(obj, part) { * @param {Any} _copying */ -exports.setValue = function(path, val, obj, map, _copying) { +exports.setValue = function (path, val, obj, map, _copying) { mpath.set(path, val, obj, '_doc', map, _copying); }; @@ -687,7 +687,7 @@ const hop = Object.prototype.hasOwnProperty; * @param {String} prop */ -exports.object.hasOwnProperty = function(obj, prop) { +exports.object.hasOwnProperty = function (obj, prop) { return hop.call(obj, prop); }; @@ -698,7 +698,7 @@ exports.object.hasOwnProperty = function(obj, prop) { * @return {Boolean} */ -exports.isNullOrUndefined = function(val) { +exports.isNullOrUndefined = function (val) { return val === null || val === undefined; }; @@ -723,7 +723,7 @@ exports.array = {}; exports.array.flatten = function flatten(arr, filter, ret) { ret || (ret = []); - arr.forEach(function(item) { + arr.forEach(function (item) { if (Array.isArray(item)) { flatten(item, filter, ret); } else { @@ -742,7 +742,7 @@ exports.array.flatten = function flatten(arr, filter, ret) { const _hasOwnProperty = Object.prototype.hasOwnProperty; -exports.hasUserDefinedProperty = function(obj, key) { +exports.hasUserDefinedProperty = function (obj, key) { if (obj == null) { return false; } @@ -773,7 +773,7 @@ exports.hasUserDefinedProperty = function(obj, key) { const MAX_ARRAY_INDEX = Math.pow(2, 32) - 1; -exports.isArrayIndex = function(val) { +exports.isArrayIndex = function (val) { if (typeof val === 'number') { return val >= 0 && val <= MAX_ARRAY_INDEX; } @@ -800,7 +800,7 @@ exports.isArrayIndex = function(val) { * @api private */ -exports.array.unique = function(arr) { +exports.array.unique = function (arr) { const primitives = new Set(); const ids = new Set(); const ret = []; @@ -835,7 +835,7 @@ exports.buffer = {}; * @param {Object} b */ -exports.buffer.areEqual = function(a, b) { +exports.buffer.areEqual = function (a, b) { if (!Buffer.isBuffer(a)) { return false; } @@ -861,7 +861,7 @@ exports.getFunctionName = getFunctionName; * @param {Object} source */ -exports.decorate = function(destination, source) { +exports.decorate = function (destination, source) { for (const key in source) { if (specialProperties.has(key)) { continue; @@ -878,7 +878,7 @@ exports.decorate = function(destination, source) { * @api private */ -exports.mergeClone = function(to, fromObj) { +exports.mergeClone = function (to, fromObj) { if (isMongooseObject(fromObj)) { fromObj = fromObj.toObject({ transform: false, @@ -943,7 +943,7 @@ exports.mergeClone = function(to, fromObj) { * @api private */ -exports.each = function(arr, fn) { +exports.each = function (arr, fn) { for (const item of arr) { fn(item); } @@ -957,7 +957,7 @@ exports.each = function(arr, fn) { * @param {String|Number} newKey * @api private */ -exports.renameObjKey = function(oldObj, oldKey, newKey) { +exports.renameObjKey = function (oldObj, oldKey, newKey) { const keys = Object.keys(oldObj); return keys.reduce( (acc, val) => { @@ -976,7 +976,7 @@ exports.renameObjKey = function(oldObj, oldKey, newKey) { * ignore */ -exports.getOption = function(name) { +exports.getOption = function (name) { const sources = Array.prototype.slice.call(arguments, 1); for (const source of sources) { @@ -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; @@ -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 } + ) + ) +} \ No newline at end of file diff --git a/test/encryption/encryption.test.js b/test/encryption/encryption.test.js index cd06208b6d..9c45819d72 100644 --- a/test/encryption/encryption.test.js +++ b/test/encryption/encryption.test.js @@ -21,228 +21,196 @@ describe('ci', () => { }); }); - describe('basic integration', () => { - let keyVaultClient; - let dataKey; - let encryptedClient; - let unencryptedClient; - - beforeEach(async function () { - keyVaultClient = new mdb.MongoClient(process.env.MONGOOSE_TEST_URI); - await keyVaultClient.connect(); - await keyVaultClient.db('keyvault').collection('datakeys'); - const clientEncryption = new mdb.ClientEncryption(keyVaultClient, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_KEY } } - }); - dataKey = await clientEncryption.createDataKey('local'); - - encryptedClient = new mdb.MongoClient( - process.env.MONGOOSE_TEST_URI, - { - autoEncryption: { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_KEY } }, - schemaMap: { - 'db.coll': { - bsonType: 'object', - encryptMetadata: { - keyId: [dataKey] - }, - properties: { - a: { - encrypt: { - bsonType: 'int', - algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random', - keyId: [dataKey] - } - } - } - } - }, - extraOptions: { - cryptdSharedLibRequired: true, - cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH - } - } - } - ); - - unencryptedClient = new mdb.MongoClient(process.env.MONGOOSE_TEST_URI); + let dataKey; + let utilClient; + + beforeEach(async function () { + const keyVaultClient = new mdb.MongoClient(process.env.MONGOOSE_TEST_URI); + await keyVaultClient.connect(); + await keyVaultClient.db('keyvault').collection('datakeys'); + const clientEncryption = new mdb.ClientEncryption(keyVaultClient, { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } } }); + dataKey = await clientEncryption.createDataKey('local'); + await keyVaultClient.close(); - afterEach(async function () { - await keyVaultClient.close(); - await encryptedClient.close(); - await unencryptedClient.close(); - }); - - it('ci set-up should support basic mongodb auto-encryption integration', async () => { - await encryptedClient.connect(); - const { insertedId } = await encryptedClient.db('db').collection('coll').insertOne({ a: 1 }); - - // client not configured with autoEncryption, returns a encrypted binary type, meaning that encryption succeeded - const encryptedResult = await unencryptedClient.db('db').collection('coll').findOne({ _id: insertedId }); - - assert.ok(encryptedResult); - assert.ok(encryptedResult.a); - assert.ok(isBsonType(encryptedResult.a, 'Binary')); - assert.ok(encryptedResult.a.sub_type === 6); + utilClient = new mdb.MongoClient(process.env.MONGOOSE_TEST_URI); + }); - // when the encryptedClient runs a find, the original unencrypted value is returned - const unencryptedResult = await encryptedClient.db('db').collection('coll').findOne({ _id: insertedId }); - assert.ok(unencryptedResult); - assert.ok(unencryptedResult.a === 1); - }); + afterEach(async function () { + await utilClient.close(); + }); - describe('Tests that fields of valid schema types can be declared as encrypted schemas', function () { - const algorithm = 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'; - - const basicSchemaTypes = [ - { type: String, name: 'string', input: 3, expected: 3 }, - { type: Schema.Types.Boolean, name: 'boolean', input: true, expected: true }, - { type: Schema.Types.Buffer, name: 'buffer', input: Buffer.from([1, 2, 3]) }, - { type: Date, name: 'date', input: new Date(12, 12, 2012), expected: new Date(12, 12, 2012) }, - { type: ObjectId, name: 'objectid', input: new ObjectId() }, - { type: BigInt, name: 'bigint', input: 3n }, - { type: Decimal128, name: 'Decimal128', input: new Decimal128('1.5') }, - { type: Int32, name: 'int32', input: new Int32(5), expected: 5 }, - { type: Double, name: 'double', input: new Double(1.5) } - ]; - - for (const { type, name, input, expected } of basicSchemaTypes) { - let schema; - let model; - let connection; - - async function test() { - const [{ _id }] = await model.insertMany([{ field: input }]); - const encryptedDoc = await unencryptedClient.db('db').collection('schemas').findOne({ _id }); - - assert.ok(isBsonType(encryptedDoc.field, 'Binary')); - assert.ok(encryptedDoc.field.sub_type === 6); - - const doc = await model.findOne({ _id }); - if (Buffer.isBuffer(input)) { - // mongoose's Buffer does not support deep equality - instead use the Buffer.equals method. - assert.ok(doc.field.equals(input)); - } else { - assert.deepEqual(doc.field, expected ?? input); + describe('Tests that fields of valid schema types can be declared as encrypted schemas', function () { + const algorithm = 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'; + let connection; + let schema; + let model; + + const basicSchemaTypes = [ + { type: String, name: 'string', input: 3, expected: 3 }, + { type: Schema.Types.Boolean, name: 'boolean', input: true, expected: true }, + { type: Schema.Types.Buffer, name: 'buffer', input: Buffer.from([1, 2, 3]) }, + { type: Date, name: 'date', input: new Date(12, 12, 2012), expected: new Date(12, 12, 2012) }, + { type: ObjectId, name: 'objectid', input: new ObjectId() }, + { type: BigInt, name: 'bigint', input: 3n }, + { type: Decimal128, name: 'Decimal128', input: new Decimal128('1.5') }, + { type: Int32, name: 'int32', input: new Int32(5), expected: 5 }, + { type: Double, name: 'double', input: new Double(1.5) } + ]; + + for (const { type, name, input, expected } of basicSchemaTypes) { + + this.afterEach(async function () { + await connection?.close(); + }) + + async function test() { + const [{ _id }] = await model.insertMany([{ field: input }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.field, 'Binary')); + assert.ok(encryptedDoc.field.sub_type === 6); + + const doc = await model.findOne({ _id }); + if (Buffer.isBuffer(input)) { + // mongoose's Buffer does not support deep equality - instead use the Buffer.equals method. + assert.ok(doc.field.equals(input)); + } else { + assert.deepEqual(doc.field, expected ?? input); - } } + } - describe('CSFLE', function () { - beforeEach(async function () { - schema = new Schema({ - field: { - type, encrypt: { keyId: [dataKey], algorithm } - } - }, { - encryptionType: 'csfle' - }); - - connection = createConnection(); - model = connection.model('Schema', schema); - await connection.openUri(process.env.MONGOOSE_TEST_URI, { - dbName: 'db', autoEncryption: { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_KEY } }, - extraOptions: { - cryptdSharedLibRequired: true, - cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH - } - } - }); - - await model.deleteMany({}); + describe('CSFLE', function () { + beforeEach(async function () { + schema = new Schema({ + field: { + type, encrypt: { keyId: [dataKey], algorithm } + } + }, { + encryptionType: 'csfle' }); - it(`${name} encrypts and decrypts`, test); - }); - - describe('QE', function () { - beforeEach(async function () { - schema = new Schema({ - field: { - type, encrypt: { keyId: dataKey } - } - }, { - encryptionType: 'qe' - }); - - connection = createConnection(); - model = connection.model('Schema', schema); - await connection.openUri(process.env.MONGOOSE_TEST_URI, { - dbName: 'db', autoEncryption: { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_KEY } }, - extraOptions: { - cryptdSharedLibRequired: true, - cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH - } + connection = createConnection(); + model = connection.model('Schema', schema); + await connection.openUri(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH } - }); - - await model.deleteMany({}); + } }); - it(`${name} encrypts and decrypts`, test); + await model.deleteMany({}); }); - } - describe.skip('when a schema is instantiated with a nested encrypted schema', function () { - let schema; - beforeEach(function () { - const encryptedSchema = new Schema({ - encrypted: { - type: String, encrypt: { keyId: KEY_ID, algorithm } - } - }, { encryptionType: 'csfle' }); - schema = new Schema({ - field: encryptedSchema - }, { encryptionType: 'csfle' }); - }); - - - it('then the schema has a nested property that is encrypted', function () { - assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); - }); + it(`${name} encrypts and decrypts`, test); }); - describe.skip('when a schema is instantiated with a nested schema object', function () { - let schema; - beforeEach(function () { + describe('QE', function () { + beforeEach(async function () { schema = new Schema({ field: { - encrypted: { - type: String, encrypt: { keyId: KEY_ID, algorithm } + type, encrypt: { keyId: dataKey } + } + }, { + encryptionType: 'qe' + }); + + connection = createConnection(); + model = connection.model('Schema', schema); + await connection.openUri(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH } } - }, { encryptionType: 'csfle' }); + }); + + await model.deleteMany({}); + }); + + it(`${name} encrypts and decrypts`, test); + }); + } + + describe.skip('CSFLE - nested object schema', function () { + it(`nested objects encrypts and decrypts`, async function () { + const schema = new Schema({ + field: { + nested: { + type: String, + encrypt: { keyId: [dataKey], algorithm } + } + } + }, { + encryptionType: 'csfle' }); - it('then the schema has a nested property that is encrypted', function () { - assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); + connection = createConnection(); + model = connection.model('Schema', schema); + await connection.openUri(process.env.MONGOOSE_TEST_URI, { + dbName: 'db', autoEncryption: { + keyVaultNamespace: 'keyvault.datakeys', + kmsProviders: { local: { key: LOCAL_KEY } }, + extraOptions: { + cryptdSharedLibRequired: true, + cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH + } + } }); + + await model.deleteMany({}); + const [{ _id }] = await model.insertMany([{ field: { nested: "hello" } }]); + const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id }); + + assert.ok(isBsonType(encryptedDoc.field.nested, 'Binary')); + assert.ok(encryptedDoc.field.nested.sub_type === 6); + + const doc = await model.findOne({ _id }); + assert.deepEqual(doc.field.nested, "hello"); }); + }); - describe.skip('when a schema is instantiated as an Array', function () { - let schema; - beforeEach(function () { - schema = new Schema({ + describe.skip('when a schema is instantiated with a nested schema object', function () { + let schema; + beforeEach(function () { + schema = new Schema({ + field: { encrypted: { - type: [Number], - encrypt: { keyId: KEY_ID, algorithm } + type: String, encrypt: { keyId: KEY_ID, algorithm } } - }, { encryptionType: 'csfle' }); - }); + } + }, { encryptionType: 'csfle' }); + }); - it('then the schema has a nested property that is encrypted', function () { - assert.ok(schemaHasEncryptedProperty(schema, 'encrypted')); - }); + it('then the schema has a nested property that is encrypted', function () { + assert.ok(schemaHasEncryptedProperty(schema, ['field', 'encrypted'])); }); + }); + describe.skip('when a schema is instantiated as an Array', function () { + let schema; + beforeEach(function () { + schema = new Schema({ + encrypted: { + type: [Number], + encrypt: { keyId: KEY_ID, algorithm } + } + }, { encryptionType: 'csfle' }); + }); + + it('then the schema has a nested property that is encrypted', function () { + assert.ok(schemaHasEncryptedProperty(schema, 'encrypted')); + }); }); }); });