Skip to content

Commit 6c469ec

Browse files
wip
1 parent 682160f commit 6c469ec

File tree

4 files changed

+733
-34
lines changed

4 files changed

+733
-34
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ module.exports = {
1414
'**/docs/js/native.js',
1515
'!.*',
1616
'node_modules',
17-
'.git'
17+
'.git',
18+
'data'
1819
],
1920
overrides: [
2021
{

lib/encryption_utils.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
const { Array } = require('./schema/index.js');
4+
const SchemaBigInt = require('./schema/bigint');
5+
const SchemaBoolean = require('./schema/boolean');
6+
const SchemaBuffer = require('./schema/buffer');
7+
const SchemaDate = require('./schema/date');
8+
const SchemaDecimal128 = require('./schema/decimal128');
9+
const SchemaDouble = require('./schema/double');
10+
const SchemaInt32 = require('./schema/int32');
11+
const SchemaObjectId = require('./schema/objectId');
12+
const SchemaString = require('./schema/string');
13+
14+
/**
15+
* Given a schema and a path to a field in the schema, this returns the
16+
* BSON type of the field, if it can be determined. This method specifically
17+
* **only** handles BSON types that are used for CSFLE and QE - any other
18+
* BSON types will return `null`. (example: MinKey and MaxKey).
19+
*
20+
* @param {import('.').Schema} schema
21+
* @param {string} path
22+
* @returns
23+
*/
24+
function inferBSONType(schema, path) {
25+
const type = schema.path(path);
26+
27+
if (type instanceof SchemaString) {
28+
return 'string';
29+
}
30+
31+
if (type instanceof SchemaInt32) {
32+
return 'int';
33+
}
34+
35+
if (type instanceof SchemaBigInt) {
36+
return 'long';
37+
}
38+
39+
if (type instanceof SchemaBoolean) {
40+
return 'bool';
41+
}
42+
43+
if (type instanceof SchemaDate) {
44+
return 'date';
45+
}
46+
47+
if (type instanceof SchemaBuffer) {
48+
return 'binData';
49+
}
50+
51+
if (type instanceof SchemaObjectId) {
52+
return 'objectId';
53+
}
54+
55+
if (type instanceof SchemaDecimal128) {
56+
return 'decimal';
57+
}
58+
59+
if (type instanceof SchemaDouble) {
60+
return 'double';
61+
}
62+
63+
if (type instanceof Array) {
64+
return 'array';
65+
}
66+
67+
return null;
68+
}
69+
70+
module.exports = {
71+
inferBSONType
72+
};

lib/schema.js

Lines changed: 127 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const setPopulatedVirtualValue = require('./helpers/populate/setPopulatedVirtual
2525
const setupTimestamps = require('./helpers/timestamps/setupTimestamps');
2626
const utils = require('./utils');
2727
const validateRef = require('./helpers/populate/validateRef');
28+
const { inferBSONType } = require('./encryption_utils');
2829

2930
const hasNumericSubpathRegex = /\.\d+(\.|$)/;
3031

@@ -128,6 +129,8 @@ function Schema(obj, options) {
128129
// For internal debugging. Do not use this to try to save a schema in MDB.
129130
this.$id = ++id;
130131
this.mapPaths = [];
132+
this.encryptedFields = {};
133+
this._encryptionType = options?.encryptionType;
131134

132135
this.s = {
133136
hooks: new Kareem()
@@ -166,7 +169,7 @@ function Schema(obj, options) {
166169

167170
// ensure the documents get an auto _id unless disabled
168171
const auto_id = !this.paths['_id'] &&
169-
(this.options._id) && !_idSubDoc;
172+
(this.options._id) && !_idSubDoc;
170173

171174
if (auto_id) {
172175
addAutoId(this);
@@ -463,6 +466,8 @@ Schema.prototype._clone = function _clone(Constructor) {
463466

464467
s.aliases = Object.assign({}, this.aliases);
465468

469+
s.encryptedFields = clone(this.encryptedFields);
470+
466471
return s;
467472
};
468473

@@ -495,7 +500,17 @@ Schema.prototype.pick = function(paths, options) {
495500
}
496501

497502
for (const path of paths) {
498-
if (this.nested[path]) {
503+
if (path in this.encryptedFields) {
504+
const encrypt = this.encryptedFields[path];
505+
const schemaType = this.path(path);
506+
newSchema.add({
507+
[path]: {
508+
encrypt,
509+
[this.options.typeKey]: schemaType
510+
}
511+
});
512+
}
513+
else if (this.nested[path]) {
499514
newSchema.add({ [path]: get(this.tree, path) });
500515
} else {
501516
const schematype = this.path(path);
@@ -506,6 +521,10 @@ Schema.prototype.pick = function(paths, options) {
506521
}
507522
}
508523

524+
if (!this._hasEncryptedFields()) {
525+
newSchema._encryptionType = null;
526+
}
527+
509528
return newSchema;
510529
};
511530

@@ -534,9 +553,9 @@ Schema.prototype.omit = function(paths, options) {
534553
if (!Array.isArray(paths)) {
535554
throw new MongooseError(
536555
'Schema#omit() only accepts an array argument, ' +
537-
'got "' +
538-
typeof paths +
539-
'"'
556+
'got "' +
557+
typeof paths +
558+
'"'
540559
);
541560
}
542561

@@ -667,6 +686,20 @@ Schema.prototype._defaultToObjectOptions = function(json) {
667686
return defaultOptions;
668687
};
669688

689+
/**
690+
* Sets the encryption type of the schema, if a value is provided, otherwise
691+
* returns the encryption type.
692+
*
693+
* @param {'csfle' | 'queryable encryption' | undefined} encryptionType plain object with paths to add, or another schema
694+
*/
695+
Schema.prototype.encryptionType = function encryptionType(encryptionType) {
696+
if (typeof encryptionType === 'string' || encryptionType === null) {
697+
this._encryptionType = encryptionType;
698+
} else {
699+
return this._encryptionType;
700+
}
701+
};
702+
670703
/**
671704
* Adds key path / schema type pairs to this schema.
672705
*
@@ -735,7 +768,7 @@ Schema.prototype.add = function add(obj, prefix) {
735768
if (
736769
key !== '_id' &&
737770
((typeof val !== 'object' && typeof val !== 'function' && !isMongooseTypeString) ||
738-
val == null)
771+
val == null)
739772
) {
740773
throw new TypeError(`Invalid schema configuration: \`${val}\` is not ` +
741774
`a valid type at path \`${key}\`. See ` +
@@ -818,15 +851,64 @@ Schema.prototype.add = function add(obj, prefix) {
818851
}
819852
}
820853
}
854+
855+
if (val.instanceOfSchema && val.encryptionType() != null) {
856+
// schema.add({ field: <instance of encrypted schema> })
857+
if (this.encryptionType() != val.encryptionType()) {
858+
throw new Error('encryptionType of a nested schema must match the encryption type of the parent schema.');
859+
}
860+
861+
for (const [encryptedField, encryptedFieldConfig] of Object.entries(val.encryptedFields)) {
862+
const path = fullPath + '.' + encryptedField;
863+
this._addEncryptedField(path, encryptedFieldConfig);
864+
}
865+
}
866+
else if (typeof val === 'object' && 'encrypt' in val) {
867+
// schema.add({ field: { type: <schema type>, encrypt: { ... }}})
868+
const { encrypt } = val;
869+
870+
if (this.encryptionType() == null) {
871+
throw new Error('encryptionType must be provided');
872+
}
873+
874+
this._addEncryptedField(fullPath, encrypt);
875+
} else {
876+
// if the field was already encrypted and we re-configure it to be unencrypted, remove
877+
// the encrypted field configuration
878+
this._removeEncryptedField(fullPath);
879+
}
821880
}
822881

823882
const aliasObj = Object.fromEntries(
824883
Object.entries(obj).map(([key]) => ([prefix + key, null]))
825884
);
826885
aliasFields(this, aliasObj);
886+
827887
return this;
828888
};
829889

890+
/**
891+
* @param {string} path
892+
* @param {object} fieldConfig
893+
* @returns
894+
*/
895+
Schema.prototype._addEncryptedField = function _addEncryptedField(path, fieldConfig) {
896+
const type = inferBSONType(this, path);
897+
if (type == null) {
898+
throw new Error('unable to determine bson type for field `' + path + '`');
899+
}
900+
901+
this.encryptedFields[path] = clone(fieldConfig);
902+
};
903+
904+
Schema.prototype._removeEncryptedField = function _removeEncryptedField(path) {
905+
delete this.encryptedFields[path];
906+
};
907+
908+
Schema.prototype._hasEncryptedFields = function _hasEncryptedFields() {
909+
return Object.keys(this.encryptedFields).length > 0;
910+
};
911+
830912
/**
831913
* Add an alias for `path`. This means getting or setting the `alias`
832914
* is equivalent to getting or setting the `path`.
@@ -1008,23 +1090,23 @@ Schema.prototype.reserved = Schema.reserved;
10081090
const reserved = Schema.reserved;
10091091
// Core object
10101092
reserved['prototype'] =
1011-
// EventEmitter
1012-
reserved.emit =
1013-
reserved.listeners =
1014-
reserved.removeListener =
1015-
1016-
// document properties and functions
1017-
reserved.collection =
1018-
reserved.errors =
1019-
reserved.get =
1020-
reserved.init =
1021-
reserved.isModified =
1022-
reserved.isNew =
1023-
reserved.populated =
1024-
reserved.remove =
1025-
reserved.save =
1026-
reserved.toObject =
1027-
reserved.validate = 1;
1093+
// EventEmitter
1094+
reserved.emit =
1095+
reserved.listeners =
1096+
reserved.removeListener =
1097+
1098+
// document properties and functions
1099+
reserved.collection =
1100+
reserved.errors =
1101+
reserved.get =
1102+
reserved.init =
1103+
reserved.isModified =
1104+
reserved.isNew =
1105+
reserved.populated =
1106+
reserved.remove =
1107+
reserved.save =
1108+
reserved.toObject =
1109+
reserved.validate = 1;
10281110
reserved.collection = 1;
10291111

10301112
/**
@@ -1104,10 +1186,10 @@ Schema.prototype.path = function(path, obj) {
11041186
}
11051187
if (typeof branch[sub] !== 'object') {
11061188
const msg = 'Cannot set nested path `' + path + '`. '
1107-
+ 'Parent path `'
1108-
+ fullPath
1109-
+ '` already set to type ' + branch[sub].name
1110-
+ '.';
1189+
+ 'Parent path `'
1190+
+ fullPath
1191+
+ '` already set to type ' + branch[sub].name
1192+
+ '.';
11111193
throw new Error(msg);
11121194
}
11131195
branch = branch[sub];
@@ -1375,6 +1457,16 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
13751457
let type = obj[options.typeKey] && (obj[options.typeKey] instanceof Function || options.typeKey !== 'type' || !obj.type.type)
13761458
? obj[options.typeKey]
13771459
: {};
1460+
1461+
if (type instanceof SchemaType) {
1462+
if (type.path === path) {
1463+
return type;
1464+
}
1465+
const clone = type.clone();
1466+
clone.path = path;
1467+
return clone;
1468+
}
1469+
13781470
let name;
13791471

13801472
if (utils.isPOJO(type) || type === 'mixed') {
@@ -1404,8 +1496,8 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
14041496
return new MongooseTypes.DocumentArray(path, cast, obj);
14051497
}
14061498
if (cast &&
1407-
cast[options.typeKey] &&
1408-
cast[options.typeKey].instanceOfSchema) {
1499+
cast[options.typeKey] &&
1500+
cast[options.typeKey].instanceOfSchema) {
14091501
if (!(cast[options.typeKey] instanceof Schema)) {
14101502
if (this.options._isMerging) {
14111503
cast[options.typeKey] = new Schema(cast[options.typeKey]);
@@ -1739,7 +1831,7 @@ Schema.prototype.hasMixedParent = function(path) {
17391831
for (let i = 0; i < subpaths.length; ++i) {
17401832
path = i > 0 ? path + '.' + subpaths[i] : subpaths[i];
17411833
if (this.paths.hasOwnProperty(path) &&
1742-
this.paths[path] instanceof MongooseTypes.Mixed) {
1834+
this.paths[path] instanceof MongooseTypes.Mixed) {
17431835
return this.paths[path];
17441836
}
17451837
}
@@ -2516,6 +2608,8 @@ Schema.prototype.remove = function(path) {
25162608

25172609
delete this.paths[name];
25182610
_deletePath(this, name);
2611+
2612+
this._removeEncryptedField(name);
25192613
}, this);
25202614
}
25212615
return this;
@@ -2611,9 +2705,9 @@ Schema.prototype.removeVirtual = function(path) {
26112705
Schema.prototype.loadClass = function(model, virtualsOnly) {
26122706
// Stop copying when hit certain base classes
26132707
if (model === Object.prototype ||
2614-
model === Function.prototype ||
2615-
model.prototype.hasOwnProperty('$isMongooseModelPrototype') ||
2616-
model.prototype.hasOwnProperty('$isMongooseDocumentPrototype')) {
2708+
model === Function.prototype ||
2709+
model.prototype.hasOwnProperty('$isMongooseModelPrototype') ||
2710+
model.prototype.hasOwnProperty('$isMongooseDocumentPrototype')) {
26172711
return this;
26182712
}
26192713

0 commit comments

Comments
 (0)