mirror of https://github.com/mongodb/mongo
479 lines
17 KiB
JavaScript
479 lines
17 KiB
JavaScript
// Verify implicit schema validation works for encrypted collections
|
|
|
|
/**
|
|
* @tags: [
|
|
* assumes_unsharded_collection,
|
|
* does_not_support_transactions,
|
|
* requires_fcv_70,
|
|
* assumes_balancer_off,
|
|
* ]
|
|
*/
|
|
import {assertDocumentValidationFailure} from "jstests/libs/doc_validation_utils.js";
|
|
|
|
const dbTest = db.getSiblingDB('implicit_schema_validation_db');
|
|
const collName = jsTestName();
|
|
|
|
const validEncryptedString = HexData(6, "060102030405060708091011121314151602");
|
|
const validEncryptedInt = HexData(6, "060102030405060708091011121314151610");
|
|
const nonEncryptedBinData = HexData(3, "060102030405060708091011121314151610");
|
|
const fle1RandomBinData = HexData(6, "020102030405060708091011121314151602");
|
|
const fle2PlaceholderBinData = HexData(6, "030102030405060708091011121314151602");
|
|
|
|
const typeMatchedArrayError = {
|
|
operator: "type",
|
|
reason: "type did match",
|
|
consideredType: "array"
|
|
};
|
|
const valueNotEncryptedError = {
|
|
operator: "fle2Encrypt",
|
|
reason: "value was not encrypted"
|
|
};
|
|
const wrongEncryptedTypeError = {
|
|
operator: "fle2Encrypt",
|
|
reason: "Queryable Encryption encrypted value has wrong type"
|
|
};
|
|
|
|
const userMalformedSchema = {
|
|
$or: [
|
|
{name: {$not: {$foo: true}}},
|
|
{name: {$type: "string"}},
|
|
]
|
|
};
|
|
|
|
const userQueryOpSchema = {
|
|
$or: [
|
|
{name: {$not: {$exists: true}}},
|
|
{name: {$type: "string"}},
|
|
]
|
|
};
|
|
|
|
const userJsonSchema = {
|
|
$jsonSchema: {
|
|
bsonType: "object",
|
|
properties: {
|
|
name: {
|
|
bsonType: "string",
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const userJsonConflictSchema = {
|
|
$jsonSchema: {
|
|
bsonType: "object",
|
|
properties: {
|
|
name: {bsonType: "string"},
|
|
firstName: {bsonType: "string"},
|
|
}
|
|
}
|
|
};
|
|
|
|
const fle1Schema = {
|
|
$jsonSchema: {
|
|
bsonType: "object",
|
|
properties: {
|
|
name: {
|
|
encrypt: {
|
|
algorithm: "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
|
|
keyId: [UUID()],
|
|
bsonType: "string",
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const sampleEncryptedFields = {
|
|
fields: [
|
|
{
|
|
path: "firstName",
|
|
keyId: UUID("11d58b8a-0c6c-4d69-a0bd-70c6d9befae9"),
|
|
bsonType: "string",
|
|
queries: {"queryType": "equality"}
|
|
},
|
|
{
|
|
path: "a.b.c",
|
|
keyId: UUID("11d58b8a-0c6c-4d69-a0bd-000000000001"),
|
|
bsonType: "int",
|
|
queries: {"queryType": "equality"}
|
|
},
|
|
{
|
|
path: "a.b.d",
|
|
keyId: UUID("11d58b8a-0c6c-4d69-a0bd-000000000002"),
|
|
bsonType: "int",
|
|
queries: {"queryType": "equality"}
|
|
},
|
|
{
|
|
path: "e.g",
|
|
keyId: UUID("11d58b8a-0c6c-4d69-a0bd-000000000003"),
|
|
bsonType: "string",
|
|
queries: {"queryType": "equality"}
|
|
},
|
|
{
|
|
path: "a.x.y",
|
|
keyId: UUID("11d58b8a-0c6c-4d69-a0bd-000000000004"),
|
|
bsonType: "string",
|
|
queries: {"queryType": "equality"}
|
|
},
|
|
]
|
|
};
|
|
|
|
/**
|
|
* Finds the sub-object starting at 'obj' that contains the property 'key'
|
|
* and has the string value 'value'
|
|
* @param {object} obj the object to traverse
|
|
* @param {string} key the attribute name to find
|
|
* @param {string} value the attribute value to find
|
|
* @returns the first subobject found that contains the target key-value pair
|
|
*/
|
|
function findContainingObject(obj, key, value) {
|
|
let queue = [obj];
|
|
while (queue.length > 0) {
|
|
let o = queue.shift();
|
|
if (o.hasOwnProperty(key) && o[key] === value) {
|
|
return o;
|
|
}
|
|
for (let prop in o) {
|
|
if (typeof o[prop] === "object" && o[prop] !== null) {
|
|
queue.push(o[prop]);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Asserts the result of a command is a document validation failure.
|
|
* If 'fleErrors' is defined, then this asserts that the errInfo in the result
|
|
* contains an "implicitFLESchema" annotation, and an annotation for each
|
|
* attribute in 'fleErrors'. Each attribute in 'fleErrors' is a pair where the key
|
|
* is the encrypted field path that is expected to cause an error, and the value is
|
|
* an object containing the expected 'operatorName' and detail fields.
|
|
*/
|
|
function assertFailedWithAnnotation(result, coll, fleErrors) {
|
|
assertDocumentValidationFailure(result, coll);
|
|
assert(result instanceof WriteResult);
|
|
const errInfo = result.getWriteError().errInfo;
|
|
const schema = findContainingObject(errInfo, "operatorName", "implicitFLESchema");
|
|
|
|
if (fleErrors) {
|
|
assert(schema,
|
|
"Result errInfo does not contain an implicitFLESchema error: " + tojson(errInfo));
|
|
} else {
|
|
assert(!schema,
|
|
"Result errInfo contains unexpected implicitFLESchema error: " + tojson(errInfo));
|
|
return;
|
|
}
|
|
assert(schema.hasOwnProperty("schemaRulesNotSatisfied"));
|
|
|
|
for (let path in fleErrors) {
|
|
const pathParts = path.split('.');
|
|
let subschema = schema;
|
|
for (let pathIdx in pathParts) {
|
|
subschema = findContainingObject(subschema, "propertyName", pathParts[pathIdx]);
|
|
assert(subschema, "No errors found for property '" + path + "': " + tojson(errInfo));
|
|
}
|
|
assert(subschema.hasOwnProperty("details"),
|
|
"No error details found for property '" + path + "': " + tojson(errInfo));
|
|
|
|
const detail =
|
|
findContainingObject(subschema.details, "operatorName", fleErrors[path].operator);
|
|
assert(detail,
|
|
"Error details for property '" + path +
|
|
"' does not contain the expected operator '" + fleErrors[path].operator +
|
|
"': " + tojson(errInfo));
|
|
|
|
for (let field in fleErrors[path]) {
|
|
if (field === "operator") {
|
|
continue;
|
|
}
|
|
const detailWithField = findContainingObject(detail, field, fleErrors[path][field]);
|
|
assert(detailWithField,
|
|
"Error details for property '" + path + "' does not contain the expected " +
|
|
field + " '" + fleErrors[path][field] + "': " + tojson(errInfo));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tests invalid inserts on encrypted collection 'coll'.
|
|
// This assumes 'coll' was created encrypted fields specified in 'sampleEncryptedFields'.
|
|
// If 'hasUserValidator' is true, this assumes it validates the optional field 'name' is a string.
|
|
function negativeTests(coll, hasUserValidator, invert = false) {
|
|
function assertExpectedResult(result, fleErrors) {
|
|
if (invert) {
|
|
assert.commandWorked(result);
|
|
} else {
|
|
assertFailedWithAnnotation(result, coll, fleErrors);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
jsTestLog("test inserting non-bindata value for encrypted field");
|
|
assertExpectedResult(coll.insert({firstName: "foo"}), {firstName: valueNotEncryptedError});
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: "bar",
|
|
d: "foo",
|
|
},
|
|
}
|
|
}),
|
|
{"a.b.c": valueNotEncryptedError, "a.b.d": valueNotEncryptedError});
|
|
|
|
jsTestLog("test path to encrypted field has arrays");
|
|
assertExpectedResult(coll.insert({a: [{b: {c: validEncryptedInt}}]}),
|
|
{"a": typeMatchedArrayError});
|
|
assertExpectedResult(coll.insert({a: {b: [{c: validEncryptedInt}]}}),
|
|
{"a.b": typeMatchedArrayError});
|
|
assertExpectedResult(coll.insert({a: {b: {c: []}}}), {"a.b.c": valueNotEncryptedError});
|
|
|
|
jsTestLog("test inserting encrypted field with BinData of incorrect subtype");
|
|
assertExpectedResult(coll.insert({firstName: nonEncryptedBinData}),
|
|
{firstName: valueNotEncryptedError});
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: nonEncryptedBinData,
|
|
d: validEncryptedInt,
|
|
},
|
|
}
|
|
}),
|
|
{"a.b.c": valueNotEncryptedError});
|
|
|
|
jsTestLog("test inserting encrypted field with incorrect Queryable Encryption subtype");
|
|
assertExpectedResult(coll.insert({firstName: fle1RandomBinData}),
|
|
{firstName: wrongEncryptedTypeError});
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: fle2PlaceholderBinData,
|
|
d: validEncryptedInt,
|
|
},
|
|
}
|
|
}),
|
|
{"a.b.c": wrongEncryptedTypeError});
|
|
|
|
jsTestLog(
|
|
"test inserting encrypted field with incorrect BSONType specifier for the unencrypted value");
|
|
assertExpectedResult(coll.insert({firstName: validEncryptedInt}),
|
|
{firstName: wrongEncryptedTypeError});
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: validEncryptedString,
|
|
d: validEncryptedInt,
|
|
},
|
|
}
|
|
}),
|
|
{"a.b.c": wrongEncryptedTypeError});
|
|
|
|
if (!hasUserValidator) {
|
|
return;
|
|
}
|
|
|
|
jsTestLog("test insert violating user-provided validator");
|
|
assertExpectedResult(coll.insert({firstName: validEncryptedString, name: 234}));
|
|
assertExpectedResult(coll.insert({firstName: nonEncryptedBinData, name: 234}),
|
|
{firstName: valueNotEncryptedError});
|
|
}
|
|
|
|
// Tests invalid updates on encrypted collection 'coll'
|
|
// This assumes 'coll' was created encrypted fields specified in 'sampleEncryptedFields'.
|
|
function negativeUpdateTests(coll, invert = false) {
|
|
function assertExpectedResult(result, fleErrors) {
|
|
if (invert) {
|
|
assert.commandWorked(result);
|
|
} else {
|
|
assertFailedWithAnnotation(result, coll, fleErrors);
|
|
}
|
|
}
|
|
|
|
// first, insert a valid document to update
|
|
assert.commandWorked(coll.insert({
|
|
test_id: 0,
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: validEncryptedInt,
|
|
d: validEncryptedInt,
|
|
},
|
|
x: {
|
|
y: validEncryptedString,
|
|
}
|
|
}
|
|
}));
|
|
|
|
jsTestLog("test updating encrypted field with invalid value");
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"firstName": "roger"}}),
|
|
{firstName: valueNotEncryptedError});
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"firstName": nonEncryptedBinData}}),
|
|
{firstName: valueNotEncryptedError});
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"firstName": fle1RandomBinData}}),
|
|
{firstName: wrongEncryptedTypeError});
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"firstName": validEncryptedInt}}),
|
|
{firstName: wrongEncryptedTypeError});
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"a.x.y": [1, 2, 3]}}),
|
|
{"a.x.y": valueNotEncryptedError});
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"a.x": {"y": 42}}}),
|
|
{"a.x": valueNotEncryptedError});
|
|
|
|
jsTestLog("test updating prefix of encrypted field with array value");
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"a.b": [1, 2, 3]}}),
|
|
{"a.b": typeMatchedArrayError});
|
|
}
|
|
|
|
// Tests valid inserts on encrypted collection 'coll'.
|
|
// This assumes 'coll' was created encrypted fields specified in 'sampleEncryptedFields'.
|
|
// If 'hasUserValidator' is true, this assumes it validates the optional field 'name' is a string.
|
|
function positiveTests(coll, hasUserValidator, invert = false) {
|
|
function assertExpectedResult(result) {
|
|
if (invert) {
|
|
assert.commandFailedWithCode(result, ErrorCodes.DocumentValidationFailure);
|
|
} else {
|
|
assert.commandWorked(result);
|
|
}
|
|
}
|
|
|
|
jsTestLog("test inserting document without any encrypted fields");
|
|
assert.commandWorked(coll.insert({}));
|
|
assert.commandWorked(coll.insert({foo: 1}));
|
|
assert.commandWorked(coll.insert({a: {foo: 1}}));
|
|
assert.commandWorked(coll.insert({a: {b: {foo: 1}, x: {foo: 1}}}));
|
|
|
|
jsTestLog("test inserting single encrypted field with valid type");
|
|
assertExpectedResult(coll.insert({firstName: validEncryptedString}));
|
|
|
|
jsTestLog("test inserting multiple encrypted fields with valid type");
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: validEncryptedInt,
|
|
d: validEncryptedInt,
|
|
},
|
|
x: {
|
|
y: validEncryptedString,
|
|
}
|
|
}
|
|
}));
|
|
|
|
jsTestLog("test inserting non-object along encrypted path");
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: "foo",
|
|
}));
|
|
assertExpectedResult(coll.insert({
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: validEncryptedInt,
|
|
d: validEncryptedInt,
|
|
},
|
|
x: "foo",
|
|
}
|
|
}));
|
|
|
|
jsTestLog("test insert satisfies user-provided validator");
|
|
assertExpectedResult(coll.insert({name: "joe", firstName: validEncryptedString}));
|
|
}
|
|
|
|
// Tests valid updates on encrypted collection 'coll'
|
|
// This assumes 'coll' was created encrypted fields specified in 'sampleEncryptedFields'.
|
|
function positiveUpdateTests(coll, invert = false) {
|
|
function assertExpectedResult(result) {
|
|
if (invert) {
|
|
assert.commandFailedWithCode(result, ErrorCodes.DocumentValidationFailure);
|
|
} else {
|
|
assert.commandWorked(result);
|
|
}
|
|
}
|
|
|
|
// first, insert a valid document to update
|
|
assert.commandWorked(coll.insert({
|
|
test_id: 0,
|
|
firstName: validEncryptedString,
|
|
a: {
|
|
b: {
|
|
c: validEncryptedInt,
|
|
d: validEncryptedInt,
|
|
},
|
|
x: {
|
|
y: validEncryptedString,
|
|
}
|
|
}
|
|
}));
|
|
|
|
jsTestLog("test unset & set of encrypted field");
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$unset: {"firstName": ""}}));
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"firstName": validEncryptedString}}));
|
|
assertExpectedResult(
|
|
coll.update({"test_id": 0}, {$set: {"a": {"x": {"y": validEncryptedString}}}}));
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"a.x": {"y": validEncryptedString}}}));
|
|
|
|
jsTestLog("test updating prefix of encrypted field with non-array value");
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"a.b": 1}}));
|
|
assertExpectedResult(coll.update({"test_id": 0}, {$set: {"a.x": {"z": 42}}}));
|
|
}
|
|
|
|
jsTestLog("test implicit validator only");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(collName, {encryptedFields: sampleEncryptedFields}));
|
|
negativeTests(dbTest[collName], false);
|
|
positiveTests(dbTest[collName], false);
|
|
|
|
jsTestLog("test implicit validator with user validator containing query ops");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(
|
|
collName, {encryptedFields: sampleEncryptedFields, validator: userQueryOpSchema}));
|
|
negativeTests(dbTest[collName], true);
|
|
positiveTests(dbTest[collName], true);
|
|
|
|
jsTestLog("test implicit validator with user validator containing json schema");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(
|
|
collName, {encryptedFields: sampleEncryptedFields, validator: userJsonSchema}));
|
|
negativeTests(dbTest[collName], true);
|
|
positiveTests(dbTest[collName], true);
|
|
|
|
jsTestLog("test user validator rules conflicting with implicit rules");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(
|
|
collName, {encryptedFields: sampleEncryptedFields, validator: userJsonConflictSchema}));
|
|
negativeTests(dbTest[collName], true);
|
|
positiveTests(dbTest[collName], true, true);
|
|
|
|
jsTestLog("test malformed user validator on encrypted collection");
|
|
dbTest[collName].drop();
|
|
assert.commandFailed(dbTest.createCollection(
|
|
collName, {encryptedFields: sampleEncryptedFields, validator: userMalformedSchema}));
|
|
|
|
jsTestLog("test FLE1 schema validator on Queryable Encryption collection");
|
|
dbTest[collName].drop();
|
|
assert.commandFailedWithCode(
|
|
dbTest.createCollection(collName,
|
|
{encryptedFields: sampleEncryptedFields, validator: fle1Schema}),
|
|
ErrorCodes.QueryFeatureNotAllowed);
|
|
|
|
jsTestLog("test collMod adding user validator on encrypted collection");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(collName, {encryptedFields: sampleEncryptedFields}));
|
|
assert.commandWorked(dbTest.runCommand({collMod: collName, validator: userQueryOpSchema}));
|
|
negativeTests(dbTest[collName], true);
|
|
positiveTests(dbTest[collName], true);
|
|
|
|
jsTestLog("test collMod adding FLE1 user validator on encrypted collection");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(collName, {encryptedFields: sampleEncryptedFields}));
|
|
assert.commandFailedWithCode(dbTest.runCommand({collMod: collName, validator: fle1Schema}),
|
|
ErrorCodes.QueryFeatureNotAllowed);
|
|
|
|
jsTestLog("test implicit validation works on updates");
|
|
dbTest[collName].drop();
|
|
assert.commandWorked(dbTest.createCollection(collName, {encryptedFields: sampleEncryptedFields}));
|
|
negativeUpdateTests(dbTest[collName]);
|
|
positiveUpdateTests(dbTest[collName]);
|