diff options
author | James Wahlin <james@mongodb.com> | 2017-07-03 15:33:36 -0400 |
---|---|---|
committer | James Wahlin <james@mongodb.com> | 2017-07-25 12:24:49 -0400 |
commit | 5dcaad5f137eebc1915c0fc7b5078da4aa86f915 (patch) | |
tree | 3994b41708bce7cf5cbc5b7c9ba422db77f9bfb3 /src | |
parent | 079763d2cd06776edf81f3ecf6c32ab66d1742ec (diff) | |
download | mongo-5dcaad5f137eebc1915c0fc7b5078da4aa86f915.tar.gz |
SERVER-29371 DocumentSource classes should provide auth requirements
Diffstat (limited to 'src')
25 files changed, 425 insertions, 353 deletions
diff --git a/src/mongo/db/auth/SConscript b/src/mongo/db/auth/SConscript index 07b685afa64..396c0b4b24b 100644 --- a/src/mongo/db/auth/SConscript +++ b/src/mongo/db/auth/SConscript @@ -81,7 +81,9 @@ env.Library( '$BUILD_DIR/mongo/crypto/scramauth', '$BUILD_DIR/mongo/db/catalog/document_validation', '$BUILD_DIR/mongo/db/common', + '$BUILD_DIR/mongo/db/mongod_options', '$BUILD_DIR/mongo/db/namespace_string', + '$BUILD_DIR/mongo/db/pipeline/lite_parsed_document_source', '$BUILD_DIR/mongo/db/service_context', '$BUILD_DIR/mongo/db/update/update_driver', '$BUILD_DIR/mongo/util/md5', @@ -262,6 +264,8 @@ env.CppUnitTest( 'authmocks', 'saslauth', 'authorization_session_for_test', + '$BUILD_DIR/mongo/db/pipeline/document_source', + '$BUILD_DIR/mongo/db/service_context_d_test_fixture', '$BUILD_DIR/mongo/transport/transport_layer_mock', ] ) diff --git a/src/mongo/db/auth/authorization_session.cpp b/src/mongo/db/auth/authorization_session.cpp index 6c09f5f1f1d..976fa767f3c 100644 --- a/src/mongo/db/auth/authorization_session.cpp +++ b/src/mongo/db/auth/authorization_session.cpp @@ -49,6 +49,8 @@ #include "mongo/db/client.h" #include "mongo/db/jsobj.h" #include "mongo/db/namespace_string.h" +#include "mongo/db/pipeline/aggregation_request.h" +#include "mongo/db/pipeline/lite_parsed_pipeline.h" #include "mongo/util/assert_util.h" #include "mongo/util/log.h" #include "mongo/util/mongoutils/str.h" @@ -74,11 +76,13 @@ Status checkAuthForCreateOrModifyView(AuthorizationSession* authzSession, return Status::OK(); } - // This check ignores some invalid pipeline specifications. For example, if a user specifies a - // view definition with an invalid specification like {$lookup: "blah"}, the authorization check - // will succeed but the pipeline will fail to parse later in Command::run(). + // This check performs some validation but it is not exhaustive and may allow for an invalid + // pipeline specification. In this case the authorization check will succeed but the pipeline + // will fail to parse later in Command::run(). return authzSession->checkAuthForAggregate( - viewOnNs, BSON("aggregate" << viewOnNs.coll() << "pipeline" << viewPipeline), isMongos); + viewOnNs, + BSON("aggregate" << viewOnNs.coll() << "pipeline" << viewPipeline << "cursor" << BSONObj()), + isMongos); } /** Deleter for User*. @@ -248,245 +252,6 @@ PrivilegeVector AuthorizationSession::getDefaultPrivileges() { return defaultPrivileges; } -Status AuthorizationSession::_addPrivilegesForStage(StringData db, - BSONObj stageSpec, - bool bypassDocumentValidation, - bool isMongos, - PrivilegeVector* requiredPrivileges) { - StringData stageName = stageSpec.firstElementFieldName(); - if (stageName == "$out") { - if (stageSpec.firstElementType() != BSONType::String) { - return Status(ErrorCodes::TypeMismatch, - str::stream() << "The '$out' stage must be of type String, is type " - << stageSpec.firstElementType()); - } - - NamespaceString outputNs(db, stageSpec.firstElement().valueStringData()); - if (!outputNs.isValid()) { - return Status(ErrorCodes::InvalidNamespace, - mongoutils::str::stream() << "Invalid $out target namespace, " - << outputNs.ns()); - } - - ActionSet actions; - actions.addAction(ActionType::remove); - actions.addAction(ActionType::insert); - if (bypassDocumentValidation) { - actions.addAction(ActionType::bypassDocumentValidation); - } - Privilege::addPrivilegeToPrivilegeVector( - requiredPrivileges, Privilege(ResourcePattern::forExactNamespace(outputNs), actions)); - } else if (stageName == "$lookup") { - if (stageSpec.firstElementType() != BSONType::Object) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "The '$lookup' stage must be of type Object, is type " - << stageSpec.firstElementType()); - } - - auto fromElem = stageSpec.firstElement().Obj()["from"]; - if (!fromElem) { - return Status(ErrorCodes::FailedToParse, - "$lookup argument 'from' field must be specified"); - } - - if (fromElem.type() != BSONType::String) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "$lookup argument '" << fromElem - << "' must be a string, is type " - << fromElem.type()); - } - - NamespaceString fromNs(db, fromElem.valueStringData()); - if (!fromNs.isValid()) { - return Status(ErrorCodes::InvalidNamespace, - mongoutils::str::stream() << "Invalid 'from' namespace, " << fromNs.ns()); - } - - Privilege::addPrivilegeToPrivilegeVector( - requiredPrivileges, - Privilege(ResourcePattern::forExactNamespace(fromNs), ActionType::find)); - - auto pipelineElem = stageSpec.firstElement().Obj()["pipeline"]; - if (pipelineElem) { - auto status = _addPrivilegesForPipeline( - fromNs, pipelineElem, bypassDocumentValidation, isMongos, requiredPrivileges); - if (!status.isOK()) { - return status; - } - } - } else if (stageName == "$graphLookup") { - if (stageSpec.firstElementType() != BSONType::Object) { - return Status(ErrorCodes::FailedToParse, - str::stream() - << "The '$graphLookup' stage must be of type Object, is type " - << stageSpec.firstElementType()); - } - - auto fromElem = stageSpec.firstElement().Obj()["from"]; - if (!fromElem) { - return Status(ErrorCodes::FailedToParse, - "$graphLookup argument 'from' field must be specified"); - } - - if (fromElem.type() != BSONType::String) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "$graphLookup argument '" << fromElem - << "' must be a string, is type " - << fromElem.type()); - } - - NamespaceString fromNs(db, fromElem.valueStringData()); - Privilege::addPrivilegeToPrivilegeVector( - requiredPrivileges, - Privilege(ResourcePattern::forExactNamespace(fromNs), ActionType::find)); - } else if (stageName == "$facet") { - if (stageSpec.firstElementType() != BSONType::Object) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "The '$facet' stage must be of type Object, is type " - << stageSpec.firstElementType()); - } - - for (auto&& subPipeline : stageSpec.firstElement().embeddedObject()) { - if (subPipeline.type() != BSONType::Array) { - return Status(ErrorCodes::TypeMismatch, - str::stream() << "The '$facet' field '" << subPipeline.fieldName() - << "' is expected to be of type Array, is type " - << subPipeline.type()); - } - - for (auto&& subPipeStageSpec : subPipeline.embeddedObject()) { - if (subPipeStageSpec.type() != BSONType::Object) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "argument '" << subPipeStageSpec - << "' must be an Object, is type " - << subPipeStageSpec.type()); - } - - auto status = _addPrivilegesForStage(db, - subPipeStageSpec.embeddedObject(), - bypassDocumentValidation, - isMongos, - requiredPrivileges); - if (!status.isOK()) { - return status; - } - } - } - } - - return Status::OK(); -} - -Status AuthorizationSession::_addPrivilegesForPipeline(const NamespaceString& nss, - const BSONElement& pipelineElem, - bool bypassDocumentValidation, - bool isMongos, - PrivilegeVector* requiredPrivileges) { - if (pipelineElem.type() != BSONType::Array) { - return Status(ErrorCodes::TypeMismatch, "'pipeline' must be specified as an array"); - } - - BSONObj pipeline = pipelineElem.embeddedObject(); - if (pipeline.isEmpty()) { - // The pipeline is empty, so we require only the find action. - Privilege::addPrivilegeToPrivilegeVector( - requiredPrivileges, - Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find)); - } else { - if (pipeline.firstElementType() != BSONType::Object) { - // The pipeline contains something that's not an object. - return Status(ErrorCodes::TypeMismatch, - "'pipeline' cannot contain non-object elements"); - } - - // We treat the first stage in the pipeline specially, as some aggregation stages that are - // valid initial sources have different auth requirements. - Privilege firstStagePrivilege; - BSONObj firstPipelineStage = pipeline.firstElement().embeddedObject(); - BSONElement firstStageSpec = firstPipelineStage.firstElement(); - if (str::equals("$indexStats", firstStageSpec.fieldName())) { - firstStagePrivilege = - Privilege(ResourcePattern::forExactNamespace(nss), ActionType::indexStats); - } else if (str::equals("$collStats", firstStageSpec.fieldName())) { - firstStagePrivilege = - Privilege(ResourcePattern::forExactNamespace(nss), ActionType::collStats); - } else if (str::equals("$currentOp", firstStageSpec.fieldName())) { - // Need to check the value of allUsers; if true then inprog privilege is required. - // {$currentOp: {idleConnections: <boolean|false>, allUsers: <boolean|false>}} - if (firstStageSpec.type() != BSONType::Object) { - return Status( - ErrorCodes::TypeMismatch, - str::stream() - << "$currentOp options must be specified in an object, but found: " - << typeName(firstStageSpec.type())); - } - - bool allUsers = false; - - // Check the spec for all fields named 'allUsers'. If any of them are 'true', we require - // the 'inprog' privilege. This avoids the possibility that a spec with multiple - // allUsers fields might allow an unauthorized user to view all operations. - for (auto&& elem : firstStageSpec.embeddedObject()) { - if (elem.fieldNameStringData() == "allUsers"_sd) { - if (elem.type() != BSONType::Bool) { - return Status(ErrorCodes::TypeMismatch, - str::stream() - << "The 'allUsers' parameter of the $currentOp stage " - "must be a boolean value, but found: " - << typeName(elem.type())); - } else if (elem.Bool()) { - allUsers = true; - break; - } - } - } - - // In a sharded cluster, we always need the inprog privilege to run $currentOp. - if (isMongos || allUsers) { - firstStagePrivilege = - Privilege(ResourcePattern::forClusterResource(), ActionType::inprog); - } else if (!getAuthenticatedUserNames().more()) { - // This connection is not authenticated, so we should return an error even though - // there are no privilege requirements when allUsers is false. - return Status(ErrorCodes::Unauthorized, "unauthorized"); - } - } else { - // If no source requiring an alternative permission scheme is specified then default to - // requiring find() privileges on the given namespace. - firstStagePrivilege = - Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find); - } - - // Exit early if not authorized for the pipline's input data source. This will prevent a - // malicious user, who doesn't have access to the initial document source, from consuming - // server resources needed to parse a potentially large pipeline. - if (!isAuthorizedForPrivilege(firstStagePrivilege)) { - return Status(ErrorCodes::Unauthorized, "unauthorized"); - } - - // Add additional required privileges for each stage in the pipeline. - for (auto&& stageElem : pipeline) { - if (stageElem.type() != BSONType::Object) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "argument '" << stageElem - << "' must be an Object, is type " - << stageElem.type()); - } - - auto status = _addPrivilegesForStage(nss.db(), - stageElem.embeddedObject(), - bypassDocumentValidation, - isMongos, - requiredPrivileges); - if (!status.isOK()) { - return status; - } - } - } - - return Status::OK(); -} - Status AuthorizationSession::checkAuthForAggregate(const NamespaceString& nss, const BSONObj& cmdObj, bool isMongos) { @@ -501,20 +266,59 @@ Status AuthorizationSession::checkAuthForAggregate(const NamespaceString& nss, return Status::OK(); } - PrivilegeVector privileges; - auto status = _addPrivilegesForPipeline(nss, - cmdObj["pipeline"], - shouldBypassDocumentValidationForCommand(cmdObj), - isMongos, - &privileges); - if (!status.isOK()) { - return status; + // We require at least one authenticated user when running aggregate with auth enabled. + if (!getAuthenticatedUserNames().more()) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + + auto statusWithAggRequest = AggregationRequest::parseFromBSON(nss, cmdObj); + if (!statusWithAggRequest.isOK()) { + return statusWithAggRequest.getStatus(); } + AggregationRequest aggRequest = std::move(statusWithAggRequest.getValue()); + + const auto& pipeline = aggRequest.getPipeline(); + + // If the aggregation pipeline is empty, confirm the user is authorized for find on 'nss'. + if (pipeline.empty()) { + if (!isAuthorizedForPrivilege( + Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find))) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } - if (isAuthorizedForPrivileges(privileges)) return Status::OK(); + } - return Status(ErrorCodes::Unauthorized, "unauthorized"); + // Confirm the user is authorized for the pipeline's initial document source. We confirm a user + // is authorized incrementally rather than once for the entire pipeline. This will prevent a + // malicious user, who doesn't have access to the initial document source, from consuming the + // resources needed to parse a potentially large pipeline. + auto liteParsedFirstDocumentSource = LiteParsedDocumentSource::parse(aggRequest, pipeline[0]); + if (!liteParsedFirstDocumentSource->isInitialSource() && + !isAuthorizedForPrivilege( + Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find))) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + + // We have done the work to lite parse the first stage. Given that, we check required privileges + // for it using 'liteParsedFirstDocumentSource' regardless of whether is an initial source or + // not. + if (!isAuthorizedForPrivileges(liteParsedFirstDocumentSource->requiredPrivileges(isMongos))) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + + // Confirm privileges for the remainder of the pipepline. Start with the second stage as we have + // already authorized the first. + auto pipelineIter = pipeline.begin() + 1; + + for (; pipelineIter != pipeline.end(); ++pipelineIter) { + auto liteParsedDocSource = LiteParsedDocumentSource::parse(aggRequest, *pipelineIter); + if (!isAuthorizedForPrivileges(liteParsedDocSource->requiredPrivileges(isMongos))) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + } + + return Status::OK(); } Status AuthorizationSession::checkAuthForFind(const NamespaceString& ns, bool hasTerm) { diff --git a/src/mongo/db/auth/authorization_session.h b/src/mongo/db/auth/authorization_session.h index 0161523a79b..65588ab0708 100644 --- a/src/mongo/db/auth/authorization_session.h +++ b/src/mongo/db/auth/authorization_session.h @@ -312,20 +312,6 @@ private: // lock on the admin database (to update out-of-date user privilege information). bool _isAuthorizedForPrivilege(const Privilege& privilege); - // Helper for recursively checking for privileges in an aggregation pipeline. - Status _addPrivilegesForPipeline(const NamespaceString& nss, - const BSONElement& pipelineElem, - bool bypassDocumentValidation, - bool isMongos, - PrivilegeVector* requiredPrivileges); - - // Helper for recursively checking for privileges in an aggregation stage. - Status _addPrivilegesForStage(StringData db, - BSONObj stageSpec, - bool bypassDocumentValidation, - bool isMongos, - PrivilegeVector* requiredPrivileges); - std::unique_ptr<AuthzSessionExternalState> _externalState; // A vector of impersonated UserNames and a vector of those users' RoleNames. diff --git a/src/mongo/db/auth/authorization_session_test.cpp b/src/mongo/db/auth/authorization_session_test.cpp index d6cd4d686fe..9cdbe4aa730 100644 --- a/src/mongo/db/auth/authorization_session_test.cpp +++ b/src/mongo/db/auth/authorization_session_test.cpp @@ -734,6 +734,8 @@ TEST_F(AuthorizationSessionTest, AcquireUserObtainsAndValidatesAuthenticationRes } TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineIsNotAnArray) { + authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); + BSONObj cmdObjIntPipeline = BSON("aggregate" << testFooNss.coll() << "pipeline" << 7); ASSERT_EQ(ErrorCodes::TypeMismatch, authzSession->checkAuthForAggregate(testFooNss, cmdObjIntPipeline, false)); @@ -748,6 +750,8 @@ TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineIsNotAnArra } TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineFirstStageIsNotAnObject) { + authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); + BSONObj cmdObjFirstStageInt = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSON_ARRAY(7)); ASSERT_EQ(ErrorCodes::TypeMismatch, @@ -760,7 +764,8 @@ TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineFirstStageI } TEST_F(AuthorizationSessionTest, CannotAggregateEmptyPipelineWithoutFindAction) { - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONArray()); + BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONArray() << "cursor" + << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -768,7 +773,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateEmptyPipelineWithoutFindAction) TEST_F(AuthorizationSessionTest, CanAggregateEmptyPipelineWithFindAction) { authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONArray()); + BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONArray() << "cursor" + << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -778,25 +784,28 @@ TEST_F(AuthorizationSessionTest, CannotAggregateWithoutFindActionIfFirstStageNot BSONArray pipeline = BSON_ARRAY(BSON("$limit" << 1) << BSON("$collStats" << BSONObj()) << BSON("$indexStats" << BSONObj())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } -TEST_F(AuthorizationSessionTest, CanAggregateWithFindActionIfFirstStageNotIndexOrCollStats) { +TEST_F(AuthorizationSessionTest, CannotAggregateWithFindActionIfPipelineContainsIndexOrCollStats) { authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); - BSONArray pipeline = BSON_ARRAY(BSON("$limit" << 1) << BSON("$collStats" << BSONObj()) << BSON("$indexStats" << BSONObj())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); + ASSERT_EQ(ErrorCodes::Unauthorized, + authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } TEST_F(AuthorizationSessionTest, CannotAggregateCollStatsWithoutCollStatsAction) { authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$collStats" << BSONObj())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -805,7 +814,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateCollStatsWithCollStatsAction) { authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::collStats})); BSONArray pipeline = BSON_ARRAY(BSON("$collStats" << BSONObj())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -813,7 +823,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateIndexStatsWithoutIndexStatsActio authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$indexStats" << BSONObj())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -822,7 +833,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateIndexStatsWithIndexStatsAction) { authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::indexStats})); BSONArray pipeline = BSON_ARRAY(BSON("$indexStats" << BSONObj())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -830,7 +842,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersFalseWithoutInprog authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -838,14 +851,16 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseWithoutInp authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); } TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseIfNotAuthenticatedOnMongoD) { BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); @@ -853,7 +868,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseIfNotAuthe TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseIfNotAuthenticatedOnMongoS) { BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); @@ -863,7 +879,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersTrueWithoutInpr authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -872,7 +889,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersTrueWithoutInpr authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); } @@ -882,7 +900,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersTrueWithInprogActi Privilege(ResourcePattern::forClusterResource(), {ActionType::inprog})); BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -891,7 +910,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersTrueWithInprogActi Privilege(ResourcePattern::forClusterResource(), {ActionType::inprog})); BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); } @@ -900,7 +920,8 @@ TEST_F(AuthorizationSessionTest, CannotSpoofAllUsersTrueWithoutInprogActionOnMon BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false << "allUsers" << true))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -910,7 +931,8 @@ TEST_F(AuthorizationSessionTest, CannotSpoofAllUsersTrueWithoutInprogActionOnMon BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false << "allUsers" << true))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); } @@ -920,9 +942,11 @@ TEST_F(AuthorizationSessionTest, AddPrivilegesForStageFailsIfOutNamespaceIsNotVa BSONArray pipeline = BSON_ARRAY(BSON("$out" << "")); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); - ASSERT_EQ(ErrorCodes::InvalidNamespace, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); + ASSERT_THROWS_CODE(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false).ignore(), + UserException, + ErrorCodes::InvalidNamespace); } TEST_F(AuthorizationSessionTest, CannotAggregateOutWithoutInsertAndRemoveOnTargetNamespace) { @@ -930,7 +954,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateOutWithoutInsertAndRemoveOnTarge authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$out" << testBarNss.coll())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); @@ -953,12 +978,15 @@ TEST_F(AuthorizationSessionTest, CanAggregateOutWithInsertAndRemoveOnTargetNames Privilege(testBarCollResource, {ActionType::insert, ActionType::remove})}); BSONArray pipeline = BSON_ARRAY(BSON("$out" << testBarNss.coll())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); BSONObj cmdObjNoBypassDocumentValidation = BSON( "aggregate" << testFooNss.coll() << "pipeline" << pipeline << "bypassDocumentValidation" - << false); + << false + << "cursor" + << BSONObj()); ASSERT_OK( authzSession->checkAuthForAggregate(testFooNss, cmdObjNoBypassDocumentValidation, false)); } @@ -970,9 +998,10 @@ TEST_F(AuthorizationSessionTest, Privilege(testBarCollResource, {ActionType::insert, ActionType::remove})}); BSONArray pipeline = BSON_ARRAY(BSON("$out" << testBarNss.coll())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline - << "bypassDocumentValidation" - << true); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj() + << "bypassDocumentValidation" + << true); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -986,9 +1015,10 @@ TEST_F(AuthorizationSessionTest, {ActionType::insert, ActionType::remove, ActionType::bypassDocumentValidation})}); BSONArray pipeline = BSON_ARRAY(BSON("$out" << testBarNss.coll())); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline - << "bypassDocumentValidation" - << true); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj() + << "bypassDocumentValidation" + << true); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -996,7 +1026,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnJoinedNamespa authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << testBarNss.coll()))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -1006,7 +1037,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnJoinedNamespace) { Privilege(testBarCollResource, {ActionType::find})}); BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << testBarNss.coll()))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -1018,7 +1050,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnNestedJoinedN BSONArray nestedPipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << testQuxNss.coll()))); BSONArray pipeline = BSON_ARRAY( BSON("$lookup" << BSON("from" << testBarNss.coll() << "pipeline" << nestedPipeline))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -1031,7 +1064,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnNestedJoinedNamespa BSONArray nestedPipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << testQuxNss.coll()))); BSONArray pipeline = BSON_ARRAY( BSON("$lookup" << BSON("from" << testBarNss.coll() << "pipeline" << nestedPipeline))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -1071,6 +1105,7 @@ TEST_F(AuthorizationSessionTest, CheckAuthForAggregateWithDeeplyNestedLookup) { BSONArrayBuilder pipelineBuilder(cmdBuilder.subarrayStart("pipeline")); addNestedPipeline(&pipelineBuilder, maxLookupDepth); pipelineBuilder.doneFast(); + cmdBuilder << "cursor" << BSONObj(); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdBuilder.obj(), false)); } @@ -1080,7 +1115,8 @@ TEST_F(AuthorizationSessionTest, CannotAggregateGraphLookupWithoutFindOnJoinedNa authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONArray pipeline = BSON_ARRAY(BSON("$graphLookup" << BSON("from" << testBarNss.coll()))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -1090,7 +1126,8 @@ TEST_F(AuthorizationSessionTest, CanAggregateGraphLookupWithFindOnJoinedNamespac Privilege(testBarCollResource, {ActionType::find})}); BSONArray pipeline = BSON_ARRAY(BSON("$graphLookup" << BSON("from" << testBarNss.coll()))); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } @@ -1102,7 +1139,8 @@ TEST_F(AuthorizationSessionTest, BSONArray pipeline = BSON_ARRAY(fromjson("{$facet: {lookup: [{$lookup: {from: 'bar'}}], graphLookup: " "[{$graphLookup: {from: 'qux'}}]}}")); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_EQ(ErrorCodes::Unauthorized, authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); @@ -1128,7 +1166,8 @@ TEST_F(AuthorizationSessionTest, BSONArray pipeline = BSON_ARRAY(fromjson("{$facet: {lookup: [{$lookup: {from: 'bar'}}], graphLookup: " "[{$graphLookup: {from: 'qux'}}]}}")); - BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline); + BSONObj cmdObj = + BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); } diff --git a/src/mongo/db/auth/privilege.cpp b/src/mongo/db/auth/privilege.cpp index 6e33bb54082..93c75cce9d8 100644 --- a/src/mongo/db/auth/privilege.cpp +++ b/src/mongo/db/auth/privilege.cpp @@ -45,6 +45,13 @@ void Privilege::addPrivilegeToPrivilegeVector(PrivilegeVector* privileges, privileges->push_back(privilegeToAdd); } +void Privilege::addPrivilegesToPrivilegeVector(PrivilegeVector* privileges, + const PrivilegeVector& privilegesToAdd) { + for (auto&& priv : privilegesToAdd) { + addPrivilegeToPrivilegeVector(privileges, priv); + } +} + Privilege::Privilege(const ResourcePattern& resource, const ActionType& action) : _resource(resource) { _actions.addAction(action); diff --git a/src/mongo/db/auth/privilege.h b/src/mongo/db/auth/privilege.h index c946123947f..43f5fb48d9c 100644 --- a/src/mongo/db/auth/privilege.h +++ b/src/mongo/db/auth/privilege.h @@ -52,6 +52,9 @@ public: static void addPrivilegeToPrivilegeVector(PrivilegeVector* privileges, const Privilege& privilegeToAdd); + static void addPrivilegesToPrivilegeVector(PrivilegeVector* privileges, + const PrivilegeVector& privilegesToAdd); + Privilege(){}; Privilege(const ResourcePattern& resource, const ActionType& action); diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 85c78e5a0c9..0d67ccbb2df 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -254,7 +254,6 @@ docSourceEnv.Library( 'document_source_sort.cpp', 'document_source_sort_by_count.cpp', 'document_source_unwind.cpp', - 'lite_parsed_document_source.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/client/clientdriver', @@ -262,6 +261,7 @@ docSourceEnv.Library( '$BUILD_DIR/mongo/db/index/key_generator', '$BUILD_DIR/mongo/db/matcher/expression_algo', '$BUILD_DIR/mongo/db/matcher/expressions', + '$BUILD_DIR/mongo/db/pipeline/lite_parsed_document_source', '$BUILD_DIR/mongo/db/service_context', '$BUILD_DIR/mongo/db/stats/top', '$BUILD_DIR/mongo/db/storage/encryption_hooks', @@ -278,6 +278,16 @@ docSourceEnv.Library( ) env.Library( + target='lite_parsed_document_source', + source=[ + 'lite_parsed_document_source.cpp', + ], + LIBDEPS=[ + 'aggregation_request', + ] +) + +env.Library( target='pipeline', source=[ 'pipeline.cpp', diff --git a/src/mongo/db/pipeline/aggregation_request.cpp b/src/mongo/db/pipeline/aggregation_request.cpp index 844907bd89a..8947c6f9a03 100644 --- a/src/mongo/db/pipeline/aggregation_request.cpp +++ b/src/mongo/db/pipeline/aggregation_request.cpp @@ -216,10 +216,14 @@ StatusWith<AggregationRequest> AggregationRequest::parseFromBSON( request.setExplain(explainVerbosity); } - if (!hasCursorElem && !request.getExplain()) { + // 'hasExplainElem' implies an aggregate command-level explain option, which does not require + // a cursor argument. + if (!hasCursorElem && !hasExplainElem) { return {ErrorCodes::FailedToParse, - str::stream() << "The '" << kCursorName - << "' option is required, except for aggregation explain"}; + str::stream() + << "The '" + << kCursorName + << "' option is required, except for aggregate with the explain argument"}; } if (request.getExplain() && !request.getReadConcern().isEmpty()) { @@ -277,8 +281,9 @@ Document AggregationRequest::serializeToCommandObj() const { _bypassDocumentValidation ? Value(true) : Value()}, // Only serialize a collation if one was specified. {kCollationName, _collation.isEmpty() ? Value() : Value(_collation)}, - // Only serialize batchSize when explain is false. - {kCursorName, _explainMode ? Value() : Value(Document{{kBatchSizeName, _batchSize}})}, + // Only serialize batchSize if not an explain, otherwise serialize an empty cursor object. + {kCursorName, + _explainMode ? Value(Document()) : Value(Document{{kBatchSizeName, _batchSize}})}, // Only serialize a hint if one was specified. {kHintName, _hint.isEmpty() ? Value() : Value(_hint)}, // Only serialize a comment if one was specified. diff --git a/src/mongo/db/pipeline/aggregation_request_test.cpp b/src/mongo/db/pipeline/aggregation_request_test.cpp index 0b1330009da..109bb6083ef 100644 --- a/src/mongo/db/pipeline/aggregation_request_test.cpp +++ b/src/mongo/db/pipeline/aggregation_request_test.cpp @@ -98,7 +98,7 @@ TEST(AggregationRequestTest, ShouldParseExplicitExplainFalseWithCursorOption) { TEST(AggregationRequestTest, ShouldParseWithSeparateQueryPlannerExplainModeArg) { NamespaceString nss("a.collection"); - const BSONObj inputBson = fromjson("{pipeline: []}"); + const BSONObj inputBson = fromjson("{pipeline: [], cursor: {}}"); auto request = unittest::assertGet(AggregationRequest::parseFromBSON( nss, inputBson, ExplainOptions::Verbosity::kQueryPlanner)); ASSERT_TRUE(request.getExplain()); @@ -236,7 +236,7 @@ TEST(AggregationRequestTest, ShouldAcceptHintAsString) { << "a_1")); } -TEST(AggregationRequestTest, ShouldNotSerializeBatchSizeOrExplainWhenExplainSet) { +TEST(AggregationRequestTest, ShouldNotSerializeBatchSizeWhenExplainSet) { NamespaceString nss("a.collection"); AggregationRequest request(nss, {}); request.setBatchSize(10); @@ -244,7 +244,8 @@ TEST(AggregationRequestTest, ShouldNotSerializeBatchSizeOrExplainWhenExplainSet) auto expectedSerialization = Document{{AggregationRequest::kCommandName, nss.coll()}, - {AggregationRequest::kPipelineName, Value(std::vector<Value>{})}}; + {AggregationRequest::kPipelineName, Value(std::vector<Value>{})}, + {AggregationRequest::kCursorName, Value(Document())}}; ASSERT_DOCUMENT_EQ(request.serializeToCommandObj(), expectedSerialization); } diff --git a/src/mongo/db/pipeline/document_source_change_notification.h b/src/mongo/db/pipeline/document_source_change_notification.h index c6748ab3781..e5a1186f04e 100644 --- a/src/mongo/db/pipeline/document_source_change_notification.h +++ b/src/mongo/db/pipeline/document_source_change_notification.h @@ -53,6 +53,11 @@ public: stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { return stdx::unordered_set<NamespaceString>(); } + + // TODO SERVER-29138: Add required privileges. + PrivilegeVector requiredPrivileges(bool isMongos) const final { + return {}; + } }; class Transformation : public DocumentSourceSingleDocumentTransformation::TransformerInterface { diff --git a/src/mongo/db/pipeline/document_source_coll_stats.h b/src/mongo/db/pipeline/document_source_coll_stats.h index ad8673643e6..2506c1be823 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.h +++ b/src/mongo/db/pipeline/document_source_coll_stats.h @@ -42,16 +42,29 @@ public: public: static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request, const BSONElement& spec) { - return stdx::make_unique<LiteParsed>(); + return stdx::make_unique<LiteParsed>(request.getNamespaceString()); } + explicit LiteParsed(NamespaceString nss) : _nss(std::move(nss)) {} + bool isCollStats() const final { return true; } + PrivilegeVector requiredPrivileges(bool isMongos) const final { + return {Privilege(ResourcePattern::forExactNamespace(_nss), ActionType::collStats)}; + } + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { return stdx::unordered_set<NamespaceString>(); } + + bool isInitialSource() const final { + return true; + } + + private: + const NamespaceString _nss; }; DocumentSourceCollStats(const boost::intrusive_ptr<ExpressionContext>& pExpCtx) diff --git a/src/mongo/db/pipeline/document_source_current_op.cpp b/src/mongo/db/pipeline/document_source_current_op.cpp index 6a37192f8ad..b49d7de702c 100644 --- a/src/mongo/db/pipeline/document_source_current_op.cpp +++ b/src/mongo/db/pipeline/document_source_current_op.cpp @@ -48,9 +48,40 @@ const StringData kShardFieldName = "shard"_sd; using boost::intrusive_ptr; REGISTER_DOCUMENT_SOURCE(currentOp, - LiteParsedDocumentSourceDefault::parse, + DocumentSourceCurrentOp::LiteParsed::parse, DocumentSourceCurrentOp::createFromBson); +std::unique_ptr<DocumentSourceCurrentOp::LiteParsed> DocumentSourceCurrentOp::LiteParsed::parse( + const AggregationRequest& request, const BSONElement& spec) { + // Need to check the value of allUsers; if true then inprog privilege is required. + if (spec.type() != BSONType::Object) { + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "$currentOp options must be specified in an object, but found: " + << typeName(spec.type())); + } + + bool allUsers = false; + + // Check the spec for all fields named 'allUsers'. If any of them are 'true', we require + // the 'inprog' privilege. This avoids the possibility that a spec with multiple + // allUsers fields might allow an unauthorized user to view all operations. + for (auto&& elem : spec.embeddedObject()) { + if (elem.fieldNameStringData() == "allUsers"_sd) { + if (elem.type() != BSONType::Bool) { + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "The 'allUsers' parameter of the $currentOp stage " + "must be a boolean value, but found: " + << typeName(elem.type())); + } + + allUsers = allUsers || elem.boolean(); + } + } + + return stdx::make_unique<DocumentSourceCurrentOp::LiteParsed>(allUsers); +} + + const char* DocumentSourceCurrentOp::getSourceName() const { return "$currentOp"; } diff --git a/src/mongo/db/pipeline/document_source_current_op.h b/src/mongo/db/pipeline/document_source_current_op.h index 24ef1ba964d..4526134c13f 100644 --- a/src/mongo/db/pipeline/document_source_current_op.h +++ b/src/mongo/db/pipeline/document_source_current_op.h @@ -34,6 +34,36 @@ namespace mongo { class DocumentSourceCurrentOp final : public DocumentSourceNeedsMongod { public: + class LiteParsed final : public LiteParsedDocumentSource { + public: + static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request, + const BSONElement& spec); + + explicit LiteParsed(bool allUsers) : _allUsers(allUsers) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return stdx::unordered_set<NamespaceString>(); + } + + PrivilegeVector requiredPrivileges(bool isMongos) const final { + PrivilegeVector privileges; + + // In a sharded cluster, we always need the inprog privilege to run $currentOp. + if (isMongos || _allUsers) { + privileges.push_back({ResourcePattern::forClusterResource(), ActionType::inprog}); + } + + return privileges; + } + + bool isInitialSource() const final { + return true; + } + + private: + const bool _allUsers; + }; + using TruncationMode = MongodInterface::CurrentOpTruncateMode; using ConnMode = MongodInterface::CurrentOpConnectionsMode; using UserMode = MongodInterface::CurrentOpUserMode; diff --git a/src/mongo/db/pipeline/document_source_facet.cpp b/src/mongo/db/pipeline/document_source_facet.cpp index 222d78e69c3..992f3daac11 100644 --- a/src/mongo/db/pipeline/document_source_facet.cpp +++ b/src/mongo/db/pipeline/document_source_facet.cpp @@ -118,12 +118,24 @@ vector<pair<string, vector<BSONObj>>> extractRawPipelines(const BSONElement& ele std::unique_ptr<DocumentSourceFacet::LiteParsed> DocumentSourceFacet::LiteParsed::parse( const AggregationRequest& request, const BSONElement& spec) { std::vector<LiteParsedPipeline> liteParsedPipelines; + for (auto&& rawPipeline : extractRawPipelines(spec)) { liteParsedPipelines.emplace_back( AggregationRequest(request.getNamespaceString(), rawPipeline.second)); } - return std::unique_ptr<DocumentSourceFacet::LiteParsed>( - new DocumentSourceFacet::LiteParsed(std::move(liteParsedPipelines))); + + PrivilegeVector requiredPrivileges; + for (auto&& pipeline : liteParsedPipelines) { + + // A correct isMongos flag is only required for DocumentSourceCurrentOp which is disallowed + // in $facet pipelines. + const bool unusedIsMongosFlag = false; + Privilege::addPrivilegesToPrivilegeVector(&requiredPrivileges, + pipeline.requiredPrivileges(unusedIsMongosFlag)); + } + + return stdx::make_unique<DocumentSourceFacet::LiteParsed>(std::move(liteParsedPipelines), + std::move(requiredPrivileges)); } stdx::unordered_set<NamespaceString> DocumentSourceFacet::LiteParsed::getInvolvedNamespaces() diff --git a/src/mongo/db/pipeline/document_source_facet.h b/src/mongo/db/pipeline/document_source_facet.h index 37c2fc5cb8f..f06f73c7b27 100644 --- a/src/mongo/db/pipeline/document_source_facet.h +++ b/src/mongo/db/pipeline/document_source_facet.h @@ -71,13 +71,19 @@ public: static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request, const BSONElement& spec); + LiteParsed(std::vector<LiteParsedPipeline> liteParsedPipelines, PrivilegeVector privileges) + : _liteParsedPipelines(std::move(liteParsedPipelines)), + _requiredPrivileges(std::move(privileges)) {} + + PrivilegeVector requiredPrivileges(bool isMongos) const final { + return _requiredPrivileges; + } + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final; private: - LiteParsed(std::vector<LiteParsedPipeline> liteParsedPipelines) - : _liteParsedPipelines(std::move(liteParsedPipelines)) {} - const std::vector<LiteParsedPipeline> _liteParsedPipelines; + const PrivilegeVector _requiredPrivileges; }; static boost::intrusive_ptr<DocumentSource> createFromBson( diff --git a/src/mongo/db/pipeline/document_source_graph_lookup.cpp b/src/mongo/db/pipeline/document_source_graph_lookup.cpp index 19ce53b3769..bac942e1f31 100644 --- a/src/mongo/db/pipeline/document_source_graph_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_graph_lookup.cpp @@ -71,7 +71,12 @@ std::unique_ptr<LiteParsedDocumentSourceForeignCollections> DocumentSourceGraphL uassert(ErrorCodes::InvalidNamespace, str::stream() << "invalid $graphLookup namespace: " << nss.ns(), nss.isValid()); - return stdx::make_unique<LiteParsedDocumentSourceForeignCollections>(std::move(nss)); + + PrivilegeVector privileges{ + Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find)}; + + return stdx::make_unique<LiteParsedDocumentSourceForeignCollections>(std::move(nss), + std::move(privileges)); } REGISTER_DOCUMENT_SOURCE(graphLookup, diff --git a/src/mongo/db/pipeline/document_source_index_stats.cpp b/src/mongo/db/pipeline/document_source_index_stats.cpp index e6d5b28171c..786f8f59cf0 100644 --- a/src/mongo/db/pipeline/document_source_index_stats.cpp +++ b/src/mongo/db/pipeline/document_source_index_stats.cpp @@ -39,7 +39,7 @@ namespace mongo { using boost::intrusive_ptr; REGISTER_DOCUMENT_SOURCE(indexStats, - LiteParsedDocumentSourceDefault::parse, + DocumentSourceIndexStats::LiteParsed::parse, DocumentSourceIndexStats::createFromBson); const char* DocumentSourceIndexStats::getSourceName() const { diff --git a/src/mongo/db/pipeline/document_source_index_stats.h b/src/mongo/db/pipeline/document_source_index_stats.h index e802e0d7016..7d25aca6c9f 100644 --- a/src/mongo/db/pipeline/document_source_index_stats.h +++ b/src/mongo/db/pipeline/document_source_index_stats.h @@ -39,6 +39,31 @@ namespace mongo { */ class DocumentSourceIndexStats final : public DocumentSourceNeedsMongod { public: + class LiteParsed final : public LiteParsedDocumentSource { + public: + static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request, + const BSONElement& spec) { + return stdx::make_unique<LiteParsed>(request.getNamespaceString()); + } + + explicit LiteParsed(NamespaceString nss) : _nss(std::move(nss)) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return stdx::unordered_set<NamespaceString>(); + } + + PrivilegeVector requiredPrivileges(bool isMongos) const final { + return {Privilege(ResourcePattern::forExactNamespace(_nss), ActionType::indexStats)}; + } + + bool isInitialSource() const final { + return true; + } + + private: + const NamespaceString _nss; + }; + // virtuals from DocumentSource GetNextResult getNext() final; const char* getSourceName() const final; diff --git a/src/mongo/db/pipeline/document_source_lookup.cpp b/src/mongo/db/pipeline/document_source_lookup.cpp index ec2d37db6de..6fa34fba0df 100644 --- a/src/mongo/db/pipeline/document_source_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_lookup.cpp @@ -37,7 +37,6 @@ #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/pipeline/lite_parsed_pipeline.h" #include "mongo/db/pipeline/value.h" #include "mongo/stdx/memory.h" @@ -117,7 +116,7 @@ DocumentSourceLookUp::DocumentSourceLookUp(NamespaceString fromNs, } } -std::unique_ptr<LiteParsedDocumentSourceForeignCollections> DocumentSourceLookUp::liteParse( +std::unique_ptr<DocumentSourceLookUp::LiteParsed> DocumentSourceLookUp::LiteParsed::parse( const AggregationRequest& request, const BSONElement& spec) { uassert(ErrorCodes::FailedToParse, str::stream() << "the $lookup stage specification must be an object, but found " @@ -134,29 +133,33 @@ std::unique_ptr<LiteParsedDocumentSourceForeignCollections> DocumentSourceLookUp << typeName(specObj["from"].type()), fromElement.type() == BSONType::String); - NamespaceString nss(request.getNamespaceString().db(), fromElement.valueStringData()); + NamespaceString fromNss(request.getNamespaceString().db(), fromElement.valueStringData()); uassert(ErrorCodes::InvalidNamespace, - str::stream() << "invalid $lookup namespace: " << nss.ns(), - nss.isValid()); + str::stream() << "invalid $lookup namespace: " << fromNss.ns(), + fromNss.isValid()); stdx::unordered_set<NamespaceString> foreignNssSet; // Recursively lite parse the nested pipeline, if one exists. auto pipelineElem = specObj["pipeline"]; + boost::optional<LiteParsedPipeline> liteParsedPipeline; if (pipelineElem) { auto pipeline = uassertStatusOK(AggregationRequest::parsePipelineFromBSON(pipelineElem)); - AggregationRequest foreignAggReq(nss, std::move(pipeline)); - LiteParsedPipeline liteParsedPipeline(foreignAggReq); - auto pipelineInvolvedNamespaces = liteParsedPipeline.getInvolvedNamespaces(); + AggregationRequest foreignAggReq(fromNss, std::move(pipeline)); + liteParsedPipeline = LiteParsedPipeline(foreignAggReq); + + auto pipelineInvolvedNamespaces = liteParsedPipeline->getInvolvedNamespaces(); foreignNssSet.insert(pipelineInvolvedNamespaces.begin(), pipelineInvolvedNamespaces.end()); } - foreignNssSet.insert(std::move(nss)); - return stdx::make_unique<LiteParsedDocumentSourceForeignCollections>(std::move(foreignNssSet)); + foreignNssSet.insert(fromNss); + + return stdx::make_unique<DocumentSourceLookUp::LiteParsed>( + std::move(fromNss), std::move(foreignNssSet), std::move(liteParsedPipeline)); } REGISTER_DOCUMENT_SOURCE(lookup, - DocumentSourceLookUp::liteParse, + DocumentSourceLookUp::LiteParsed::parse, DocumentSourceLookUp::createFromBson); const char* DocumentSourceLookUp::getSourceName() const { diff --git a/src/mongo/db/pipeline/document_source_lookup.h b/src/mongo/db/pipeline/document_source_lookup.h index 12965eee2d0..bb4fae8df2e 100644 --- a/src/mongo/db/pipeline/document_source_lookup.h +++ b/src/mongo/db/pipeline/document_source_lookup.h @@ -28,10 +28,13 @@ #pragma once +#include <boost/optional.hpp> + #include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/document_source_match.h" #include "mongo/db/pipeline/document_source_unwind.h" #include "mongo/db/pipeline/expression.h" +#include "mongo/db/pipeline/lite_parsed_pipeline.h" #include "mongo/db/pipeline/lookup_set_cache.h" #include "mongo/db/pipeline/value_comparator.h" @@ -44,8 +47,41 @@ namespace mongo { class DocumentSourceLookUp final : public DocumentSourceNeedsMongod, public SplittableDocumentSource { public: - static std::unique_ptr<LiteParsedDocumentSourceForeignCollections> liteParse( - const AggregationRequest& request, const BSONElement& spec); + class LiteParsed final : public LiteParsedDocumentSource { + public: + static std::unique_ptr<LiteParsed> parse(const AggregationRequest& request, + const BSONElement& spec); + + LiteParsed(NamespaceString fromNss, + stdx::unordered_set<NamespaceString> foreignNssSet, + boost::optional<LiteParsedPipeline> liteParsedPipeline) + : _fromNss{std::move(fromNss)}, + _foreignNssSet(std::move(foreignNssSet)), + _liteParsedPipeline(std::move(liteParsedPipeline)) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return {_foreignNssSet}; + } + + PrivilegeVector requiredPrivileges(bool isMongos) const final { + PrivilegeVector requiredPrivileges; + Privilege::addPrivilegeToPrivilegeVector( + &requiredPrivileges, + Privilege(ResourcePattern::forExactNamespace(_fromNss), ActionType::find)); + + if (_liteParsedPipeline) { + Privilege::addPrivilegesToPrivilegeVector( + &requiredPrivileges, _liteParsedPipeline->requiredPrivileges(isMongos)); + } + + return requiredPrivileges; + } + + private: + const NamespaceString _fromNss; + const stdx::unordered_set<NamespaceString> _foreignNssSet; + const boost::optional<LiteParsedPipeline> _liteParsedPipeline; + }; GetNextResult getNext() final; const char* getSourceName() const final; diff --git a/src/mongo/db/pipeline/document_source_lookup_test.cpp b/src/mongo/db/pipeline/document_source_lookup_test.cpp index c9f56aa2295..0ca5f1a475a 100644 --- a/src/mongo/db/pipeline/document_source_lookup_test.cpp +++ b/src/mongo/db/pipeline/document_source_lookup_test.cpp @@ -160,7 +160,8 @@ TEST_F(DocumentSourceLookUpTest, LiteParsedDocumentSourceLookupContainsExpectedN NamespaceString nss("test.test"); std::vector<BSONObj> pipeline; AggregationRequest aggRequest(nss, pipeline); - auto liteParsedLookup = DocumentSourceLookUp::liteParse(aggRequest, stageSpec.firstElement()); + auto liteParsedLookup = + DocumentSourceLookUp::LiteParsed::parse(aggRequest, stageSpec.firstElement()); auto namespaceSet = liteParsedLookup->getInvolvedNamespaces(); diff --git a/src/mongo/db/pipeline/document_source_out.cpp b/src/mongo/db/pipeline/document_source_out.cpp index a2e0ceea44b..069257abcb9 100644 --- a/src/mongo/db/pipeline/document_source_out.cpp +++ b/src/mongo/db/pipeline/document_source_out.cpp @@ -48,16 +48,25 @@ DocumentSourceOut::~DocumentSourceOut() { std::unique_ptr<LiteParsedDocumentSourceForeignCollections> DocumentSourceOut::liteParse( const AggregationRequest& request, const BSONElement& spec) { - uassert(40325, + uassert(ErrorCodes::TypeMismatch, str::stream() << "$out stage requires a string argument, but found " << typeName(spec.type()), spec.type() == BSONType::String); NamespaceString targetNss(request.getNamespaceString().db(), spec.valueStringData()); - uassert(40326, + uassert(ErrorCodes::InvalidNamespace, str::stream() << "Invalid $out target namespace, " << targetNss.ns(), targetNss.isValid()); - return stdx::make_unique<LiteParsedDocumentSourceForeignCollections>(std::move(targetNss)); + + ActionSet actions{ActionType::remove, ActionType::insert}; + if (request.shouldBypassDocumentValidation()) { + actions.addAction(ActionType::bypassDocumentValidation); + } + + PrivilegeVector privileges{Privilege(ResourcePattern::forExactNamespace(targetNss), actions)}; + + return stdx::make_unique<LiteParsedDocumentSourceForeignCollections>(std::move(targetNss), + std::move(privileges)); } REGISTER_DOCUMENT_SOURCE(out, DocumentSourceOut::liteParse, DocumentSourceOut::createFromBson); diff --git a/src/mongo/db/pipeline/lite_parsed_document_source.h b/src/mongo/db/pipeline/lite_parsed_document_source.h index dae9ee222ce..489e8bc1aa7 100644 --- a/src/mongo/db/pipeline/lite_parsed_document_source.h +++ b/src/mongo/db/pipeline/lite_parsed_document_source.h @@ -32,6 +32,7 @@ #include <memory> #include <vector> +#include "mongo/db/auth/privilege.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregation_request.h" #include "mongo/stdx/functional.h" @@ -85,6 +86,11 @@ public: virtual stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const = 0; /** + * Returns a list of the privileges required for this stage. + */ + virtual PrivilegeVector requiredPrivileges(bool isMongos) const = 0; + + /** * Returns true if this is a $collStats stage. */ virtual bool isCollStats() const { @@ -97,6 +103,13 @@ public: virtual bool isChangeNotification() const { return false; } + + /** + * Returns true if this stage does not require an input source. + */ + virtual bool isInitialSource() const { + return false; + } }; class LiteParsedDocumentSourceDefault final : public LiteParsedDocumentSource { @@ -116,6 +129,10 @@ public: stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { return stdx::unordered_set<NamespaceString>(); } + + PrivilegeVector requiredPrivileges(bool isMongos) const final { + return {}; + } }; /** @@ -123,18 +140,24 @@ public: */ class LiteParsedDocumentSourceForeignCollections : public LiteParsedDocumentSource { public: - explicit LiteParsedDocumentSourceForeignCollections(NamespaceString foreignNss) - : _foreignNssSet{std::move(foreignNss)} {} + LiteParsedDocumentSourceForeignCollections(NamespaceString foreignNss, + PrivilegeVector privileges) + : _foreignNssSet{std::move(foreignNss)}, _requiredPrivileges(std::move(privileges)) {} - explicit LiteParsedDocumentSourceForeignCollections( - stdx::unordered_set<NamespaceString> foreignNssSet) - : _foreignNssSet(std::move(foreignNssSet)) {} + LiteParsedDocumentSourceForeignCollections(stdx::unordered_set<NamespaceString> foreignNssSet, + PrivilegeVector privileges) + : _foreignNssSet(std::move(foreignNssSet)), _requiredPrivileges(std::move(privileges)) {} stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { return {_foreignNssSet}; } + PrivilegeVector requiredPrivileges(bool isMongos) const final { + return _requiredPrivileges; + } + private: stdx::unordered_set<NamespaceString> _foreignNssSet; + PrivilegeVector _requiredPrivileges; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/lite_parsed_pipeline.h b/src/mongo/db/pipeline/lite_parsed_pipeline.h index ca2e9dfda77..07e5c947cc3 100644 --- a/src/mongo/db/pipeline/lite_parsed_pipeline.h +++ b/src/mongo/db/pipeline/lite_parsed_pipeline.h @@ -72,6 +72,19 @@ public: } /** + * Returns a list of the priviliges required for this pipeline. + */ + PrivilegeVector requiredPrivileges(bool isMongos) const { + PrivilegeVector requiredPrivileges; + for (auto&& spec : _stageSpecs) { + Privilege::addPrivilegesToPrivilegeVector(&requiredPrivileges, + spec->requiredPrivileges(isMongos)); + } + + return requiredPrivileges; + } + + /** * Returns true if the pipeline begins with a $collStats stage. */ bool startsWithCollStats() const { diff --git a/src/mongo/shell/explainable.js b/src/mongo/shell/explainable.js index 0dcc5c6d76e..795326ab9d0 100644 --- a/src/mongo/shell/explainable.js +++ b/src/mongo/shell/explainable.js @@ -92,13 +92,9 @@ var Explainable = (function() { // Explainable operations. // - /** - * Adds "explain: true" to "extraOpts", and then passes through to the regular collection's - * aggregate helper. - */ this.aggregate = function(pipeline, extraOpts) { if (!(pipeline instanceof Array)) { - // support legacy varargs form. (Also handles db.foo.aggregate()) + // Support legacy varargs form. (Also handles db.foo.aggregate()) pipeline = Array.from(arguments); extraOpts = {}; } @@ -113,6 +109,11 @@ var Explainable = (function() { extraOpts.explain = true; return this._collection.aggregate(pipeline, extraOpts); } else { + // The aggregate command requires a cursor field. + if (!extraOpts.hasOwnProperty("cursor")) { + extraOpts = Object.extend(extraOpts, {cursor: {}}); + } + let aggCmd = Object.extend( {"aggregate": this._collection.getName(), "pipeline": pipeline}, extraOpts); let explainCmd = {"explain": aggCmd, "verbosity": this._verbosity}; |