diff options
author | Mihai Andrei <mihai.andrei@10gen.com> | 2020-08-17 11:20:35 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-09-11 14:15:15 +0000 |
commit | 94e5a1620a00997e053f5d9cc75be65aa8b73016 (patch) | |
tree | 1617d94703923ab8df855d7e2b19f5f892347ba5 | |
parent | a70ac5e30ee48259173ad172c1ca38999871181d (diff) | |
download | mongo-94e5a1620a00997e053f5d9cc75be65aa8b73016.tar.gz |
SERVER-49447 Implement validation error generation for jsonSchema scalar/miscellaneous keywords
13 files changed, 622 insertions, 57 deletions
diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp index bbc2f4eb1cb..0175172f3b5 100644 --- a/src/mongo/db/matcher/doc_validation_error.cpp +++ b/src/mongo/db/matcher/doc_validation_error.cpp @@ -44,7 +44,11 @@ #include "mongo/db/matcher/expression_type.h" #include "mongo/db/matcher/expression_visitor.h" #include "mongo/db/matcher/match_expression_walker.h" +#include "mongo/db/matcher/schema/expression_internal_schema_fmod.h" +#include "mongo/db/matcher/schema/expression_internal_schema_max_length.h" +#include "mongo/db/matcher/schema/expression_internal_schema_min_length.h" #include "mongo/db/matcher/schema/expression_internal_schema_object_match.h" +#include "mongo/db/matcher/schema/expression_internal_schema_str_length.h" namespace mongo::doc_validation_error { namespace { @@ -356,17 +360,65 @@ public: void visit(const InternalExprEqMatchExpression* expr) final {} void visit(const InternalSchemaAllElemMatchFromIndexMatchExpression* expr) final {} void visit(const InternalSchemaAllowedPropertiesMatchExpression* expr) final {} - void visit(const InternalSchemaBinDataEncryptedTypeExpression* expr) final {} - void visit(const InternalSchemaBinDataSubTypeExpression* expr) final {} + void visit(const InternalSchemaBinDataEncryptedTypeExpression* expr) final { + static constexpr auto kNormalReason = "encrypted value has wrong type"; + _context->pushNewFrame(*expr, _context->getCurrentDocument()); + if (_context->shouldGenerateError(*expr)) { + ElementPath path(expr->path(), LeafArrayBehavior::kNoTraversal); + BSONMatchableDocument doc(_context->getCurrentDocument()); + MatchableDocument::IteratorHolder cursor(&doc, &path); + invariant(cursor->more()); + auto elem = cursor->next().element(); + // Only generate an error in the normal case since if the value exists and it is + // encrypted, in the inverted case, this node's sibling expression will generate an + // appropriate error. + if (elem.type() == BSONType::BinData && elem.binDataType() == BinDataType::Encrypt && + _context->inversion == InvertError::kNormal) { + auto& builder = _context->getCurrentObjBuilder(); + appendOperatorName(*expr->getErrorAnnotation(), &builder); + builder.append("reason", kNormalReason); + } else { + _context->setCurrentRuntimeState(RuntimeState::kNoError); + } + } + } + void visit(const InternalSchemaBinDataSubTypeExpression* expr) final { + static constexpr auto kNormalReason = "value was not encrypted"; + static constexpr auto kInvertedReason = "value was encrypted"; + _context->pushNewFrame(*expr, _context->getCurrentDocument()); + if (_context->shouldGenerateError(*expr)) { + auto& builder = _context->getCurrentObjBuilder(); + appendOperatorName(*expr->getErrorAnnotation(), &builder); + appendErrorReason(*expr, kNormalReason, kInvertedReason); + } + } void visit(const InternalSchemaCondMatchExpression* expr) final {} void visit(const InternalSchemaEqMatchExpression* expr) final {} - void visit(const InternalSchemaFmodMatchExpression* expr) final {} + void visit(const InternalSchemaFmodMatchExpression* expr) final { + static constexpr auto kNormalReason = + "considered value is not a multiple of the specified value"; + static constexpr auto kInvertedReason = + "considered value is a multiple of the specified value"; + static const std::set<BSONType> kExpectedTypes{BSONType::NumberLong, + BSONType::NumberDouble, + BSONType::NumberDecimal, + BSONType::NumberInt}; + generatePathError(*expr, + kNormalReason, + kInvertedReason, + &kExpectedTypes, + LeafArrayBehavior::kNoTraversal); + } void visit(const InternalSchemaMatchArrayIndexMatchExpression* expr) final {} void visit(const InternalSchemaMaxItemsMatchExpression* expr) final {} - void visit(const InternalSchemaMaxLengthMatchExpression* expr) final {} + void visit(const InternalSchemaMaxLengthMatchExpression* expr) final { + generateStringLengthError(*expr); + } void visit(const InternalSchemaMaxPropertiesMatchExpression* expr) final {} void visit(const InternalSchemaMinItemsMatchExpression* expr) final {} - void visit(const InternalSchemaMinLengthMatchExpression* expr) final {} + void visit(const InternalSchemaMinLengthMatchExpression* expr) final { + generateStringLengthError(*expr); + } void visit(const InternalSchemaMinPropertiesMatchExpression* expr) final {} void visit(const InternalSchemaObjectMatchExpression* expr) final { // This node should never be responsible for generating an error directly. @@ -414,9 +466,11 @@ public: void visit(const ModMatchExpression* expr) final { static constexpr auto kNormalReason = "$mod did not evaluate to expected remainder"; static constexpr auto kInvertedReason = "$mod did evaluate to expected remainder"; - static const std::set<BSONType> expectedTypes{ - NumberLong, NumberDouble, NumberDecimal, NumberInt}; - generatePathError(*expr, kNormalReason, kInvertedReason, &expectedTypes); + static const std::set<BSONType> kExpectedTypes{BSONType::NumberLong, + BSONType::NumberDouble, + BSONType::NumberDecimal, + BSONType::NumberInt}; + generatePathError(*expr, kNormalReason, kInvertedReason, &kExpectedTypes); } void visit(const NorMatchExpression* expr) final { preVisitTreeOperator(expr); @@ -442,8 +496,9 @@ public: void visit(const RegexMatchExpression* expr) final { static constexpr auto kNormalReason = "regular expression did not match"; static constexpr auto kInvertedReason = "regular expression did match"; - static const std::set<BSONType> expectedTypes{String, Symbol, RegEx}; - generatePathError(*expr, kNormalReason, kInvertedReason, &expectedTypes); + static const std::set<BSONType> kExpectedTypes{ + BSONType::String, BSONType::Symbol, BSONType::RegEx}; + generatePathError(*expr, kNormalReason, kInvertedReason, &kExpectedTypes); } void visit(const SizeMatchExpression* expr) final { static constexpr auto kNormalReason = "array length was not equal to given size"; @@ -713,6 +768,14 @@ private: } } + void generateStringLengthError(const InternalSchemaStrLengthMatchExpression& expr) { + static constexpr auto kNormalReason = "specified string length was not satisfied"; + static constexpr auto kInvertedReason = "specified string length was satisfied"; + static const std::set<BSONType> expectedTypes{BSONType::String}; + generatePathError( + expr, kNormalReason, kInvertedReason, &expectedTypes, LeafArrayBehavior::kNoTraversal); + } + ValidationErrorContext* _context; }; @@ -873,17 +936,27 @@ public: void visit(const InternalExprEqMatchExpression* expr) final {} void visit(const InternalSchemaAllElemMatchFromIndexMatchExpression* expr) final {} void visit(const InternalSchemaAllowedPropertiesMatchExpression* expr) final {} - void visit(const InternalSchemaBinDataEncryptedTypeExpression* expr) final {} - void visit(const InternalSchemaBinDataSubTypeExpression* expr) final {} + void visit(const InternalSchemaBinDataEncryptedTypeExpression* expr) final { + _context->finishCurrentError(expr); + } + void visit(const InternalSchemaBinDataSubTypeExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const InternalSchemaCondMatchExpression* expr) final {} void visit(const InternalSchemaEqMatchExpression* expr) final {} - void visit(const InternalSchemaFmodMatchExpression* expr) final {} + void visit(const InternalSchemaFmodMatchExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const InternalSchemaMatchArrayIndexMatchExpression* expr) final {} void visit(const InternalSchemaMaxItemsMatchExpression* expr) final {} - void visit(const InternalSchemaMaxLengthMatchExpression* expr) final {} + void visit(const InternalSchemaMaxLengthMatchExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const InternalSchemaMaxPropertiesMatchExpression* expr) final {} void visit(const InternalSchemaMinItemsMatchExpression* expr) final {} - void visit(const InternalSchemaMinLengthMatchExpression* expr) final {} + void visit(const InternalSchemaMinLengthMatchExpression* expr) final { + _context->finishCurrentError(expr); + } void visit(const InternalSchemaMinPropertiesMatchExpression* expr) final {} void visit(const InternalSchemaObjectMatchExpression* expr) final { _context->finishCurrentError(expr); diff --git a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp index 7de748f1f43..9af4f7f0a60 100644 --- a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp +++ b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp @@ -440,5 +440,444 @@ TEST(JSONSchemaValidation, BSONTypeNoImplicitArrayTraversal) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } + +// Scalar keywords + +// minLength +TEST(JSONSchemaValidation, BasicMinLength) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','minLength': 4}}}}"); + BSONObj document = fromjson("{'a': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'minLength'," + " 'specifiedAs': {'minLength': 4}," + " 'reason': 'specified string length was not satisfied'," + " 'consideredValue': 'foo'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinLengthNoExplicitType) { + BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'minLength': 4}}}}"); + BSONObj document = fromjson("{'a': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'minLength'," + " 'specifiedAs': {'minLength': 4}," + " 'reason': 'specified string length was not satisfied'," + " 'consideredValue': 'foo'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinLengthNonString) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','minLength': 4}}}}"); + BSONObj document = fromjson("{'a': 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'minLength', " + " 'specifiedAs': { 'minLength': 4 }, " + " 'reason': 'type did not match', " + " 'consideredType': 'int', " + " 'expectedType': 'string', " + " 'consideredValue': 1 }, " + " {'operatorName': 'type', " + " 'specifiedAs': { 'type': 'string' }, " + " 'reason': 'type did not match', " + " 'consideredValue': 1, " + " 'consideredType': 'int' }]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinLengthNested) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'properties': " + " {'b': {'type': 'string', 'minLength': 4}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b':'foo'}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'minLength'," + " 'specifiedAs': {'minLength': 4}," + " 'reason': 'specified string length was not " + "satisfied'," + " 'consideredValue': 'foo'}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinLengthAtTopLevelHasNoEffect) { + BSONObj query = fromjson("{'$nor': [{'$jsonSchema': {'minLength': 1}}]}"); + BSONObj document = fromjson("{a: 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$nor'," + " 'clausesNotSatisfied': [{'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'minLength', " + " 'specifiedAs': {'minLength': 1}, " + " 'reason': 'expression always evaluates to true'}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// maxLength +TEST(JSONSchemaValidation, BasicMaxLength) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','maxLength': 4}}}}"); + BSONObj document = fromjson("{'a': 'foo, bar, baz'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'maxLength'," + " 'specifiedAs': {'maxLength': 4}," + " 'reason': 'specified string length was not satisfied'," + " 'consideredValue': 'foo, bar, baz'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaxLengthNoExplicitType) { + BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'maxLength': 4}}}}"); + BSONObj document = fromjson("{'a': 'foo, bar, baz'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'maxLength'," + " 'specifiedAs': {'maxLength': 4}," + " 'reason': 'specified string length was not satisfied'," + " 'consideredValue': 'foo, bar, baz'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaxLengthNonString) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','maxLength': 4}}}}"); + BSONObj document = fromjson("{'a': 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'maxLength', " + " 'specifiedAs': { 'maxLength': 4 }, " + " 'reason': 'type did not match', " + " 'consideredType': 'int', " + " 'expectedType': 'string', " + " 'consideredValue': 1 }, " + " {'operatorName': 'type', " + " 'specifiedAs': { 'type': 'string' }, " + " 'reason': 'type did not match', " + " 'consideredValue': 1, " + " 'consideredType': 'int' }]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaxLengthNested) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'properties': " + " {'b': {'type': 'string', 'maxLength': 4}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 'foo, bar, baz'}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'maxLength'," + " 'specifiedAs': {'maxLength': 4}," + " 'reason': 'specified string length was not " + "satisfied'," + " 'consideredValue': 'foo, bar, baz'}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaxLengthAtTopLevelHasNoEffect) { + BSONObj query = fromjson("{'$nor': [{'$jsonSchema': {'maxLength': 1000}}]}"); + BSONObj document = fromjson("{a: 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$nor'," + " 'clausesNotSatisfied': [{'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'maxLength', " + " 'specifiedAs': {'maxLength': 1000}, " + " 'reason': 'expression always evaluates to true'}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// pattern +TEST(JSONSchemaValidation, BasicPattern) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','pattern': '^S'}}}}"); + BSONObj document = fromjson("{'a': 'slow'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'pattern'," + " 'specifiedAs': {'pattern': '^S'}," + " 'reason': 'regular expression did not match'," + " 'consideredValue': 'slow'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PatternNoExplicitType) { + BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'pattern': '^S'}}}}"); + BSONObj document = fromjson("{'a': 'slow'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'pattern'," + " 'specifiedAs': {'pattern': '^S'}," + " 'reason': 'regular expression did not match'," + " 'consideredValue': 'slow'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PatternNonString) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','pattern': '^S'}}}}"); + BSONObj document = fromjson("{'a': 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " { operatorName: 'properties', 'propertiesNotSatisfied': [" + " {propertyName: 'a', 'details': [" + " {'operatorName': 'pattern'," + " 'specifiedAs': { pattern: '^S' }, " + " 'reason': 'type did not match', " + " 'consideredType': 'int', " + " 'expectedTypes': [ 'regex', 'string', 'symbol' ], " + " 'consideredValue': 1 }, " + " {'operatorName': 'type', " + " 'specifiedAs': { 'type': 'string' }, " + " 'reason': 'type did not match', " + " 'consideredValue': 1, " + " 'consideredType': 'int'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PatternNested) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'properties': " + " {'b': {'type': 'string', 'pattern': '^S'}}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 'foo'}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'pattern'," + " 'specifiedAs': {'pattern': '^S'}," + " 'reason': 'regular expression did not match'," + " 'consideredValue': 'foo'}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PatternAtTopLevelHasNoEffect) { + BSONObj query = fromjson("{'$nor': [{'$jsonSchema': {'pattern': '^S'}}]}"); + BSONObj document = fromjson("{a: 'Slow'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$nor'," + " 'clausesNotSatisfied': [{'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'pattern', " + " 'specifiedAs': {'pattern': '^S'}, " + " 'reason': 'expression always evaluates to true'}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// multipleOf +TEST(JSONSchemaValidation, BasicMultipleOf) { + BSONObj query = + fromjson("{'$jsonSchema':{properties: {a: {type: 'number', multipleOf: 2.1}}}}"); + BSONObj document = fromjson("{'a': 1.1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', details: [" + " {'operatorName': 'multipleOf'," + " 'specifiedAs': {'multipleOf': 2.1}," + " 'reason': 'considered value is not a multiple of the specified " + "value'," + " 'consideredValue': 1.1}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleOfNoExplicitType) { + BSONObj query = fromjson("{'$jsonSchema':{properties: {a: {multipleOf: 2.1}}}}"); + BSONObj document = fromjson("{'a': 1.1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', details: [" + " {'operatorName': 'multipleOf'," + " 'specifiedAs': {'multipleOf': 2.1}," + " 'reason': 'considered value is not a multiple of the specified " + "value'," + " 'consideredValue': 1.1}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleOfNonNumeric) { + BSONObj query = + fromjson("{'$jsonSchema':{properties: {a: {type: 'number', 'multipleOf': 2.1}}}}"); + BSONObj document = fromjson("{'a': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'multipleOf', " + " 'specifiedAs': { 'multipleOf': 2.1 }, " + " 'reason': 'type did not match', " + " 'consideredType': 'string', " + " 'expectedTypes': ['decimal', 'double', 'int', 'long'], " + " 'consideredValue': 'foo' }, " + " {'operatorName': 'type', " + " 'specifiedAs': { 'type': 'number' }, " + " 'reason': 'type did not match', " + " 'consideredValue': 'foo', " + " 'consideredType': 'string' }]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleOfNested) { + BSONObj query = fromjson( + "{'$jsonSchema': {" + " 'properties': {" + " 'a': {'properties': {'b': {'multipleOf': 2.1}}}}}}}"); + BSONObj document = fromjson("{'a': {'b': 1}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', " + " 'details':" + " [{'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'b', " + " 'details': [" + " {'operatorName': 'multipleOf'," + " 'specifiedAs': {'multipleOf': 2.1}," + " 'reason': 'considered value is not a multiple of " + "the specified value'," + " 'consideredValue': 1}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleOfAtTopLevelHasNoEffect) { + BSONObj query = fromjson("{'$nor': [{'$jsonSchema': {'multipleOf': 1}}]}"); + BSONObj document = fromjson("{a: 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$nor'," + " 'clausesNotSatisfied': [{'index': 0, 'details': " + " {'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'multipleOf', " + " 'specifiedAs': {'multipleOf': 1}, " + " 'reason': 'expression always evaluates to true'}]}}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// encrypt +TEST(JSONSchemaValidation, BasicEncrypt) { + BSONObj query = + fromjson("{'$jsonSchema': {bsonType: 'object', properties: {a: {encrypt: {}}}}}"); + BSONObj document = BSON("a" << BSONBinData("abc", 3, BinDataType::BinDataGeneral)); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', details: [" + " {'operatorName': 'encrypt'," + " 'reason': 'value was not encrypted'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, EncryptWithSubtypeFailsBecauseNotEncrypted) { + BSONObj query = fromjson( + "{'$jsonSchema':" + "{bsonType: 'object', properties: {a: {encrypt: {bsonType: 'number'}}}}}"); + BSONObj document = BSON("a" + << "foo"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', details: [" + " {'operatorName': 'encrypt'," + " 'reason': 'value was not encrypted'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, EncryptWithSubtypeFailsDueToMismatchedSubtype) { + BSONObj query = fromjson( + "{'$jsonSchema':" + "{bsonType: 'object', properties: {a: {encrypt: {bsonType: 'number'}}}}}"); + FleBlobHeader blob; + blob.fleBlobSubtype = FleBlobSubtype::Deterministic; + memset(blob.keyUUID, 0, sizeof(blob.keyUUID)); + blob.originalBsonType = BSONType::String; + + BSONObj document = BSON("a" << BSONBinData(reinterpret_cast<const void*>(&blob), + sizeof(FleBlobHeader), + BinDataType::Encrypt)); + + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', details: [" + " {'operatorName': 'encrypt'," + " 'reason': 'encrypted value has wrong type'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp index 6362c3e8d41..e86e4d0240d 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp +++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.cpp @@ -37,10 +37,12 @@ namespace mongo { -InternalSchemaFmodMatchExpression::InternalSchemaFmodMatchExpression(StringData path, - Decimal128 divisor, - Decimal128 remainder) - : LeafMatchExpression(MatchType::INTERNAL_SCHEMA_FMOD, path), +InternalSchemaFmodMatchExpression::InternalSchemaFmodMatchExpression( + StringData path, + Decimal128 divisor, + Decimal128 remainder, + clonable_ptr<ErrorAnnotation> annotation) + : LeafMatchExpression(MatchType::INTERNAL_SCHEMA_FMOD, path, std::move(annotation)), _divisor(divisor), _remainder(remainder) { uassert(ErrorCodes::BadValue, "divisor cannot be 0", !divisor.isZero()); diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h index fd16fa9e220..ae147f3a4fe 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_fmod.h @@ -39,11 +39,15 @@ namespace mongo { */ class InternalSchemaFmodMatchExpression final : public LeafMatchExpression { public: - InternalSchemaFmodMatchExpression(StringData path, Decimal128 divisor, Decimal128 remainder); + InternalSchemaFmodMatchExpression(StringData path, + Decimal128 divisor, + Decimal128 remainder, + clonable_ptr<ErrorAnnotation> annotation = nullptr); std::unique_ptr<MatchExpression> shallowClone() const final { std::unique_ptr<InternalSchemaFmodMatchExpression> m = - std::make_unique<InternalSchemaFmodMatchExpression>(path(), _divisor, _remainder); + std::make_unique<InternalSchemaFmodMatchExpression>( + path(), _divisor, _remainder, _errorAnnotation); if (getTag()) { m->setTag(getTag()->clone()); } diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h index 4077b43f5f3..ce8cf2a2931 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_max_items.h @@ -40,9 +40,14 @@ namespace mongo { class InternalSchemaMaxItemsMatchExpression final : public InternalSchemaNumArrayItemsMatchExpression { public: - InternalSchemaMaxItemsMatchExpression(StringData path, long long numItems) - : InternalSchemaNumArrayItemsMatchExpression( - INTERNAL_SCHEMA_MAX_ITEMS, path, numItems, "$_internalSchemaMaxItems"_sd) {} + InternalSchemaMaxItemsMatchExpression(StringData path, + long long numItems, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : InternalSchemaNumArrayItemsMatchExpression(INTERNAL_SCHEMA_MAX_ITEMS, + path, + numItems, + "$_internalSchemaMaxItems"_sd, + std::move(annotation)) {} bool matchesArray(const BSONObj& anArray, MatchDetails* details) const final { return (anArray.nFields() <= numItems()); diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_max_length.h b/src/mongo/db/matcher/schema/expression_internal_schema_max_length.h index eb26a28f897..abe98806e26 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_max_length.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_max_length.h @@ -37,10 +37,14 @@ namespace mongo { class InternalSchemaMaxLengthMatchExpression final : public InternalSchemaStrLengthMatchExpression { public: - InternalSchemaMaxLengthMatchExpression(StringData path, long long strLen) - : InternalSchemaStrLengthMatchExpression( - MatchType::INTERNAL_SCHEMA_MAX_LENGTH, path, strLen, "$_internalSchemaMaxLength"_sd) { - } + InternalSchemaMaxLengthMatchExpression(StringData path, + long long strLen, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : InternalSchemaStrLengthMatchExpression(MatchType::INTERNAL_SCHEMA_MAX_LENGTH, + path, + strLen, + "$_internalSchemaMaxLength"_sd, + std::move(annotation)) {} Validator getComparator() const final { return [strLen = strLen()](int lenWithoutNullTerm) { return lenWithoutNullTerm <= strLen; }; @@ -48,7 +52,8 @@ public: std::unique_ptr<MatchExpression> shallowClone() const final { std::unique_ptr<InternalSchemaMaxLengthMatchExpression> maxLen = - std::make_unique<InternalSchemaMaxLengthMatchExpression>(path(), strLen()); + std::make_unique<InternalSchemaMaxLengthMatchExpression>( + path(), strLen(), _errorAnnotation); if (getTag()) { maxLen->setTag(getTag()->clone()); } diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h index 1e631ce59f0..50d4768458b 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_min_items.h @@ -40,9 +40,14 @@ namespace mongo { class InternalSchemaMinItemsMatchExpression final : public InternalSchemaNumArrayItemsMatchExpression { public: - InternalSchemaMinItemsMatchExpression(StringData path, long long numItems) - : InternalSchemaNumArrayItemsMatchExpression( - INTERNAL_SCHEMA_MIN_ITEMS, path, numItems, "$_internalSchemaMinItems"_sd) {} + InternalSchemaMinItemsMatchExpression(StringData path, + long long numItems, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : InternalSchemaNumArrayItemsMatchExpression(INTERNAL_SCHEMA_MIN_ITEMS, + path, + numItems, + "$_internalSchemaMinItems"_sd, + std::move(annotation)) {} bool matchesArray(const BSONObj& anArray, MatchDetails* details) const final { return (anArray.nFields() >= numItems()); diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_min_length.h b/src/mongo/db/matcher/schema/expression_internal_schema_min_length.h index eb26bac606b..d7c4b8aa06b 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_min_length.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_min_length.h @@ -37,10 +37,14 @@ namespace mongo { class InternalSchemaMinLengthMatchExpression final : public InternalSchemaStrLengthMatchExpression { public: - InternalSchemaMinLengthMatchExpression(StringData path, long long strLen) - : InternalSchemaStrLengthMatchExpression( - MatchType::INTERNAL_SCHEMA_MIN_LENGTH, path, strLen, "$_internalSchemaMinLength"_sd) { - } + InternalSchemaMinLengthMatchExpression(StringData path, + long long strLen, + clonable_ptr<ErrorAnnotation> annotation = nullptr) + : InternalSchemaStrLengthMatchExpression(MatchType::INTERNAL_SCHEMA_MIN_LENGTH, + path, + strLen, + "$_internalSchemaMinLength"_sd, + std::move(annotation)) {} Validator getComparator() const final { return [strLen = strLen()](int lenWithoutNullTerm) { return lenWithoutNullTerm >= strLen; }; @@ -48,7 +52,8 @@ public: std::unique_ptr<MatchExpression> shallowClone() const final { std::unique_ptr<InternalSchemaMinLengthMatchExpression> minLen = - std::make_unique<InternalSchemaMinLengthMatchExpression>(path(), strLen()); + std::make_unique<InternalSchemaMinLengthMatchExpression>( + path(), strLen(), _errorAnnotation); if (getTag()) { minLen->setTag(getTag()->clone()); } diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp index c6aa1cf0cca..6fde9a327cf 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp +++ b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.cpp @@ -34,8 +34,14 @@ namespace mongo { InternalSchemaNumArrayItemsMatchExpression::InternalSchemaNumArrayItemsMatchExpression( - MatchType type, StringData path, long long numItems, StringData name) - : ArrayMatchingMatchExpression(type, path), _name(name), _numItems(numItems) {} + MatchType type, + StringData path, + long long numItems, + StringData name, + clonable_ptr<ErrorAnnotation> annotation) + : ArrayMatchingMatchExpression(type, path, std::move(std::move(annotation))), + _name(name), + _numItems(numItems) {} void InternalSchemaNumArrayItemsMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const { diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h index ecc269641a0..ca6391bc821 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_num_array_items.h @@ -44,7 +44,8 @@ public: InternalSchemaNumArrayItemsMatchExpression(MatchType type, StringData path, long long numItems, - StringData name); + StringData name, + clonable_ptr<ErrorAnnotation> annotation = nullptr); virtual ~InternalSchemaNumArrayItemsMatchExpression() {} diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_str_length.cpp b/src/mongo/db/matcher/schema/expression_internal_schema_str_length.cpp index d8f961ce35b..87d6396fd0f 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_str_length.cpp +++ b/src/mongo/db/matcher/schema/expression_internal_schema_str_length.cpp @@ -35,11 +35,13 @@ namespace mongo { -InternalSchemaStrLengthMatchExpression::InternalSchemaStrLengthMatchExpression(MatchType type, - StringData path, - long long strLen, - StringData name) - : LeafMatchExpression(type, path), _name(name), _strLen(strLen) {} +InternalSchemaStrLengthMatchExpression::InternalSchemaStrLengthMatchExpression( + MatchType type, + StringData path, + long long strLen, + StringData name, + clonable_ptr<ErrorAnnotation> annotation) + : LeafMatchExpression(type, path, std::move(annotation)), _name(name), _strLen(strLen) {} void InternalSchemaStrLengthMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const { diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h b/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h index 8721a8d718a..43483be1ce3 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_str_length.h @@ -43,7 +43,8 @@ public: InternalSchemaStrLengthMatchExpression(MatchType type, StringData path, long long strLen, - StringData name); + StringData name, + clonable_ptr<ErrorAnnotation> annotation = nullptr); virtual ~InternalSchemaStrLengthMatchExpression() {} diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index 9e11324c732..7de01f1ee43 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -287,11 +287,13 @@ StatusWithMatchExpression parseLength(const boost::intrusive_ptr<ExpressionConte return parsedLength.getStatus(); } + auto annotation = doc_validation_error::createAnnotation( + expCtx, length.fieldNameStringData().toString(), length.wrap()); if (path.empty()) { - return {std::make_unique<AlwaysTrueMatchExpression>()}; + return {std::make_unique<AlwaysTrueMatchExpression>(std::move(annotation))}; } - auto expr = std::make_unique<T>(path, parsedLength.getValue()); + auto expr = std::make_unique<T>(path, parsedLength.getValue(), std::move(annotation)); return makeRestriction(expCtx, restrictionType, path, std::move(expr), typeExpr); } @@ -306,13 +308,16 @@ StatusWithMatchExpression parsePattern(const boost::intrusive_ptr<ExpressionCont << "' must be a string")}; } + auto annotation = doc_validation_error::createAnnotation( + expCtx, pattern.fieldNameStringData().toString(), pattern.wrap()); if (path.empty()) { - return {std::make_unique<AlwaysTrueMatchExpression>()}; + return {std::make_unique<AlwaysTrueMatchExpression>(std::move(annotation))}; } // JSON Schema does not allow regex flags to be specified. constexpr auto emptyFlags = ""; - auto expr = std::make_unique<RegexMatchExpression>(path, pattern.valueStringData(), emptyFlags); + auto expr = std::make_unique<RegexMatchExpression>( + path, pattern.valueStringData(), emptyFlags, std::move(annotation)); return makeRestriction(expCtx, BSONType::String, path, std::move(expr), typeExpr); } @@ -334,12 +339,14 @@ StatusWithMatchExpression parseMultipleOf(const boost::intrusive_ptr<ExpressionC << "$jsonSchema keyword '" << JSONSchemaParser::kSchemaMultipleOfKeyword << "' must have a positive value")}; } + auto annotation = doc_validation_error::createAnnotation( + expCtx, multipleOf.fieldNameStringData().toString(), multipleOf.wrap()); if (path.empty()) { - return {std::make_unique<AlwaysTrueMatchExpression>()}; + return {std::make_unique<AlwaysTrueMatchExpression>(std::move(annotation))}; } auto expr = std::make_unique<InternalSchemaFmodMatchExpression>( - path, multipleOf.numberDecimal(), Decimal128(0)); + path, multipleOf.numberDecimal(), Decimal128(0), std::move(annotation)); MatcherTypeSet restrictionType; restrictionType.allNumbers = true; @@ -1465,11 +1472,19 @@ Status translateEncryptionKeywords(StringMap<BSONElement>& keywordMap, auto encryptInfo = EncryptionInfo::parse(encryptCtxt, encryptElt.embeddedObject()); auto infoType = encryptInfo.getBsonType(); - andExpr->add(new InternalSchemaBinDataSubTypeExpression(path, BinDataType::Encrypt)); + andExpr->add(new InternalSchemaBinDataSubTypeExpression( + path, + BinDataType::Encrypt, + doc_validation_error::createAnnotation( + expCtx, encryptElt.fieldNameStringData().toString(), BSONObj()))); - if (auto typeOptional = infoType) + if (auto typeOptional = infoType) { andExpr->add(new InternalSchemaBinDataEncryptedTypeExpression( - path, typeOptional->typeSet())); + path, + typeOptional->typeSet(), + doc_validation_error::createAnnotation( + expCtx, encryptElt.fieldNameStringData().toString(), BSONObj()))); + } } catch (const AssertionException&) { return exceptionToStatus(); } @@ -1624,8 +1639,10 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& } else if (encryptElem) { // The presence of the encrypt keyword implies the restriction that the field must be // of type BinData. - typeExpr = - std::make_unique<InternalSchemaTypeExpression>(path, MatcherTypeSet(BSONType::BinData)); + typeExpr = std::make_unique<InternalSchemaTypeExpression>( + path, + MatcherTypeSet(BSONType::BinData), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnore)); } auto andExpr = std::make_unique<AndMatchExpression>( |