Skip to content

Commit ecb3f7c

Browse files
Add support for encrypted discriminators
1 parent 2c02d45 commit ecb3f7c

File tree

2 files changed

+91
-9
lines changed

2 files changed

+91
-9
lines changed

lib/helpers/model/discriminator.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,60 @@ const CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = {
1717
methods: true
1818
};
1919

20+
/**
21+
* Validate fields declared on the child schema when either schema is configured for encryption. Specifically, this function ensures that:
22+
*
23+
* - any encrypted fields are declared on exactly one of the schemas (not both)
24+
* - encrypted fields cannot be declared on either the parent or child schema, where the other schema declares the same field without encryption.
25+
*
26+
* @param {Schema} parentSchema
27+
* @param {Schema} childSchema
28+
*/
29+
function validateDiscriminatorSchemasForEncryption(parentSchema, childSchema) {
30+
/**
31+
* @param schema { Schema }
32+
*
33+
* Given a schema, yields **all** paths to values inside that schema, recursively iterating over any nested schemas. This is
34+
* intended for use with encryption, so nested arrays are not considered because encryption on values inside arrays is not supported.
35+
*
36+
* @returns { Iterable<string> }
37+
*/
38+
function* allPaths(schema, prefix) {
39+
for (const path of Object.keys(schema.paths)) {
40+
const fullPath = prefix != null ? `${prefix}.${path}` : path;
41+
if (schema.path(path).instance === 'Embedded') {
42+
yield* allPaths(schema.path(path).schema, fullPath);
43+
} else {
44+
yield fullPath;
45+
}
46+
}
47+
}
48+
49+
/**
50+
* @param {Iterable<T>} i1
51+
* @param {Iterable<T>} i2
52+
*
53+
* @returns {Generator<T>}
54+
*/
55+
function* setIntersection(i1, i2) {
56+
const s1 = new Set(i1);
57+
for (const item of i2) {
58+
if (s1.has(item)) {
59+
yield item;
60+
}
61+
}
62+
}
63+
64+
for (const path of setIntersection(allPaths(parentSchema), allPaths(childSchema))) {
65+
if (parentSchema._hasEncryptedField(path) && childSchema._hasEncryptedField(path)) {
66+
throw new Error(`encrypted fields cannot be declared on both the base schema and the child schema in a discriminator. path=${path}`);
67+
}
68+
69+
if (parentSchema._hasEncryptedField(path) || childSchema._hasEncryptedField(path)) {
70+
throw new Error(`encrypted fields cannot have the same path as a non-encrypted field for discriminators. path=${path}`);
71+
}
72+
}
73+
}
2074
/*!
2175
* ignore
2276
*/
@@ -80,6 +134,8 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
80134
value = tiedValue;
81135
}
82136

137+
validateDiscriminatorSchemasForEncryption(model.schema, schema);
138+
83139
function merge(schema, baseSchema) {
84140
// Retain original schema before merging base schema
85141
schema._baseSchema = baseSchema;
@@ -95,13 +151,6 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
95151
const baseSchemaPaths = Object.keys(baseSchema.paths);
96152
const conflictingPaths = [];
97153

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-
105154
for (const path of baseSchemaPaths) {
106155
if (schema.nested[path]) {
107156
conflictingPaths.push(path);

test/encryption/encryption.test.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ describe('encryption integration tests', () => {
10241024
}, {
10251025
encryptionType: 'csfle'
10261026
})),
1027-
/cannot declare an encrypted field on child schema overriding base schema\. key=name/
1027+
/encrypted fields cannot be declared on both the base schema and the child schema in a discriminator\. path=name/
10281028
);
10291029
});
10301030
});
@@ -1085,12 +1085,45 @@ describe('encryption integration tests', () => {
10851085
}, {
10861086
encryptionType: 'queryableEncryption'
10871087
})),
1088-
/cannot declare an encrypted field on child schema overriding base schema\. key=name/
1088+
/encrypted fields cannot be declared on both the base schema and the child schema in a discriminator\. path=name/
10891089
);
10901090
});
10911091
});
10921092
});
10931093

1094+
describe('Nested Schema overrides nested path', function() {
1095+
beforeEach(async function() {
1096+
connection = createConnection();
1097+
});
1098+
1099+
it('nested objects throw an error', async function() {
1100+
model = connection.model('Schema', new Schema({
1101+
name: {
1102+
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
1103+
}
1104+
}, { encryptionType: 'csfle' }));
1105+
1106+
assert.throws(() => {
1107+
model.discriminator('Test', new Schema({
1108+
name: { first: Number } // Different type, no encryption, stored as same field in MDB
1109+
}));
1110+
});
1111+
});
1112+
1113+
it('nested schemas throw an error', async function() {
1114+
model = connection.model('Schema', new Schema({
1115+
name: {
1116+
first: { type: String, encrypt: { keyId: [keyId], algorithm } }
1117+
}
1118+
}, { encryptionType: 'csfle' }));
1119+
1120+
assert.throws(() => {
1121+
model.discriminator('Test', new Schema({
1122+
name: new Schema({ first: Number }) // Different type, no encryption, stored as same field in MDB
1123+
}));
1124+
});
1125+
});
1126+
});
10941127
});
10951128
});
10961129
});

0 commit comments

Comments
 (0)