0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-12-01 09:32:32 +01:00

SERVER-45611 lazily enforce that collection validators are valid

This commit is contained in:
Ian Boros 2020-01-27 23:41:42 +00:00 committed by evergreen
parent e846a6d700
commit 582a071fa3
3 changed files with 129 additions and 28 deletions

View File

@ -0,0 +1,53 @@
/**
* Test that a node can start up despite the presence of an invalid collection validator that's on
* disk (because it was written using a prior version of the server).
*
* We restart mongod during the test and expect it to have the same data after restarting.
* @tags: [requires_persistence]
*/
(function() {
const testName = "invalid_collection_validator_at_startup";
const dbpath = MongoRunner.dataPath + testName;
const collName = "collectionWithMalformedValidator";
// Create a collection with an invalid regex using a fail point.
(function createCollectionWithMalformedValidator() {
const conn = MongoRunner.runMongod({dbpath: dbpath});
assert.neq(null, conn, "mongod was unable to start up");
const testDB = conn.getDB("test");
assert.commandWorked(testDB[collName].insert({a: "hello world"}));
assert.commandWorked(conn.adminCommand(
{configureFailPoint: 'allowSettingMalformedCollectionValidators', mode: 'alwaysOn'}));
// Invalid because '*' indicates that repetitions should be allowed but it's preceded by a
// special character.
const invalidRegex = "^*";
// Use collMod to give the collection a malformed validator.
assert.commandWorked(
testDB.runCommand({collMod: collName, validator: {email: {$regex: invalidRegex}}}));
MongoRunner.stopMongod(conn);
})();
(function startUpWithMalformedValidator() {
const conn = MongoRunner.runMongod({dbpath: dbpath, noCleanData: true});
assert.neq(null, conn, "mongod was unable to start up");
const testDB = conn.getDB("test");
// Check that we logged a startup warning.
const cmdRes = assert.commandWorked(testDB.adminCommand({getLog: "startupWarnings"}));
assert(/has malformed validator/.test(cmdRes.log));
// Be sure that inserting to the collection with the malformed validator fails.
assert.commandFailedWithCode(testDB[collName].insert({email: "hello world"}), 51091);
// Inserting to another collection should succeed.
assert.commandWorked(testDB.someOtherCollection.insert({a: 1}));
assert.eq(testDB.someOtherCollection.find().itcount(), 1);
MongoRunner.stopMongod(conn);
})();
})();

View File

@ -98,6 +98,10 @@ MONGO_FAIL_POINT_DEFINE(hangAfterCollectionInserts);
// This fail point throws a WriteConflictException after a successful call to insertRecords.
MONGO_FAIL_POINT_DEFINE(failAfterBulkLoadDocInsert);
// This fail point allows collections to be given malformed validator. A malformed validator
// will not (and cannot) be enforced but it will be persisted.
MONGO_FAIL_POINT_DEFINE(allowSettingMalformedCollectionValidators);
/**
* Checks the 'failCollectionInserts' fail point at the beginning of an insert operation to see if
* the insert should fail. Returns Status::OK if The function should proceed with the insertion.
@ -186,6 +190,27 @@ StatusWith<CollectionImpl::ValidationAction> _parseValidationAction(StringData n
MONGO_UNREACHABLE;
}
Status checkValidatorCanBeUsedOnNs(const BSONObj& validator,
const NamespaceString& nss,
const UUID& uuid) {
if (validator.isEmpty())
return Status::OK();
if (nss.isSystem() && !nss.isDropPendingNamespace()) {
return {ErrorCodes::InvalidOptions,
str::stream() << "Document validators not allowed on system collection " << nss
<< " with UUID " << uuid};
}
if (nss.isOnInternalDb()) {
return {ErrorCodes::InvalidOptions,
str::stream() << "Document validators are not allowed on collection " << nss.ns()
<< " with UUID " << uuid << " in the " << nss.db()
<< " internal database"};
}
return Status::OK();
}
} // namespace
CollectionImpl::CollectionImpl(OperationContext* opCtx,
@ -200,6 +225,7 @@ CollectionImpl::CollectionImpl(OperationContext* opCtx,
_needCappedLock(supportsDocLocking() && _recordStore && _recordStore->isCapped() &&
_ns.db() != "local"),
_indexCatalog(std::make_unique<IndexCatalogImpl>(this)),
_swValidator{nullptr},
_cappedNotifier(_recordStore && _recordStore->isCapped()
? std::make_unique<CappedInsertNotifier>()
: nullptr) {
@ -232,8 +258,19 @@ void CollectionImpl::init(OperationContext* opCtx) {
DurableCatalog::get(opCtx)->getCollectionOptions(opCtx, getCatalogId());
_collator = parseCollation(opCtx, _ns, collectionOptions.collation);
_validatorDoc = collectionOptions.validator.getOwned();
_validator = uassertStatusOK(
parseValidator(opCtx, _validatorDoc, MatchExpressionParser::kAllowAllSpecialFeatures));
// Enforce that the validator can be used on this namespace.
uassertStatusOK(checkValidatorCanBeUsedOnNs(_validatorDoc, ns(), _uuid));
// 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.
_swValidator =
parseValidator(opCtx, _validatorDoc, MatchExpressionParser::kAllowAllSpecialFeatures);
if (!_swValidator.isOK()) {
// Log an error and startup warning if the collection validator is malformed.
warning() << "Collection " << _ns
<< " has malformed validator: " << _swValidator.getStatus() << startupWarningsLog;
}
_validationAction = uassertStatusOK(_parseValidationAction(collectionOptions.validationAction));
_validationLevel = uassertStatusOK(_parseValidationLevel(collectionOptions.validationLevel));
@ -282,7 +319,12 @@ bool CollectionImpl::findDoc(OperationContext* opCtx,
}
Status CollectionImpl::checkValidation(OperationContext* opCtx, const BSONObj& document) const {
if (!_validator)
if (!_swValidator.isOK()) {
return _swValidator.getStatus();
}
const auto* const validatorMatchExpr = _swValidator.getValue().get();
if (!validatorMatchExpr)
return Status::OK();
if (_validationLevel == ValidationLevel::OFF)
@ -291,7 +333,7 @@ Status CollectionImpl::checkValidation(OperationContext* opCtx, const BSONObj& d
if (documentValidationDisabled(opCtx))
return Status::OK();
if (_validator->matchesBSON(document))
if (validatorMatchExpr->matchesBSON(document))
return Status::OK();
if (_validationAction == ValidationAction::WARN) {
@ -309,20 +351,16 @@ StatusWithMatchExpression CollectionImpl::parseValidator(
MatchExpressionParser::AllowedFeatureSet allowedFeatures,
boost::optional<ServerGlobalParams::FeatureCompatibility::Version>
maxFeatureCompatibilityVersion) const {
if (MONGO_unlikely(allowSettingMalformedCollectionValidators.shouldFail())) {
return {nullptr};
}
if (validator.isEmpty())
return {nullptr};
if (ns().isSystem() && !ns().isDropPendingNamespace()) {
return {ErrorCodes::InvalidOptions,
str::stream() << "Document validators not allowed on system collection " << ns()
<< " with UUID " << _uuid};
}
if (ns().isOnInternalDb()) {
return {ErrorCodes::InvalidOptions,
str::stream() << "Document validators are not allowed on collection " << ns().ns()
<< " with UUID " << _uuid << " in the " << ns().db()
<< " internal database"};
Status canUseValidatorInThisContext = checkValidatorCanBeUsedOnNs(validator, ns(), _uuid);
if (!canUseValidatorInThisContext.isOK()) {
return canUseValidatorInThisContext;
}
boost::intrusive_ptr<ExpressionContext> expCtx(new ExpressionContext(opCtx, _collator.get()));
@ -340,8 +378,11 @@ StatusWithMatchExpression CollectionImpl::parseValidator(
auto statusWithMatcher =
MatchExpressionParser::parse(validator, expCtx, ExtensionsCallbackNoop(), allowedFeatures);
if (!statusWithMatcher.isOK())
return statusWithMatcher.getStatus();
if (!statusWithMatcher.isOK()) {
return StatusWithMatchExpression{
statusWithMatcher.getStatus().withContext("Parsing of collection validator failed")};
}
return statusWithMatcher;
}
@ -352,7 +393,8 @@ Status CollectionImpl::insertDocumentsForOplog(OperationContext* opCtx,
dassert(opCtx->lockState()->isWriteLocked());
// Since this is only for the OpLog, we can assume these for simplicity.
invariant(!_validator);
invariant(_swValidator.isOK());
invariant(_swValidator.getValue() == nullptr);
invariant(!_indexCatalog->haveAnyIndexes());
Status status = _recordStore->insertRecords(opCtx, records, timestamps);
@ -702,7 +744,7 @@ RecordId CollectionImpl::updateDocument(OperationContext* opCtx,
}
bool CollectionImpl::updateWithDamagesSupported() const {
if (_validator)
if (!_swValidator.isOK() || _swValidator.getValue() != nullptr)
return false;
return _recordStore->updateWithDamagesSupported();
@ -866,12 +908,12 @@ Status CollectionImpl::setValidator(OperationContext* opCtx, BSONObj validatorDo
opCtx, getCatalogId(), validatorDoc, getValidationLevel(), getValidationAction());
opCtx->recoveryUnit()->onRollback([this,
oldValidator = std::move(_validator),
oldValidator = std::move(_swValidator),
oldValidatorDoc = std::move(_validatorDoc)]() mutable {
this->_validator = std::move(oldValidator);
this->_swValidator = std::move(oldValidator);
this->_validatorDoc = std::move(oldValidatorDoc);
});
_validator = std::move(statusWithMatcher.getValue());
_swValidator = std::move(statusWithMatcher);
_validatorDoc = std::move(validatorDoc);
return Status::OK();
}
@ -944,11 +986,11 @@ Status CollectionImpl::updateValidator(OperationContext* opCtx,
invariant(opCtx->lockState()->isCollectionLockedForMode(ns(), MODE_X));
opCtx->recoveryUnit()->onRollback([this,
oldValidator = std::move(_validator),
oldValidator = std::move(_swValidator),
oldValidatorDoc = std::move(_validatorDoc),
oldValidationLevel = _validationLevel,
oldValidationAction = _validationAction]() mutable {
this->_validator = std::move(oldValidator);
this->_swValidator = std::move(oldValidator);
this->_validatorDoc = std::move(oldValidatorDoc);
this->_validationLevel = oldValidationLevel;
this->_validationAction = oldValidationAction;
@ -963,7 +1005,7 @@ Status CollectionImpl::updateValidator(OperationContext* opCtx,
if (!validatorSW.isOK()) {
return validatorSW.getStatus();
}
_validator = std::move(validatorSW.getValue());
_swValidator = std::move(validatorSW.getValue());
auto levelSW = _parseValidationLevel(newLevel);
if (!levelSW.isOK()) {

View File

@ -388,11 +388,17 @@ private:
// If null, the default collation is simple binary compare.
std::unique_ptr<CollatorInterface> _collator;
// Empty means no filter.
// Empty means no validator.
BSONObj _validatorDoc;
// Points into _validatorDoc. Null means no filter.
std::unique_ptr<MatchExpression> _validator;
// The collection validator MatchExpression. This is stored as a StatusWith, as we lazily
// enforce that collection validators are well formed.
// -A non-OK Status indicates that the validator is not well formed, and any attempts to enforce
// the validator (inserts) should error.
// -A value of {nullptr} indicates that there is no validator.
// -Anything else indicates a well formed validator. The MatchExpression will maintain
// pointers into _validatorDoc.
StatusWithMatchExpression _swValidator;
ValidationAction _validationAction;
ValidationLevel _validationLevel;