diff options
author | Nick Zolnierz <nicholas.zolnierz@mongodb.com> | 2020-04-09 10:45:06 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-04-30 20:34:39 +0000 |
commit | 00a81e7a01f1ab0450df115282b452bceac91cf7 (patch) | |
tree | 72df1fa9f7769e76fbe00b84e8e0b8ff159675fa /src | |
parent | 0832b7f863ea766d16c76f94ed9cafdf489d2b54 (diff) | |
download | mongo-00a81e7a01f1ab0450df115282b452bceac91cf7.tar.gz |
SERVER-45514 Reject document validators with encryption-related keywords if the validationAction is "warn" or validationLevel is "moderate"
(cherry picked from commit 3032eb8c2a10163bf727767efe2b73b8d60c9ecb)
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/catalog/coll_mod.cpp | 110 | ||||
-rw-r--r-- | src/mongo/db/catalog/collection.h | 2 | ||||
-rw-r--r-- | src/mongo/db/catalog/collection_impl.cpp | 67 | ||||
-rw-r--r-- | src/mongo/db/catalog/collection_impl.h | 2 | ||||
-rw-r--r-- | src/mongo/db/catalog/collection_mock.h | 2 | ||||
-rw-r--r-- | src/mongo/db/catalog/database_impl.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_parser.h | 3 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 135 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.h | 9 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/object_keywords_test.cpp | 33 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline.h | 4 |
12 files changed, 249 insertions, 133 deletions
diff --git a/src/mongo/db/catalog/coll_mod.cpp b/src/mongo/db/catalog/coll_mod.cpp index 6f2f5dcfafd..7b0b6091338 100644 --- a/src/mongo/db/catalog/coll_mod.cpp +++ b/src/mongo/db/catalog/coll_mod.cpp @@ -72,9 +72,9 @@ struct CollModRequest { BSONElement indexHidden = {}; BSONElement viewPipeLine = {}; std::string viewOn = {}; - BSONElement collValidator = {}; - std::string collValidationAction = {}; - std::string collValidationLevel = {}; + boost::optional<Collection::Validator> collValidator; + boost::optional<std::string> collValidationAction; + boost::optional<std::string> collValidationLevel; bool recordPreImages = false; }; @@ -191,15 +191,13 @@ StatusWith<CollModRequest> parseCollModRequest(OperationContext* opCtx, ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo44) { maxFeatureCompatibilityVersion = currentFCV; } - auto statusW = coll->parseValidator(opCtx, - e.Obj(), - MatchExpressionParser::kDefaultSpecialFeatures, - maxFeatureCompatibilityVersion); - if (!statusW.isOK()) { - return statusW.getStatus(); + cmr.collValidator = coll->parseValidator(opCtx, + e.Obj().getOwned(), + MatchExpressionParser::kDefaultSpecialFeatures, + maxFeatureCompatibilityVersion); + if (!cmr.collValidator->isOK()) { + return cmr.collValidator->getStatus(); } - - cmr.collValidator = e; } else if (fieldName == "validationLevel" && !isView) { auto status = coll->parseValidationLevel(e.String()); if (!status.isOK()) @@ -345,19 +343,22 @@ Status _collModInternal(OperationContext* opCtx, auto oplogEntryObj = oplogEntryBuilder.obj(); // Save both states of the CollModRequest to allow writeConflictRetries. - const CollModRequest cmrOld = statusW.getValue(); - CollModRequest cmrNew = statusW.getValue(); - - if (!cmrOld.indexHidden.eoo()) { - + CollModRequest cmrNew = std::move(statusW.getValue()); + auto viewPipeline = cmrNew.viewPipeLine; + auto viewOn = cmrNew.viewOn; + auto indexExpireAfterSeconds = cmrNew.indexExpireAfterSeconds; + auto indexHidden = cmrNew.indexHidden; + auto idx = cmrNew.idx; + + if (indexHidden) { if (serverGlobalParams.featureCompatibility.getVersion() < ServerGlobalParams::FeatureCompatibility::Version::kFullyUpgradedTo44 && - cmrOld.indexHidden.booleanSafe()) { + indexHidden.booleanSafe()) { return Status(ErrorCodes::BadValue, "Hidden indexes can only be created with FCV 4.4"); } if (coll->ns().isSystem()) return Status(ErrorCodes::BadValue, "Can't hide index on system collection"); - if (cmrOld.idx->isIdIndex()) + if (idx->isIdIndex()) return Status(ErrorCodes::BadValue, "can't hide _id index"); } @@ -367,11 +368,11 @@ Status _collModInternal(OperationContext* opCtx, // Handle collMod on a view and return early. The View Catalog handles the creation of oplog // entries for modifications on a view. if (view) { - if (!cmrOld.viewPipeLine.eoo()) - view->setPipeline(cmrOld.viewPipeLine); + if (viewPipeline) + view->setPipeline(viewPipeline); - if (!cmrOld.viewOn.empty()) - view->setViewOn(NamespaceString(dbName, cmrOld.viewOn)); + if (!viewOn.empty()) + view->setViewOn(NamespaceString(dbName, viewOn)); ViewCatalog* catalog = ViewCatalog::get(db); @@ -401,53 +402,51 @@ Status _collModInternal(OperationContext* opCtx, // Handle collMod operation type appropriately. - if (!cmrOld.indexExpireAfterSeconds.eoo() || !cmrOld.indexHidden.eoo()) { + if (indexExpireAfterSeconds || indexHidden) { BSONElement newExpireSecs = {}; BSONElement oldExpireSecs = {}; BSONElement newHidden = {}; BSONElement oldHidden = {}; + // TTL Index - if (!cmrOld.indexExpireAfterSeconds.eoo()) { - newExpireSecs = cmrOld.indexExpireAfterSeconds; - oldExpireSecs = cmrOld.idx->infoObj().getField("expireAfterSeconds"); + if (indexExpireAfterSeconds) { + newExpireSecs = indexExpireAfterSeconds; + oldExpireSecs = idx->infoObj().getField("expireAfterSeconds"); if (SimpleBSONElementComparator::kInstance.evaluate(oldExpireSecs != newExpireSecs)) { // Change the value of "expireAfterSeconds" on disk. DurableCatalog::get(opCtx)->updateTTLSetting(opCtx, coll->getCatalogId(), - cmrOld.idx->indexName(), + idx->indexName(), newExpireSecs.safeNumberLong()); } } // User wants to hide or unhide index. - if (!cmrOld.indexHidden.eoo()) { - newHidden = cmrOld.indexHidden; - oldHidden = cmrOld.idx->infoObj().getField("hidden"); + if (indexHidden) { + newHidden = indexHidden; + oldHidden = idx->infoObj().getField("hidden"); // Make sure when we set 'hidden' to false, we can remove the hidden field from // catalog. if (SimpleBSONElementComparator::kInstance.evaluate(oldHidden != newHidden)) { - DurableCatalog::get(opCtx)->updateHiddenSetting(opCtx, - coll->getCatalogId(), - cmrOld.idx->indexName(), - newHidden.booleanSafe()); + DurableCatalog::get(opCtx)->updateHiddenSetting( + opCtx, coll->getCatalogId(), idx->indexName(), newHidden.booleanSafe()); } } - - indexCollModInfo = IndexCollModInfo{ - cmrOld.indexExpireAfterSeconds.eoo() ? boost::optional<Seconds>() - : Seconds(newExpireSecs.safeNumberLong()), - cmrOld.indexExpireAfterSeconds.eoo() ? boost::optional<Seconds>() - : Seconds(oldExpireSecs.safeNumberLong()), - cmrOld.indexHidden.eoo() ? boost::optional<bool>() : newHidden.booleanSafe(), - cmrOld.indexHidden.eoo() ? boost::optional<bool>() : oldHidden.booleanSafe(), - cmrNew.idx->indexName()}; + indexCollModInfo = + IndexCollModInfo{!indexExpireAfterSeconds ? boost::optional<Seconds>() + : Seconds(newExpireSecs.safeNumberLong()), + !indexExpireAfterSeconds ? boost::optional<Seconds>() + : Seconds(oldExpireSecs.safeNumberLong()), + !indexHidden ? boost::optional<bool>() : newHidden.booleanSafe(), + !indexHidden ? boost::optional<bool>() : oldHidden.booleanSafe(), + cmrNew.idx->indexName()}; // Notify the index catalog that the definition of this index changed. This will - // invalidate the idx pointer in cmrOld. On rollback of this WUOW, the idx pointer - // in cmrNew will be invalidated and the idx pointer in cmrOld will be valid again. - cmrNew.idx = coll->getIndexCatalog()->refreshEntry(opCtx, cmrOld.idx); + // invalidate the local idx pointer. On rollback of this WUOW, the idx pointer in + // cmrNew will be invalidated and the local var idx pointer will be valid again. + cmrNew.idx = coll->getIndexCatalog()->refreshEntry(opCtx, idx); opCtx->recoveryUnit()->registerChange(std::make_unique<CollModResultChange>( oldExpireSecs, newExpireSecs, oldHidden, newHidden, result)); @@ -457,13 +456,17 @@ Status _collModInternal(OperationContext* opCtx, } } - // The Validator, ValidationAction and ValidationLevel are already parsed and must be OK. - if (!cmrNew.collValidator.eoo()) - invariant(coll->setValidator(opCtx, cmrNew.collValidator.Obj())); - if (!cmrNew.collValidationAction.empty()) - invariant(coll->setValidationAction(opCtx, cmrNew.collValidationAction)); - if (!cmrNew.collValidationLevel.empty()) - invariant(coll->setValidationLevel(opCtx, cmrNew.collValidationLevel)); + if (cmrNew.collValidator) { + coll->setValidator(opCtx, std::move(*cmrNew.collValidator)); + } + if (cmrNew.collValidationAction) + uassertStatusOKWithContext( + coll->setValidationAction(opCtx, *cmrNew.collValidationAction), + "Failed to set validationAction"); + if (cmrNew.collValidationLevel) { + uassertStatusOKWithContext(coll->setValidationLevel(opCtx, *cmrNew.collValidationLevel), + "Failed to set validationLevel"); + } if (cmrNew.recordPreImages != oldCollOptions.recordPreImages) { coll->setRecordPreImages(opCtx, cmrNew.recordPreImages); @@ -471,7 +474,6 @@ Status _collModInternal(OperationContext* opCtx, // Only observe non-view collMods, as view operations are observed as operations on the // system.views collection. - auto* const opObserver = opCtx->getServiceContext()->getOpObserver(); opObserver->onCollMod( opCtx, nss, coll->uuid(), oplogEntryObj, oldCollOptions, indexCollModInfo); diff --git a/src/mongo/db/catalog/collection.h b/src/mongo/db/catalog/collection.h index 86deaf95b0a..eb33fe95582 100644 --- a/src/mongo/db/catalog/collection.h +++ b/src/mongo/db/catalog/collection.h @@ -417,7 +417,7 @@ public: * An empty validator removes all validation. * Requires an exclusive lock on the collection. */ - virtual Status setValidator(OperationContext* const opCtx, const BSONObj validator) = 0; + virtual void setValidator(OperationContext* const opCtx, Validator validator) = 0; virtual Status setValidationLevel(OperationContext* const opCtx, const StringData newLevel) = 0; virtual Status setValidationAction(OperationContext* const opCtx, diff --git a/src/mongo/db/catalog/collection_impl.cpp b/src/mongo/db/catalog/collection_impl.cpp index 93c83701c6c..f19a49252b7 100644 --- a/src/mongo/db/catalog/collection_impl.cpp +++ b/src/mongo/db/catalog/collection_impl.cpp @@ -54,6 +54,7 @@ #include "mongo/db/index/index_access_method.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/keypattern.h" +#include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/op_observer.h" #include "mongo/db/operation_context.h" @@ -292,6 +293,16 @@ void CollectionImpl::init(OperationContext* opCtx) { // Enforce that the validator can be used on this namespace. uassertStatusOK(checkValidatorCanBeUsedOnNs(validatorDoc, ns(), _uuid)); + + // Make sure to parse the action and level before the MatchExpression, since certain features + // are not supported with certain combinations of action and level. + _validationAction = uassertStatusOK(_parseValidationAction(collectionOptions.validationAction)); + _validationLevel = uassertStatusOK(_parseValidationLevel(collectionOptions.validationLevel)); + if (collectionOptions.recordPreImages) { + uassertStatusOK(validatePreImageRecording(opCtx, _ns)); + _recordPreImages = true; + } + // Store the result (OK / error) of parsing the validator, but do not enforce that the result is // OK. This is intentional, as users may have validators on disk which were considered well // formed in older versions but not in newer versions. @@ -306,12 +317,6 @@ void CollectionImpl::init(OperationContext* opCtx) { "namespace"_attr = _ns, "validatorStatus"_attr = _validator.getStatus()); } - _validationAction = uassertStatusOK(_parseValidationAction(collectionOptions.validationAction)); - _validationLevel = uassertStatusOK(_parseValidationLevel(collectionOptions.validationLevel)); - if (collectionOptions.recordPreImages) { - uassertStatusOK(validatePreImageRecording(opCtx, _ns)); - _recordPreImages = true; - } getIndexCatalog()->init(opCtx).transitional_ignore(); _initialized = true; @@ -428,6 +433,12 @@ Collection::Validator CollectionImpl::parseValidator( // validator to apply some additional checks. expCtx->isParsingCollectionValidator = true; + // If the validation action is "warn" or the level is "moderate", then disallow any encryption + // keywords. This is to prevent any plaintext data from showing up in the logs. + if (_validationAction == CollectionImpl::ValidationAction::WARN || + _validationLevel == CollectionImpl::ValidationLevel::MODERATE) + allowedFeatures &= ~MatchExpressionParser::AllowedFeatures::kEncryptKeywords; + auto statusWithMatcher = MatchExpressionParser::parse(validator, expCtx, ExtensionsCallbackNoop(), allowedFeatures); @@ -985,28 +996,19 @@ void CollectionImpl::cappedTruncateAfter(OperationContext* opCtx, RecordId end, _recordStore->cappedTruncateAfter(opCtx, end, inclusive); } -Status CollectionImpl::setValidator(OperationContext* opCtx, BSONObj validatorDoc) { +void CollectionImpl::setValidator(OperationContext* opCtx, Validator validator) { invariant(opCtx->lockState()->isCollectionLockedForMode(ns(), MODE_X)); - // Make owned early so that the parsed match expression refers to the owned object. - if (!validatorDoc.isOwned()) - validatorDoc = validatorDoc.getOwned(); - - // Note that, by the time we reach this, we should have already done a pre-parse that checks for - // banned features, so we don't need to include that check again. - auto newValidator = - parseValidator(opCtx, validatorDoc, MatchExpressionParser::kAllowAllSpecialFeatures); - if (!newValidator.isOK()) - return newValidator.getStatus(); - - DurableCatalog::get(opCtx)->updateValidator( - opCtx, getCatalogId(), validatorDoc, getValidationLevel(), getValidationAction()); + DurableCatalog::get(opCtx)->updateValidator(opCtx, + getCatalogId(), + validator.validatorDoc.getOwned(), + getValidationLevel(), + getValidationAction()); opCtx->recoveryUnit()->onRollback([this, oldValidator = std::move(_validator)]() mutable { this->_validator = std::move(oldValidator); }); - _validator = std::move(newValidator); - return Status::OK(); + _validator = std::move(validator); } StringData CollectionImpl::getValidationLevel() const { @@ -1042,6 +1044,17 @@ Status CollectionImpl::setValidationLevel(OperationContext* opCtx, StringData ne auto oldValidationLevel = _validationLevel; _validationLevel = levelSW.getValue(); + // If setting the level to 'moderate', then reparse the validator to verify that there aren't + // any incompatible keywords. + if (_validationLevel == CollectionImpl::ValidationLevel::MODERATE) { + auto allowedFeatures = MatchExpressionParser::kAllowAllSpecialFeatures; + allowedFeatures &= ~MatchExpressionParser::AllowedFeatures::kEncryptKeywords; + auto validator = parseValidator(opCtx, _validator.validatorDoc, allowedFeatures); + if (!validator.isOK()) { + return validator.getStatus(); + } + } + DurableCatalog::get(opCtx)->updateValidator(opCtx, getCatalogId(), _validator.validatorDoc, @@ -1064,6 +1077,16 @@ Status CollectionImpl::setValidationAction(OperationContext* opCtx, StringData n auto oldValidationAction = _validationAction; _validationAction = actionSW.getValue(); + // If setting the action to 'warn', then reparse the validator to verify that there aren't any + // incompatible keywords. + if (_validationAction == CollectionImpl::ValidationAction::WARN) { + auto allowedFeatures = MatchExpressionParser::kAllowAllSpecialFeatures; + allowedFeatures &= ~MatchExpressionParser::AllowedFeatures::kEncryptKeywords; + auto validator = parseValidator(opCtx, _validator.validatorDoc, allowedFeatures); + if (!validator.isOK()) { + return validator.getStatus(); + } + } DurableCatalog::get(opCtx)->updateValidator(opCtx, getCatalogId(), diff --git a/src/mongo/db/catalog/collection_impl.h b/src/mongo/db/catalog/collection_impl.h index e3828f9471c..0d5926ec534 100644 --- a/src/mongo/db/catalog/collection_impl.h +++ b/src/mongo/db/catalog/collection_impl.h @@ -247,7 +247,7 @@ public: * An empty validator removes all validation. * Requires an exclusive lock on the collection. */ - Status setValidator(OperationContext* opCtx, BSONObj validator) final; + void setValidator(OperationContext* opCtx, Validator validator) final; Status setValidationLevel(OperationContext* opCtx, StringData newLevel) final; Status setValidationAction(OperationContext* opCtx, StringData newAction) final; diff --git a/src/mongo/db/catalog/collection_mock.h b/src/mongo/db/catalog/collection_mock.h index 9e3f97f1cc5..10672b977d4 100644 --- a/src/mongo/db/catalog/collection_mock.h +++ b/src/mongo/db/catalog/collection_mock.h @@ -178,7 +178,7 @@ public: std::abort(); } - Status setValidator(OperationContext* opCtx, BSONObj validator) { + void setValidator(OperationContext* opCtx, Validator validator) { std::abort(); } diff --git a/src/mongo/db/catalog/database_impl.cpp b/src/mongo/db/catalog/database_impl.cpp index 426010cd762..974964ba5ad 100644 --- a/src/mongo/db/catalog/database_impl.cpp +++ b/src/mongo/db/catalog/database_impl.cpp @@ -934,8 +934,17 @@ Status DatabaseImpl::userCreateNS(OperationContext* opCtx, // validator to apply some additional checks. expCtx->isParsingCollectionValidator = true; - auto statusWithMatcher = - MatchExpressionParser::parse(collectionOptions.validator, std::move(expCtx)); + // If the validation action is "warn" or the level is "moderate", then disallow any + // encryption keywords. This is to prevent any plaintext data from showing up in the logs. + auto allowedFeatures = MatchExpressionParser::kDefaultSpecialFeatures; + if (collectionOptions.validationAction == "warn" || + collectionOptions.validationLevel == "moderate") + allowedFeatures &= ~MatchExpressionParser::AllowedFeatures::kEncryptKeywords; + + auto statusWithMatcher = MatchExpressionParser::parse(collectionOptions.validator, + std::move(expCtx), + ExtensionsCallbackNoop(), + allowedFeatures); // We check the status of the parse to see if there are any banned features, but we don't // actually need the result for now. diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index a06e07b82d7..42ee487c408 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -394,7 +394,7 @@ StatusWithMatchExpression parseJSONSchema(StringData name, } return JSONSchemaParser::parse( - expCtx, elem.Obj(), internalQueryIgnoreUnknownJSONSchemaKeywords.load()); + expCtx, elem.Obj(), allowedFeatures, internalQueryIgnoreUnknownJSONSchemaKeywords.load()); } template <class T> diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h index b4048949055..466fc089eac 100644 --- a/src/mongo/db/matcher/expression_parser.h +++ b/src/mongo/db/matcher/expression_parser.h @@ -99,13 +99,14 @@ public: kJavascript = 1 << 2, kExpr = 1 << 3, kJSONSchema = 1 << 4, + kEncryptKeywords = 1 << 5, }; using AllowedFeatureSet = unsigned long long; static constexpr AllowedFeatureSet kBanAllSpecialFeatures = 0; static constexpr AllowedFeatureSet kAllowAllSpecialFeatures = std::numeric_limits<unsigned long long>::max(); static constexpr AllowedFeatureSet kDefaultSpecialFeatures = - AllowedFeatures::kExpr | AllowedFeatures::kJSONSchema; + AllowedFeatures::kExpr | AllowedFeatures::kJSONSchema | AllowedFeatures::kEncryptKeywords; /** * Parses PathAcceptingKeyword from 'typeElem'. Returns 'defaultKeyword' if 'typeElem' diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index 4cdc0ba0502..e7d8a0960c2 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -67,6 +67,7 @@ namespace mongo { using PatternSchema = InternalSchemaAllowedPropertiesMatchExpression::PatternSchema; using Pattern = InternalSchemaAllowedPropertiesMatchExpression::Pattern; +using AllowedFeatureSet = MatchExpressionParser::AllowedFeatureSet; namespace { @@ -96,6 +97,7 @@ constexpr StringData kNamePlaceholder = "i"_sd; StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, BSONObj schema, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords); /** @@ -305,6 +307,7 @@ template <class T> StatusWithMatchExpression parseLogicalKeyword(const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, BSONElement logicalElement, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { if (logicalElement.type() != BSONType::Array) { return {ErrorCodes::TypeMismatch, @@ -328,7 +331,8 @@ StatusWithMatchExpression parseLogicalKeyword(const boost::intrusive_ptr<Express << elem.type()}; } - auto nestedSchemaMatch = _parse(expCtx, path, elem.embeddedObject(), ignoreUnknownKeywords); + auto nestedSchemaMatch = + _parse(expCtx, path, elem.embeddedObject(), allowedFeatures, ignoreUnknownKeywords); if (!nestedSchemaMatch.isOK()) { return nestedSchemaMatch.getStatus(); } @@ -462,6 +466,7 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC BSONElement propertiesElt, InternalSchemaTypeExpression* typeExpr, const StringDataSet& requiredProperties, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { if (propertiesElt.type() != BSONType::Object) { return {Status(ErrorCodes::TypeMismatch, @@ -482,6 +487,7 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC auto nestedSchemaMatch = _parse(expCtx, property.fieldNameStringData(), property.embeddedObject(), + allowedFeatures, ignoreUnknownKeywords); if (!nestedSchemaMatch.isOK()) { return nestedSchemaMatch.getStatus(); @@ -522,6 +528,7 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC StatusWith<std::vector<PatternSchema>> parsePatternProperties( const boost::intrusive_ptr<ExpressionContext>& expCtx, BSONElement patternPropertiesElt, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { std::vector<PatternSchema> patternProperties; if (!patternPropertiesElt) { @@ -547,8 +554,11 @@ StatusWith<std::vector<PatternSchema>> parsePatternProperties( // Parse the nested schema using a placeholder as the path, since we intend on using the // resulting match expression inside an ExpressionWithPlaceholder. - auto nestedSchemaMatch = - _parse(expCtx, kNamePlaceholder, patternSchema.embeddedObject(), ignoreUnknownKeywords); + auto nestedSchemaMatch = _parse(expCtx, + kNamePlaceholder, + patternSchema.embeddedObject(), + allowedFeatures, + ignoreUnknownKeywords); if (!nestedSchemaMatch.isOK()) { return nestedSchemaMatch.getStatus(); } @@ -565,6 +575,7 @@ StatusWith<std::vector<PatternSchema>> parsePatternProperties( StatusWithMatchExpression parseAdditionalProperties( const boost::intrusive_ptr<ExpressionContext>& expCtx, BSONElement additionalPropertiesElt, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { if (!additionalPropertiesElt) { // The absence of the 'additionalProperties' keyword is identical in meaning to the presence @@ -590,8 +601,11 @@ StatusWithMatchExpression parseAdditionalProperties( // Parse the nested schema using a placeholder as the path, since we intend on using the // resulting match expression inside an ExpressionWithPlaceholder. - auto nestedSchemaMatch = _parse( - expCtx, kNamePlaceholder, additionalPropertiesElt.embeddedObject(), ignoreUnknownKeywords); + auto nestedSchemaMatch = _parse(expCtx, + kNamePlaceholder, + additionalPropertiesElt.embeddedObject(), + allowedFeatures, + ignoreUnknownKeywords); if (!nestedSchemaMatch.isOK()) { return nestedSchemaMatch.getStatus(); } @@ -610,6 +624,7 @@ StatusWithMatchExpression parseAllowedProperties( BSONElement patternPropertiesElt, BSONElement additionalPropertiesElt, InternalSchemaTypeExpression* typeExpr, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { // Collect the set of properties named by the 'properties' keyword. StringDataSet propertyNames; @@ -621,14 +636,14 @@ StatusWithMatchExpression parseAllowedProperties( propertyNames.insert(propertyNamesVec.begin(), propertyNamesVec.end()); } - auto patternProperties = - parsePatternProperties(expCtx, patternPropertiesElt, ignoreUnknownKeywords); + auto patternProperties = parsePatternProperties( + expCtx, patternPropertiesElt, allowedFeatures, ignoreUnknownKeywords); if (!patternProperties.isOK()) { return patternProperties.getStatus(); } - auto otherwiseExpr = - parseAdditionalProperties(expCtx, additionalPropertiesElt, ignoreUnknownKeywords); + auto otherwiseExpr = parseAdditionalProperties( + expCtx, additionalPropertiesElt, allowedFeatures, ignoreUnknownKeywords); if (!otherwiseExpr.isOK()) { return otherwiseExpr.getStatus(); } @@ -694,11 +709,12 @@ StatusWithMatchExpression translateSchemaDependency( const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, BSONElement dependency, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { invariant(dependency.type() == BSONType::Object); auto nestedSchemaMatch = - _parse(expCtx, path, dependency.embeddedObject(), ignoreUnknownKeywords); + _parse(expCtx, path, dependency.embeddedObject(), allowedFeatures, ignoreUnknownKeywords); if (!nestedSchemaMatch.isOK()) { return nestedSchemaMatch.getStatus(); } @@ -776,6 +792,7 @@ StatusWithMatchExpression translatePropertyDependency(StringData path, BSONEleme StatusWithMatchExpression parseDependencies(const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, BSONElement dependencies, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { if (dependencies.type() != BSONType::Object) { return {ErrorCodes::TypeMismatch, @@ -795,7 +812,8 @@ StatusWithMatchExpression parseDependencies(const boost::intrusive_ptr<Expressio } auto dependencyExpr = (dependency.type() == BSONType::Object) - ? translateSchemaDependency(expCtx, path, dependency, ignoreUnknownKeywords) + ? translateSchemaDependency( + expCtx, path, dependency, allowedFeatures, ignoreUnknownKeywords) : translatePropertyDependency(path, dependency); if (!dependencyExpr.isOK()) { return dependencyExpr.getStatus(); @@ -833,6 +851,7 @@ StatusWith<boost::optional<long long>> parseItems( const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, BSONElement itemsElt, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords, InternalSchemaTypeExpression* typeExpr, AndMatchExpression* andExpr) { @@ -854,8 +873,11 @@ StatusWith<boost::optional<long long>> parseItems( // We want to make an ExpressionWithPlaceholder for $_internalSchemaMatchArrayIndex, // so we use our default placeholder as the path. - auto parsedSubschema = - _parse(expCtx, kNamePlaceholder, subschema.embeddedObject(), ignoreUnknownKeywords); + auto parsedSubschema = _parse(expCtx, + kNamePlaceholder, + subschema.embeddedObject(), + allowedFeatures, + ignoreUnknownKeywords); if (!parsedSubschema.isOK()) { return parsedSubschema.getStatus(); } @@ -879,8 +901,11 @@ StatusWith<boost::optional<long long>> parseItems( // When "items" is an object, generate a single AllElemMatchFromIndex that applies to every // element in the array to match. The parsed expression is intended for an // ExpressionWithPlaceholder, so we use the default placeholder as the path. - auto nestedItemsSchema = - _parse(expCtx, kNamePlaceholder, itemsElt.embeddedObject(), ignoreUnknownKeywords); + auto nestedItemsSchema = _parse(expCtx, + kNamePlaceholder, + itemsElt.embeddedObject(), + allowedFeatures, + ignoreUnknownKeywords); if (!nestedItemsSchema.isOK()) { return nestedItemsSchema.getStatus(); } @@ -910,6 +935,7 @@ Status parseAdditionalItems(const boost::intrusive_ptr<ExpressionContext>& expCt StringData path, BSONElement additionalItemsElt, boost::optional<long long> startIndexForAdditionalItems, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords, InternalSchemaTypeExpression* typeExpr, AndMatchExpression* andExpr) { @@ -924,8 +950,11 @@ Status parseAdditionalItems(const boost::intrusive_ptr<ExpressionContext>& expCt emptyPlaceholder, std::make_unique<AlwaysFalseMatchExpression>()); } } else if (additionalItemsElt.type() == BSONType::Object) { - auto parsedOtherwiseExpr = _parse( - expCtx, kNamePlaceholder, additionalItemsElt.embeddedObject(), ignoreUnknownKeywords); + auto parsedOtherwiseExpr = _parse(expCtx, + kNamePlaceholder, + additionalItemsElt.embeddedObject(), + allowedFeatures, + ignoreUnknownKeywords); if (!parsedOtherwiseExpr.isOK()) { return parsedOtherwiseExpr.getStatus(); } @@ -957,12 +986,14 @@ Status parseAdditionalItems(const boost::intrusive_ptr<ExpressionContext>& expCt Status parseItemsAndAdditionalItems(StringMap<BSONElement>& keywordMap, const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords, InternalSchemaTypeExpression* typeExpr, AndMatchExpression* andExpr) { boost::optional<long long> startIndexForAdditionalItems; if (auto itemsElt = keywordMap[JSONSchemaParser::kSchemaItemsKeyword]) { - auto index = parseItems(expCtx, path, itemsElt, ignoreUnknownKeywords, typeExpr, andExpr); + auto index = parseItems( + expCtx, path, itemsElt, allowedFeatures, ignoreUnknownKeywords, typeExpr, andExpr); if (!index.isOK()) { return index.getStatus(); } @@ -974,6 +1005,7 @@ Status parseItemsAndAdditionalItems(StringMap<BSONElement>& keywordMap, path, additionalItemsElt, startIndexForAdditionalItems, + allowedFeatures, ignoreUnknownKeywords, typeExpr, andExpr); @@ -996,10 +1028,11 @@ Status translateLogicalKeywords(StringMap<BSONElement>& keywordMap, const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, AndMatchExpression* andExpr, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { if (auto allOfElt = keywordMap[JSONSchemaParser::kSchemaAllOfKeyword]) { - auto allOfExpr = - parseLogicalKeyword<AndMatchExpression>(expCtx, path, allOfElt, ignoreUnknownKeywords); + auto allOfExpr = parseLogicalKeyword<AndMatchExpression>( + expCtx, path, allOfElt, allowedFeatures, ignoreUnknownKeywords); if (!allOfExpr.isOK()) { return allOfExpr.getStatus(); } @@ -1007,8 +1040,8 @@ Status translateLogicalKeywords(StringMap<BSONElement>& keywordMap, } if (auto anyOfElt = keywordMap[JSONSchemaParser::kSchemaAnyOfKeyword]) { - auto anyOfExpr = - parseLogicalKeyword<OrMatchExpression>(expCtx, path, anyOfElt, ignoreUnknownKeywords); + auto anyOfExpr = parseLogicalKeyword<OrMatchExpression>( + expCtx, path, anyOfElt, allowedFeatures, ignoreUnknownKeywords); if (!anyOfExpr.isOK()) { return anyOfExpr.getStatus(); } @@ -1017,7 +1050,7 @@ Status translateLogicalKeywords(StringMap<BSONElement>& keywordMap, if (auto oneOfElt = keywordMap[JSONSchemaParser::kSchemaOneOfKeyword]) { auto oneOfExpr = parseLogicalKeyword<InternalSchemaXorMatchExpression>( - expCtx, path, oneOfElt, ignoreUnknownKeywords); + expCtx, path, oneOfElt, allowedFeatures, ignoreUnknownKeywords); if (!oneOfExpr.isOK()) { return oneOfExpr.getStatus(); } @@ -1032,7 +1065,8 @@ Status translateLogicalKeywords(StringMap<BSONElement>& keywordMap, << notElt.type()}; } - auto parsedExpr = _parse(expCtx, path, notElt.embeddedObject(), ignoreUnknownKeywords); + auto parsedExpr = + _parse(expCtx, path, notElt.embeddedObject(), allowedFeatures, ignoreUnknownKeywords); if (!parsedExpr.isOK()) { return parsedExpr.getStatus(); } @@ -1066,6 +1100,7 @@ Status translateLogicalKeywords(StringMap<BSONElement>& keywordMap, Status translateArrayKeywords(StringMap<BSONElement>& keywordMap, const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords, InternalSchemaTypeExpression* typeExpr, AndMatchExpression* andExpr) { @@ -1096,7 +1131,7 @@ Status translateArrayKeywords(StringMap<BSONElement>& keywordMap, } return parseItemsAndAdditionalItems( - keywordMap, expCtx, path, ignoreUnknownKeywords, typeExpr, andExpr); + keywordMap, expCtx, path, allowedFeatures, ignoreUnknownKeywords, typeExpr, andExpr); } /** @@ -1117,6 +1152,7 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, StringData path, InternalSchemaTypeExpression* typeExpr, AndMatchExpression* andExpr, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { StringDataSet requiredProperties; if (auto requiredElt = keywordMap[JSONSchemaParser::kSchemaRequiredKeyword]) { @@ -1128,8 +1164,13 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, } if (auto propertiesElt = keywordMap[JSONSchemaParser::kSchemaPropertiesKeyword]) { - auto propertiesExpr = parseProperties( - expCtx, path, propertiesElt, typeExpr, requiredProperties, ignoreUnknownKeywords); + auto propertiesExpr = parseProperties(expCtx, + path, + propertiesElt, + typeExpr, + requiredProperties, + allowedFeatures, + ignoreUnknownKeywords); if (!propertiesExpr.isOK()) { return propertiesExpr.getStatus(); } @@ -1149,6 +1190,7 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, patternPropertiesElt, additionalPropertiesElt, typeExpr, + allowedFeatures, ignoreUnknownKeywords); if (!allowedPropertiesExpr.isOK()) { return allowedPropertiesExpr.getStatus(); @@ -1184,8 +1226,8 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, } if (auto dependenciesElt = keywordMap[JSONSchemaParser::kSchemaDependenciesKeyword]) { - auto dependenciesExpr = - parseDependencies(expCtx, path, dependenciesElt, ignoreUnknownKeywords); + auto dependenciesExpr = parseDependencies( + expCtx, path, dependenciesElt, allowedFeatures, ignoreUnknownKeywords); if (!dependenciesExpr.isOK()) { return dependenciesExpr.getStatus(); } @@ -1311,10 +1353,16 @@ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, Status translateEncryptionKeywords(StringMap<BSONElement>& keywordMap, const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, + AllowedFeatureSet allowedFeatures, AndMatchExpression* andExpr) { auto encryptElt = keywordMap[JSONSchemaParser::kSchemaEncryptKeyword]; auto encryptMetadataElt = keywordMap[JSONSchemaParser::kSchemaEncryptMetadataKeyword]; + if ((allowedFeatures & MatchExpressionParser::AllowedFeatures::kEncryptKeywords) == 0u && + (encryptElt || encryptMetadataElt)) + return Status(ErrorCodes::QueryFeatureNotAllowed, + "Encryption-related validator keywords are not allowed in this context"); + if (encryptElt && encryptMetadataElt) { return Status(ErrorCodes::FailedToParse, str::stream() << "Cannot specify both $jsonSchema keywords '" @@ -1400,6 +1448,7 @@ Status validateMetadataKeywords(StringMap<BSONElement>& keywordMap) { StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& expCtx, StringData path, BSONObj schema, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { // Map from JSON Schema keyword to the corresponding element from 'schema', or to an empty // BSONElement if the JSON Schema keyword is not specified. @@ -1524,25 +1573,36 @@ StatusWithMatchExpression _parse(const boost::intrusive_ptr<ExpressionContext>& return translationStatus; } - translationStatus = translateArrayKeywords( - keywordMap, expCtx, path, ignoreUnknownKeywords, typeExpr.get(), andExpr.get()); + translationStatus = translateArrayKeywords(keywordMap, + expCtx, + path, + allowedFeatures, + ignoreUnknownKeywords, + typeExpr.get(), + andExpr.get()); if (!translationStatus.isOK()) { return translationStatus; } - translationStatus = translateEncryptionKeywords(keywordMap, expCtx, path, andExpr.get()); + translationStatus = + translateEncryptionKeywords(keywordMap, expCtx, path, allowedFeatures, andExpr.get()); if (!translationStatus.isOK()) { return translationStatus; } - translationStatus = translateObjectKeywords( - keywordMap, expCtx, path, typeExpr.get(), andExpr.get(), ignoreUnknownKeywords); + translationStatus = translateObjectKeywords(keywordMap, + expCtx, + path, + typeExpr.get(), + andExpr.get(), + allowedFeatures, + ignoreUnknownKeywords); if (!translationStatus.isOK()) { return translationStatus; } - translationStatus = - translateLogicalKeywords(keywordMap, expCtx, path, andExpr.get(), ignoreUnknownKeywords); + translationStatus = translateLogicalKeywords( + keywordMap, expCtx, path, andExpr.get(), allowedFeatures, ignoreUnknownKeywords); if (!translationStatus.isOK()) { return translationStatus; } @@ -1609,6 +1669,7 @@ StatusWith<MatcherTypeSet> JSONSchemaParser::parseTypeSet( StatusWithMatchExpression JSONSchemaParser::parse( const boost::intrusive_ptr<ExpressionContext>& expCtx, BSONObj schema, + AllowedFeatureSet allowedFeatures, bool ignoreUnknownKeywords) { LOGV2_DEBUG(20728, 5, @@ -1616,7 +1677,7 @@ StatusWithMatchExpression JSONSchemaParser::parse( "schema_jsonString_JsonStringFormat_LegacyStrict"_attr = schema.jsonString(JsonStringFormat::LegacyStrict)); try { - auto translation = _parse(expCtx, ""_sd, schema, ignoreUnknownKeywords); + auto translation = _parse(expCtx, ""_sd, schema, allowedFeatures, ignoreUnknownKeywords); if (shouldLog(logv2::LogSeverity::Debug(5)) && translation.isOK()) { LOGV2_DEBUG(20729, 5, diff --git a/src/mongo/db/matcher/schema/json_schema_parser.h b/src/mongo/db/matcher/schema/json_schema_parser.h index 378b093ba09..b1fd403dac4 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.h +++ b/src/mongo/db/matcher/schema/json_schema_parser.h @@ -87,9 +87,12 @@ public: * Converts a JSON schema, represented as BSON, into a semantically equivalent match expression * tree. Returns a non-OK status if the schema is invalid or cannot be parsed. */ - static StatusWithMatchExpression parse(const boost::intrusive_ptr<ExpressionContext>& expCtx, - BSONObj schema, - bool ignoreUnknownKeywords = false); + static StatusWithMatchExpression parse( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + BSONObj schema, + MatchExpressionParser::AllowedFeatureSet allowedFeatures = + MatchExpressionParser::kAllowAllSpecialFeatures, + bool ignoreUnknownKeywords = false); /** * Builds a set of type aliases from the given type element using 'aliasMapFind'. Returns a diff --git a/src/mongo/db/matcher/schema/object_keywords_test.cpp b/src/mongo/db/matcher/schema/object_keywords_test.cpp index 31709e7ab5e..73bb4b7d7dd 100644 --- a/src/mongo/db/matcher/schema/object_keywords_test.cpp +++ b/src/mongo/db/matcher/schema/object_keywords_test.cpp @@ -870,53 +870,67 @@ TEST(JSONSchemaObjectKeywordTest, TEST(JSONSchemaObjectKeywordTest, CorrectlyIgnoresUnknownKeywordsParameterIsSet) { const auto ignoreUnknownKeywords = true; + const auto allowedFeatures = MatchExpressionParser::kAllowAllSpecialFeatures; auto schema = fromjson("{ignored_keyword: 1}"); - ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) + ASSERT_OK(JSONSchemaParser::parse( + new ExpressionContextForTest(), schema, allowedFeatures, ignoreUnknownKeywords) .getStatus()); schema = fromjson("{properties: {a: {ignored_keyword: 1}}}"); - ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) + ASSERT_OK(JSONSchemaParser::parse( + new ExpressionContextForTest(), schema, allowedFeatures, ignoreUnknownKeywords) .getStatus()); schema = fromjson("{properties: {a: {oneOf: [{ignored_keyword: {}}]}}}"); - ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) + ASSERT_OK(JSONSchemaParser::parse( + new ExpressionContextForTest(), schema, allowedFeatures, ignoreUnknownKeywords) .getStatus()); } TEST(JSONSchemaObjectKeywordTest, FailsToParseUnsupportedKeywordsWhenIgnoreUnknownParameterIsSet) { const auto ignoreUnknownKeywords = true; + const auto allowedFeatures = MatchExpressionParser::kAllowAllSpecialFeatures; - auto result = JSONSchemaParser::parse( - new ExpressionContextForTest(), fromjson("{default: {}}"), ignoreUnknownKeywords); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{default: {}}"), + allowedFeatures, + ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword 'default' is not currently supported"); result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{definitions: {numberField: {type: 'number'}}}"), + allowedFeatures, ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword 'definitions' is not currently supported"); - result = JSONSchemaParser::parse( - new ExpressionContextForTest(), fromjson("{format: 'email'}"), ignoreUnknownKeywords); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{format: 'email'}"), + allowedFeatures, + ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword 'format' is not currently supported"); - result = JSONSchemaParser::parse( - new ExpressionContextForTest(), fromjson("{id: 'someschema.json'}"), ignoreUnknownKeywords); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{id: 'someschema.json'}"), + allowedFeatures, + ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword 'id' is not currently supported"); result = JSONSchemaParser::parse(new ExpressionContextForTest(), BSON("$ref" << "#/definitions/positiveInt"), + allowedFeatures, ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword '$ref' is not currently supported"); result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{$schema: 'hyper-schema'}"), + allowedFeatures, ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword '$schema' is not currently supported"); @@ -924,6 +938,7 @@ TEST(JSONSchemaObjectKeywordTest, FailsToParseUnsupportedKeywordsWhenIgnoreUnkno result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{$schema: 'http://json-schema.org/draft-04/schema#'}"), + allowedFeatures, ignoreUnknownKeywords); ASSERT_STRING_CONTAINS(result.getStatus().reason(), "$jsonSchema keyword '$schema' is not currently supported"); diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h index 5a5c40ef9c4..8ef13836c62 100644 --- a/src/mongo/db/pipeline/pipeline.h +++ b/src/mongo/db/pipeline/pipeline.h @@ -93,7 +93,8 @@ public: static constexpr MatchExpressionParser::AllowedFeatureSet kAllowedMatcherFeatures = MatchExpressionParser::AllowedFeatures::kText | MatchExpressionParser::AllowedFeatures::kExpr | - MatchExpressionParser::AllowedFeatures::kJSONSchema; + MatchExpressionParser::AllowedFeatures::kJSONSchema | + MatchExpressionParser::AllowedFeatures::kEncryptKeywords; /** * The match expression features allowed when running a pipeline with $geoNear. @@ -102,6 +103,7 @@ public: MatchExpressionParser::AllowedFeatures::kText | MatchExpressionParser::AllowedFeatures::kExpr | MatchExpressionParser::AllowedFeatures::kJSONSchema | + MatchExpressionParser::AllowedFeatures::kEncryptKeywords | MatchExpressionParser::AllowedFeatures::kGeoNear; /** |