mongo/jstests/fle2/implicit_schema_validation.js

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]);