Skip to content

Commit 6374139

Browse files
add support for discriminators
1 parent be4563e commit 6374139

File tree

3 files changed

+214
-21
lines changed

3 files changed

+214
-21
lines changed

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

Lines changed: 24 additions & 8 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('../../mongoose');
1516

1617
/**
1718
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
@@ -357,20 +358,35 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
357358
* options.
358359
*/
359360
NativeConnection.prototype._buildEncryptionSchemas = function() {
360-
const schemaMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'csfle').reduce(
361-
(schemaMap, model) => {
362-
const { schema, collection: { collectionName } } = model;
363-
const namespace = `${this.$dbName}.${collectionName}`;
361+
const qeMappings = {};
362+
const csfleMappings = {};
363+
364+
// If discriminators are configured for the collection, there might be multiple models
365+
// pointing to the same namespace. For this scenario, we merge all the schemas for each namespace
366+
// into a single schema.
367+
// Notably, this doesn't allow for discriminators to declare multiple values on the same fields.
368+
for (const model of Object.values(this.models)) {
369+
const { schema, collection: { collectionName } } = model;
370+
const namespace = `${this.$dbName}.${collectionName}`;
371+
if (schema.encryptionType() === 'csfle') {
372+
csfleMappings[namespace] ??= new Schema({}, { encryptionType: 'csfle' });
373+
csfleMappings[namespace].add(schema);
374+
} else if (schema.encryptionType() === 'queryableEncryption') {
375+
qeMappings[namespace] ??= new Schema({}, { encryptionType: 'queryableEncryption' });
376+
qeMappings[namespace].add(schema);
377+
}
378+
}
379+
380+
const schemaMap = Object.entries(csfleMappings).reduce(
381+
(schemaMap, [namespace, schema]) => {
364382
schemaMap[namespace] = schema._buildSchemaMap();
365383
return schemaMap;
366384
},
367385
{}
368386
);
369387

370-
const encryptedFieldsMap = Object.values(this.models).filter(model => model.schema.encryptionType() === 'qe').reduce(
371-
(encryptedFieldsMap, model) => {
372-
const { schema, collection: { collectionName } } = model;
373-
const namespace = `${this.$dbName}.${collectionName}`;
388+
const encryptedFieldsMap = Object.entries(qeMappings).reduce(
389+
(encryptedFieldsMap, [namespace, schema]) => {
374390
encryptedFieldsMap[namespace] = schema._buildEncryptedFields();
375391
return encryptedFieldsMap;
376392
},

scripts/configure-cluster-with-encryption.sh

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

99
# install extra dependency
10-
npm install mongodb-client-encryption
10+
npm install --no-save mongodb-client-encryption
1111

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

test/encryption/encryption.test.js

Lines changed: 189 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ const { ObjectId, Double, Int32, Decimal128 } = require('bson');
88

99
const LOCAL_KEY = Buffer.from('Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk', 'base64');
1010

11+
/**
12+
* @param {object} object
13+
* @param {string} property
14+
*/
15+
function isEncryptedValue(object, property) {
16+
const value = object[property];
17+
assert.ok(isBsonType(value, 'Binary'), `auto encryption for property ${property} failed: not a BSON binary.`);
18+
assert.ok(value.sub_type === 6, `auto encryption for property ${property} failed: not subtype 6.`);
19+
}
20+
1121
describe('ci', () => {
1222
describe('environmental variables', () => {
1323
it('MONGOOSE_TEST_URI is set', async function() {
@@ -21,7 +31,7 @@ describe('ci', () => {
2131
});
2232
});
2333

24-
let keyId, keyId2;
34+
let keyId, keyId2, keyId3;
2535
let utilClient;
2636

2737
beforeEach(async function() {
@@ -34,6 +44,7 @@ describe('ci', () => {
3444
});
3545
keyId = await clientEncryption.createDataKey('local');
3646
keyId2 = await clientEncryption.createDataKey('local');
47+
keyId3 = await clientEncryption.createDataKey('local');
3748
await keyVaultClient.close();
3849

3950
utilClient = new mdb.MongoClient(process.env.MONGOOSE_TEST_URI);
@@ -65,7 +76,6 @@ describe('ci', () => {
6576
];
6677

6778
for (const { type, name, input, expected } of basicSchemaTypes) {
68-
6979
this.afterEach(async function() {
7080
await connection?.close();
7181
});
@@ -114,14 +124,14 @@ describe('ci', () => {
114124
it(`${name} encrypts and decrypts`, test);
115125
});
116126

117-
describe('QE', function() {
127+
describe('queryableEncryption', function() {
118128
beforeEach(async function() {
119129
schema = new Schema({
120130
field: {
121131
type, encrypt: { keyId: keyId }
122132
}
123133
}, {
124-
encryptionType: 'qe'
134+
encryptionType: 'queryableEncryption'
125135
});
126136

127137
connection = createConnection();
@@ -177,7 +187,7 @@ describe('ci', () => {
177187
}
178188
}
179189
}, {
180-
encryptionType: 'qe'
190+
encryptionType: 'queryableEncryption'
181191
});
182192

183193
connection = createConnection();
@@ -221,12 +231,12 @@ describe('ci', () => {
221231
}
222232
}
223233
}, {
224-
encryptionType: 'qe'
234+
encryptionType: 'queryableEncryption'
225235
});
226236
const schema = new Schema({
227237
a: nestedSchema
228238
}, {
229-
encryptionType: 'qe'
239+
encryptionType: 'queryableEncryption'
230240
});
231241

232242
connection = createConnection();
@@ -297,7 +307,7 @@ describe('ci', () => {
297307
}
298308
}
299309
}, {
300-
encryptionType: 'qe'
310+
encryptionType: 'queryableEncryption'
301311
});
302312

303313
connection = createConnection();
@@ -388,7 +398,7 @@ describe('ci', () => {
388398
}
389399
}
390400
}, {
391-
encryptionType: 'qe'
401+
encryptionType: 'queryableEncryption'
392402
});
393403

394404
connection = createConnection();
@@ -471,7 +481,7 @@ describe('ci', () => {
471481
}
472482
}
473483
}, {
474-
encryptionType: 'qe'
484+
encryptionType: 'queryableEncryption'
475485
}));
476486
const model2 = connection.model('Model2', new Schema({
477487
b: {
@@ -481,7 +491,7 @@ describe('ci', () => {
481491
}
482492
}
483493
}, {
484-
encryptionType: 'qe'
494+
encryptionType: 'queryableEncryption'
485495
}));
486496

487497
return { model1, model2 };
@@ -541,7 +551,7 @@ describe('ci', () => {
541551
}
542552
}
543553
}, {
544-
encryptionType: 'qe'
554+
encryptionType: 'queryableEncryption'
545555
}));
546556
const model2 = connection.model('Model2', new Schema({
547557
b: {
@@ -588,5 +598,172 @@ describe('ci', () => {
588598
}
589599
});
590600
});
601+
602+
describe('Models with discriminators', function() {
603+
let discrim1, discrim2, model;
604+
605+
describe('csfle', function() {
606+
beforeEach(async function() {
607+
connection = createConnection();
608+
609+
const schema = new Schema({
610+
name: {
611+
type: String, encrypt: { keyId: [keyId], algorithm }
612+
}
613+
}, {
614+
encryptionType: 'csfle'
615+
});
616+
model = connection.model('Schema', schema);
617+
discrim1 = model.discriminator('Test', new Schema({
618+
age: {
619+
type: Int32, encrypt: { keyId: [keyId], algorithm }
620+
}
621+
}, {
622+
encryptionType: 'csfle'
623+
}));
624+
625+
discrim2 = model.discriminator('Test2', new Schema({
626+
dob: {
627+
type: Int32, encrypt: { keyId: [keyId], algorithm }
628+
}
629+
}, {
630+
encryptionType: 'csfle'
631+
}));
632+
633+
634+
await connection.openUri(process.env.MONGOOSE_TEST_URI, {
635+
dbName: 'db', autoEncryption: {
636+
keyVaultNamespace: 'keyvault.datakeys',
637+
kmsProviders: { local: { key: LOCAL_KEY } },
638+
extraOptions: {
639+
cryptdSharedLibRequired: true,
640+
cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH
641+
}
642+
}
643+
});
644+
});
645+
it('encrypts', async function() {
646+
{
647+
const doc = new discrim1({ name: 'bailey', age: 32 });
648+
await doc.save();
649+
650+
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
651+
652+
isEncryptedValue(encryptedDoc, 'age');
653+
}
654+
655+
{
656+
const doc = new discrim2({ name: 'bailey', dob: 32 });
657+
await doc.save();
658+
659+
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
660+
661+
isEncryptedValue(encryptedDoc, 'dob');
662+
}
663+
});
664+
665+
it('decrypts', async function() {
666+
{
667+
const doc = new discrim1({ name: 'bailey', age: 32 });
668+
await doc.save();
669+
670+
const decryptedDoc = await discrim1.findOne({ _id: doc._id });
671+
672+
assert.equal(decryptedDoc.age, 32);
673+
}
674+
675+
{
676+
const doc = new discrim2({ name: 'bailey', dob: 32 });
677+
await doc.save();
678+
679+
const decryptedDoc = await discrim2.findOne({ _id: doc._id });
680+
681+
assert.equal(decryptedDoc.dob, 32);
682+
}
683+
});
684+
});
685+
686+
687+
describe('queryableEncryption', function() {
688+
beforeEach(async function() {
689+
connection = createConnection();
690+
691+
const schema = new Schema({
692+
name: {
693+
type: String, encrypt: { keyId }
694+
}
695+
}, {
696+
encryptionType: 'queryableEncryption'
697+
});
698+
model = connection.model('Schema', schema);
699+
discrim1 = model.discriminator('Test', new Schema({
700+
age: {
701+
type: Int32, encrypt: { keyId: keyId2 }
702+
}
703+
}, {
704+
encryptionType: 'queryableEncryption'
705+
}));
706+
707+
discrim2 = model.discriminator('Test2', new Schema({
708+
dob: {
709+
type: Int32, encrypt: { keyId: keyId3 }
710+
}
711+
}, {
712+
encryptionType: 'queryableEncryption'
713+
}));
714+
715+
await connection.openUri(process.env.MONGOOSE_TEST_URI, {
716+
dbName: 'db', autoEncryption: {
717+
keyVaultNamespace: 'keyvault.datakeys',
718+
kmsProviders: { local: { key: LOCAL_KEY } },
719+
extraOptions: {
720+
cryptdSharedLibRequired: true,
721+
cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH
722+
}
723+
}
724+
});
725+
});
726+
it('encrypts', async function() {
727+
{
728+
const doc = new discrim1({ name: 'bailey', age: 32 });
729+
await doc.save();
730+
731+
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
732+
733+
isEncryptedValue(encryptedDoc, 'age');
734+
}
735+
736+
{
737+
const doc = new discrim2({ name: 'bailey', dob: 32 });
738+
await doc.save();
739+
740+
const encryptedDoc = await utilClient.db('db').collection('schemas').findOne({ _id: doc._id });
741+
742+
isEncryptedValue(encryptedDoc, 'dob');
743+
}
744+
});
745+
746+
it('decrypts', async function() {
747+
{
748+
const doc = new discrim1({ name: 'bailey', age: 32 });
749+
await doc.save();
750+
751+
const decryptedDoc = await discrim1.findOne({ _id: doc._id });
752+
753+
assert.equal(decryptedDoc.age, 32);
754+
}
755+
756+
{
757+
const doc = new discrim2({ name: 'bailey', dob: 32 });
758+
await doc.save();
759+
760+
const decryptedDoc = await discrim2.findOne({ _id: doc._id });
761+
762+
assert.equal(decryptedDoc.dob, 32);
763+
}
764+
});
765+
});
766+
767+
});
591768
});
592769
});

0 commit comments

Comments
 (0)