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:
parent
e846a6d700
commit
582a071fa3
@ -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);
|
||||
})();
|
||||
})();
|
@ -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()) {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user