Skip to content

Commit 53d31a6

Browse files
throw errors when discriminators have duplicate keys
1 parent a8f37eb commit 53d31a6

File tree

4 files changed

+165
-10
lines changed

4 files changed

+165
-10
lines changed

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ NativeConnection.prototype.createClient = async function createClient(uri, optio
323323

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

326+
if ((Object.keys(schemaMap).length > 0 || Object.keys(encryptedFieldsMap).length) && !options.autoEncryption) {
327+
throw new Error('Must provide `autoEncryption` when connecting with encrypted schemas.');
328+
}
329+
326330
if (Object.keys(schemaMap).length > 0) {
327331
options.autoEncryption.schemaMap = schemaMap;
328332
}
@@ -365,20 +369,30 @@ NativeConnection.prototype._buildEncryptionSchemas = function() {
365369
const qeMappings = {};
366370
const csfleMappings = {};
367371

372+
const encryptedModels = Object.values(this.models).filter(model => model.schema._hasEncryptedFields());
373+
368374
// If discriminators are configured for the collection, there might be multiple models
369375
// 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)) {
376+
// into a single schema and then generate a schemaMap/encryptedFieldsMap for the combined schema.
377+
for (const model of encryptedModels) {
373378
const { schema, collection: { collectionName } } = model;
374379
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);
380+
const mappings = schema.encryptionType() === 'csfle' ? csfleMappings : qeMappings;
381+
382+
mappings[namespace] ??= new Schema({}, { encryptionType: schema.encryptionType() });
383+
384+
const isNonRootDiscriminator = schema.discriminatorMapping && !schema.discriminatorMapping.isRoot;
385+
if (isNonRootDiscriminator) {
386+
const rootSchema = schema._baseSchema;
387+
schema.eachPath((pathname) => {
388+
if (rootSchema.path(pathname)) return;
389+
if (!mappings[namespace]._hasEncryptedField(pathname)) return;
390+
391+
throw new Error(`Cannot have duplicate keys in discriminators with encryption. key=${pathname}`);
392+
});
381393
}
394+
395+
mappings[namespace].add(schema);
382396
}
383397

384398
const schemaMap = Object.fromEntries(Object.entries(csfleMappings).map(

lib/helpers/model/discriminator.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
9595
const baseSchemaPaths = Object.keys(baseSchema.paths);
9696
const conflictingPaths = [];
9797

98+
99+
baseSchema.eachPath((pathname) => {
100+
if (schema._hasEncryptedField(pathname)) {
101+
throw new Error(`cannot declare an encrypted field on child schema overriding base schema. key=${pathname}`);
102+
}
103+
});
104+
98105
for (const path of baseSchemaPaths) {
99106
if (schema.nested[path]) {
100107
conflictingPaths.push(path);

lib/schema.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,6 @@ Schema.prototype.encryptionType = function encryptionType(encryptionType) {
721721
Schema.prototype.add = function add(obj, prefix) {
722722
if (obj instanceof Schema || (obj != null && obj.instanceOfSchema)) {
723723
merge(this, obj);
724-
725724
return this;
726725
}
727726

@@ -914,6 +913,14 @@ Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() {
914913
return Object.keys(this.encryptedFields).length > 0;
915914
};
916915

916+
/**
917+
* @api private
918+
*/
919+
Schema.prototype._hasEncryptedField = function _hasEncryptedField(path) {
920+
return path in this.encryptedFields;
921+
};
922+
923+
917924
/**
918925
* Builds an encryptedFieldsMap for the schema.
919926
*/

test/encryption/encryption.test.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,133 @@ describe('encryption integration tests', () => {
797797
});
798798
});
799799

800+
describe('duplicate keys in discriminators', function() {
801+
beforeEach(async function() {
802+
connection = createConnection();
803+
});
804+
describe('csfle', function() {
805+
it('throws on duplicate keys declared on different discriminators', async function() {
806+
const schema = new Schema({
807+
name: {
808+
type: String, encrypt: { keyId: [keyId], algorithm }
809+
}
810+
}, {
811+
encryptionType: 'csfle'
812+
});
813+
model = connection.model('Schema', schema);
814+
discrim1 = model.discriminator('Test', new Schema({
815+
age: {
816+
type: Int32, encrypt: { keyId: [keyId], algorithm }
817+
}
818+
}, {
819+
encryptionType: 'csfle'
820+
}));
821+
822+
discrim2 = model.discriminator('Test2', new Schema({
823+
age: {
824+
type: Int32, encrypt: { keyId: [keyId], algorithm }
825+
}
826+
}, {
827+
encryptionType: 'csfle'
828+
}));
829+
830+
const error = await connection.openUri(process.env.MONGOOSE_TEST_URI, {
831+
dbName: 'db', autoEncryption: {
832+
keyVaultNamespace: 'keyvault.datakeys',
833+
kmsProviders: { local: { key: LOCAL_KEY } },
834+
extraOptions: {
835+
cryptdSharedLibRequired: true,
836+
cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH
837+
}
838+
}
839+
}).catch(e => e);
840+
841+
assert.ok(error instanceof Error);
842+
assert.match(error.message, /Cannot have duplicate keys in discriminators with encryption/);
843+
});
844+
it('throws on duplicate keys declared on root and child discriminators', async function() {
845+
const schema = new Schema({
846+
name: {
847+
type: String, encrypt: { keyId: [keyId], algorithm }
848+
}
849+
}, {
850+
encryptionType: 'csfle'
851+
});
852+
model = connection.model('Schema', schema);
853+
assert.throws(() => model.discriminator('Test', new Schema({
854+
name: {
855+
type: String, encrypt: { keyId: [keyId], algorithm }
856+
}
857+
}, {
858+
encryptionType: 'csfle'
859+
})),
860+
/cannot declare an encrypted field on child schema overriding base schema\. key=name/
861+
);
862+
});
863+
});
864+
865+
describe('queryable encryption', function() {
866+
it('throws on duplicate keys declared on different discriminators', async function() {
867+
const schema = new Schema({
868+
name: {
869+
type: String, encrypt: { keyId }
870+
}
871+
}, {
872+
encryptionType: 'queryableEncryption'
873+
});
874+
model = connection.model('Schema', schema);
875+
discrim1 = model.discriminator('Test', new Schema({
876+
age: {
877+
type: Int32, encrypt: { keyId: keyId2 }
878+
}
879+
}, {
880+
encryptionType: 'queryableEncryption'
881+
}));
882+
883+
discrim2 = model.discriminator('Test2', new Schema({
884+
age: {
885+
type: Int32, encrypt: { keyId: keyId3 }
886+
}
887+
}, {
888+
encryptionType: 'queryableEncryption'
889+
}));
890+
891+
const error = await connection.openUri(process.env.MONGOOSE_TEST_URI, {
892+
dbName: 'db', autoEncryption: {
893+
keyVaultNamespace: 'keyvault.datakeys',
894+
kmsProviders: { local: { key: LOCAL_KEY } },
895+
extraOptions: {
896+
cryptdSharedLibRequired: true,
897+
cryptSharedLibPath: process.env.CRYPT_SHARED_LIB_PATH
898+
}
899+
}
900+
}).catch(e => e);
901+
902+
assert.ok(error instanceof Error);
903+
assert.match(error.message, /Cannot have duplicate keys in discriminators with encryption/);
904+
});
905+
it('throws on duplicate keys declared on root and child discriminators', async function() {
906+
const schema = new Schema({
907+
name: {
908+
type: String, encrypt: { keyId }
909+
}
910+
}, {
911+
encryptionType: 'queryableEncryption'
912+
});
913+
model = connection.model('Schema', schema);
914+
assert.throws(() => model.discriminator('Test', new Schema({
915+
name: {
916+
type: String, encrypt: { keyId: keyId2 }
917+
}
918+
}, {
919+
encryptionType: 'queryableEncryption'
920+
})),
921+
/cannot declare an encrypted field on child schema overriding base schema\. key=name/
922+
);
923+
});
924+
});
925+
});
926+
800927
});
801928
});
802929
});

0 commit comments

Comments
 (0)