diff options
author | Isabella Siu <isabella.siu@10gen.com> | 2019-01-11 11:16:24 -0500 |
---|---|---|
committer | Isabella Siu <isabella.siu@10gen.com> | 2019-02-08 14:34:32 -0500 |
commit | 8e5e745e98d33633e7d24a2629f22cdba79d9851 (patch) | |
tree | 9e02d92dadcf67140fe8707d792e55cf12b5443e | |
parent | 7a7baa2539ec169335086e45c7d0b85ba7cdb877 (diff) | |
download | mongo-8e5e745e98d33633e7d24a2629f22cdba79d9851.tar.gz |
SERVER-37836 re-evaluate authorization for originating command in getMore
42 files changed, 631 insertions, 248 deletions
diff --git a/jstests/auth/getMore.js b/jstests/auth/getMore.js index 49c60fcf6ca..d58c52a205c 100644 --- a/jstests/auth/getMore.js +++ b/jstests/auth/getMore.js @@ -40,6 +40,7 @@ assert.eq(1, testDB.auth("Alice", "pwd")); let res = assert.commandWorked(testDB.runCommand({find: "foo", batchSize: 0})); let cursorId = res.cursor.id; + assert.neq(0, cursorId); testDB.logout(); assert.eq(1, testDB.auth("Mallory", "pwd")); assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), @@ -66,6 +67,7 @@ res = assert.commandWorked( testDB.runCommand({aggregate: "foo", pipeline: [], cursor: {batchSize: 0}})); cursorId = res.cursor.id; + assert.neq(0, cursorId); testDB.logout(); assert.eq(1, testDB.auth("Mallory", "pwd")); assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), @@ -77,6 +79,7 @@ assert.eq(1, testDB.auth("Alice", "pwd")); res = assert.commandWorked(testDB.runCommand({listCollections: 1, cursor: {batchSize: 0}})); cursorId = res.cursor.id; + assert.neq(0, cursorId); testDB.logout(); assert.eq(1, testDB.auth("Mallory", "pwd")); assert.commandFailedWithCode( @@ -89,6 +92,7 @@ assert.eq(1, testDB.auth("Alice", "pwd")); res = assert.commandWorked(testDB.runCommand({listIndexes: "foo", cursor: {batchSize: 0}})); cursorId = res.cursor.id; + assert.neq(0, cursorId); testDB.logout(); assert.eq(1, testDB.auth("Mallory", "pwd")); assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), @@ -97,7 +101,7 @@ testDB.logout(); // - // Test that a user can call getMore on an indexStats cursor they created, even if the + // Test that a user can call getMore on an indexStats cursor they created, unless the // indexStats privilege has been revoked in the meantime. // @@ -115,6 +119,13 @@ res = assert.commandWorked(testDB.runCommand( {aggregate: "foo", pipeline: [{$indexStats: {}}], cursor: {batchSize: 0}})); cursorId = res.cursor.id; + assert.neq(0, cursorId); + assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); + + res = assert.commandWorked(testDB.runCommand( + {aggregate: "foo", pipeline: [{$indexStats: {}}], cursor: {batchSize: 0}})); + cursorId = res.cursor.id; + assert.neq(0, cursorId); testDB.logout(); assert.eq(1, adminDB.auth("admin", "admin")); @@ -123,11 +134,79 @@ adminDB.logout(); assert.eq(1, testDB.auth("Bob", "pwd")); + assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), + ErrorCodes.Unauthorized, + "read from a cursor without required privileges"); + testDB.logout(); + + // + // Test that a user can call getMore on a listCollections cursor they created, unless the + // readWrite privilege has been revoked in the meantime. + // + + assert.eq(1, adminDB.auth("admin", "admin")); + + assert.commandWorked( + testDB.runCommand({createUser: "Tom", pwd: "pwd", roles: ["readWrite"]})); + adminDB.logout(); + + assert.eq(1, testDB.auth("Tom", "pwd")); + res = assert.commandWorked(testDB.runCommand({listCollections: 1, cursor: {batchSize: 0}})); + cursorId = res.cursor.id; + assert.neq(0, cursorId); + assert.commandWorked( + testDB.runCommand({getMore: cursorId, collection: "$cmd.listCollections"})); + + res = assert.commandWorked(testDB.runCommand({listCollections: 1, cursor: {batchSize: 0}})); + cursorId = res.cursor.id; + assert.neq(0, cursorId); + testDB.logout(); + + assert.eq(1, adminDB.auth("admin", "admin")); + assert.commandWorked(testDB.runCommand({revokeRolesFromUser: "Tom", roles: ["readWrite"]})); + adminDB.logout(); + + assert.eq(1, testDB.auth("Tom", "pwd")); + assert.commandFailedWithCode( + testDB.runCommand({getMore: cursorId, collection: "$cmd.listCollections"}), + ErrorCodes.Unauthorized, + "read from a cursor without required privileges"); + testDB.logout(); + // + // Test that a user can call getMore on a listIndexes cursor they created, unless the + // readWrite privilege has been revoked in the meantime. + // + + assert.eq(1, adminDB.auth("admin", "admin")); + + assert.commandWorked( + testDB.runCommand({createUser: "Bill", pwd: "pwd", roles: ["readWrite"]})); + adminDB.logout(); + + assert.eq(1, testDB.auth("Bill", "pwd")); + res = assert.commandWorked(testDB.runCommand({listIndexes: "foo", cursor: {batchSize: 0}})); + cursorId = res.cursor.id; + assert.neq(0, cursorId); assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); + + res = assert.commandWorked(testDB.runCommand({listIndexes: "foo", cursor: {batchSize: 0}})); + cursorId = res.cursor.id; + assert.neq(0, cursorId); + testDB.logout(); + + assert.eq(1, adminDB.auth("admin", "admin")); + assert.commandWorked( + testDB.runCommand({revokeRolesFromUser: "Bill", roles: ["readWrite"]})); + adminDB.logout(); + + assert.eq(1, testDB.auth("Bill", "pwd")); + assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), + ErrorCodes.Unauthorized, + "read from a cursor without required privileges"); testDB.logout(); // - // Test that a user can run a getMore on an aggregate cursor they created, even if some + // Test that a user can run a getMore on an aggregate cursor they created, unless some // privileges required for the pipeline have been revoked in the meantime. // @@ -138,19 +217,33 @@ cursor: {batchSize: 0} })); cursorId = res.cursor.id; + assert.neq(0, cursorId); + assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); + + res = assert.commandWorked(testDB.runCommand({ + aggregate: "foo", + pipeline: [{$match: {_id: 0}}, {$out: "out"}], + cursor: {batchSize: 0} + })); + cursorId = res.cursor.id; + assert.neq(0, cursorId); testDB.logout(); + assert.eq(1, adminDB.auth("admin", "admin")); testDB.revokeRolesFromUser("Alice", ["readWrite"]); testDB.grantRolesToUser("Alice", ["read"]); adminDB.logout(); + assert.eq(1, testDB.auth("Alice", "pwd")); assert.commandFailedWithCode( testDB.runCommand( {aggregate: "foo", pipeline: [{$match: {_id: 0}}, {$out: "out"}], cursor: {}}), ErrorCodes.Unauthorized, "user should no longer have write privileges"); - res = assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); - assert.eq(1, testDB.out.find().itcount()); + assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), + ErrorCodes.Unauthorized, + "wrote from a cursor without required privileges"); + testDB.logout(); // // Test that if there were multiple users authenticated when the cursor was created, then at @@ -189,24 +282,26 @@ assert.eq(1, adminDB.auth("fooBarUser", "pwd")); res = assert.commandWorked(testDB.runCommand({find: "foo", batchSize: 0})); cursorId = res.cursor.id; + assert.neq(0, cursorId); adminDB.logout(); assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); testDB.logout(); - // Test that a cursor created by "fooUser" and "fooBarUser" can be used by "fooUser" even if + // Test that a cursor created by "fooUser" and "fooBarUser" cannot be used by "fooUser" if // "fooUser" does not have the privilege to read the collection. assert.eq(1, testDB.auth("fooUser", "pwd")); assert.eq(1, adminDB.auth("fooBarUser", "pwd")); res = assert.commandWorked(testDB.runCommand({find: "bar", batchSize: 0})); cursorId = res.cursor.id; + assert.neq(0, cursorId); adminDB.logout(); - assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "bar"})); + assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "bar"}), + ErrorCodes.Unauthorized, + "read from a cursor without required privileges"); testDB.logout(); - // Test that an aggregate cursor created by "fooUser" and "fooBarUser" can be used by - // "fooUser", even if "fooUser" does not have all privileges required by the pipeline. This - // is not desirable behavior, but it will be resolved when we require that only one user be - // authenticated at a time. + // Test that an aggregate cursor created by "fooUser" and "fooBarUser" cannot be used by + // "fooUser" if "fooUser" does not have all privileges required by the pipeline. assert.eq(1, testDB.auth("fooUser", "pwd")); assert.eq(1, adminDB.auth("fooBarUser", "pwd")); res = assert.commandWorked(testDB.runCommand({ @@ -218,9 +313,23 @@ cursor: {batchSize: 0} })); cursorId = res.cursor.id; + assert.neq(0, cursorId); + assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); + + res = assert.commandWorked(testDB.runCommand({ + aggregate: "foo", + pipeline: [ + {$match: {_id: 0}}, + {$lookup: {from: "bar", localField: "_id", foreignField: "_id", as: "bar"}} + ], + cursor: {batchSize: 0} + })); + cursorId = res.cursor.id; + assert.neq(0, cursorId); adminDB.logout(); - res = assert.commandWorked(testDB.runCommand({getMore: cursorId, collection: "foo"})); - assert.eq(res.cursor.nextBatch, [{_id: 0, bar: [{_id: 0}]}], tojson(res)); + assert.commandFailedWithCode(testDB.runCommand({getMore: cursorId, collection: "foo"}), + ErrorCodes.Unauthorized, + "read from a cursor without required privileges"); testDB.logout(); } diff --git a/src/mongo/db/auth/authorization_session.h b/src/mongo/db/auth/authorization_session.h index e4ce0845695..25e40608eb7 100644 --- a/src/mongo/db/auth/authorization_session.h +++ b/src/mongo/db/auth/authorization_session.h @@ -219,11 +219,12 @@ public: virtual Status checkAuthForKillCursors(const NamespaceString& cursorNss, UserNameIterator cursorOwner) = 0; - // Checks if this connection has the privileges necessary to run the aggregation pipeline - // specified in 'cmdObj' on the namespace 'ns' either directly on mongoD or via mongoS. - virtual Status checkAuthForAggregate(const NamespaceString& ns, - const BSONObj& cmdObj, - bool isMongos) = 0; + // Attempts to get the privileges necessary to run the aggregation pipeline specified in + // 'cmdObj' on the namespace 'ns' either directly on mongoD or via mongoS. Returns a non-ok + // status if it is unable to parse the pipeline. + virtual StatusWith<PrivilegeVector> getPrivilegesForAggregate(const NamespaceString& ns, + const BSONObj& cmdObj, + bool isMongos) = 0; // Checks if this connection has the privileges necessary to create 'ns' with the options // supplied in 'cmdObj' either directly on mongoD or via mongoS. @@ -245,6 +246,12 @@ public: // from a role. virtual Status checkAuthorizedToRevokePrivilege(const Privilege& privilege) = 0; + // Checks if the current session is authorized to list the collections in the given + // database. If it is, return a privilegeVector containing the privileges used to authorize + // this command. + virtual StatusWith<PrivilegeVector> checkAuthorizedToListCollections(StringData dbname, + const BSONObj& cmdObj) = 0; + // Checks if this connection is using the localhost bypass virtual bool isUsingLocalhostBypass() = 0; @@ -271,10 +278,6 @@ public: // is allowed to change his/her own password virtual bool isAuthorizedToChangeOwnPasswordAsUser(const UserName& userName) = 0; - // Returns true if the current session is authorized to list the collections in the given - // database. - virtual bool isAuthorizedToListCollections(StringData dbname, const BSONObj& cmdObj) = 0; - // Returns true if the current session is authenticated as the given user and that user // is allowed to change his/her own customData. virtual bool isAuthorizedToChangeOwnCustomDataAsUser(const UserName& userName) = 0; diff --git a/src/mongo/db/auth/authorization_session_impl.cpp b/src/mongo/db/auth/authorization_session_impl.cpp index c54b632d943..72f879f7344 100644 --- a/src/mongo/db/auth/authorization_session_impl.cpp +++ b/src/mongo/db/auth/authorization_session_impl.cpp @@ -87,10 +87,15 @@ Status checkAuthForCreateOrModifyView(AuthorizationSession* authzSession, // 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( + auto statusWithPrivs = authzSession->getPrivilegesForAggregate( viewOnNs, BSON("aggregate" << viewOnNs.coll() << "pipeline" << viewPipeline << "cursor" << BSONObj()), isMongos); + PrivilegeVector privileges = uassertStatusOK(statusWithPrivs); + if (!authzSession->isAuthorizedForPrivileges(privileges)) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + return Status::OK(); } } // namespace @@ -236,23 +241,19 @@ PrivilegeVector AuthorizationSessionImpl::getDefaultPrivileges() { return defaultPrivileges; } -Status AuthorizationSessionImpl::checkAuthForAggregate(const NamespaceString& nss, - const BSONObj& cmdObj, - bool isMongos) { +StatusWith<PrivilegeVector> AuthorizationSessionImpl::getPrivilegesForAggregate( + const NamespaceString& nss, const BSONObj& cmdObj, bool isMongos) { if (!nss.isValid()) { return Status(ErrorCodes::InvalidNamespace, mongoutils::str::stream() << "Invalid input namespace, " << nss.ns()); } + PrivilegeVector privileges; + // If this connection does not need to be authenticated (for instance, if auth is disabled), - // return Status::OK() immediately. + // returns an empty requirements set. if (_externalState->shouldIgnoreAuthChecks()) { - return Status::OK(); - } - - // We require at least one authenticated user when running aggregate with auth enabled. - if (!isAuthenticated()) { - return Status(ErrorCodes::Unauthorized, "unauthorized"); + return privileges; } auto statusWithAggRequest = AggregationRequest::parseFromBSON(nss, cmdObj); @@ -265,44 +266,28 @@ Status AuthorizationSessionImpl::checkAuthForAggregate(const NamespaceString& ns // 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"); - } - - return Status::OK(); + Privilege currentPriv = + Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find); + Privilege::addPrivilegeToPrivilegeVector(&privileges, currentPriv); + return privileges; } - // 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"); + // If the first stage of the pipeline is not an initial source, the pipeline is implicitly + // reading documents from the underlying collection. The client must be authorized to do so. + auto liteParsedDocSource = LiteParsedDocumentSource::parse(aggRequest, pipeline[0]); + if (!liteParsedDocSource->isInitialSource()) { + Privilege currentPriv = + Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find); + Privilege::addPrivilegeToPrivilegeVector(&privileges, currentPriv); } - // 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"); - } + // Confirm privileges for the pipeline. + for (auto&& pipelineStage : pipeline) { + liteParsedDocSource = LiteParsedDocumentSource::parse(aggRequest, pipelineStage); + PrivilegeVector currentPrivs = liteParsedDocSource->requiredPrivileges(isMongos); + Privilege::addPrivilegesToPrivilegeVector(&privileges, currentPrivs); } - - return Status::OK(); + return privileges; } Status AuthorizationSessionImpl::checkAuthForFind(const NamespaceString& ns, bool hasTerm) { @@ -699,16 +684,20 @@ bool AuthorizationSessionImpl::isAuthorizedToChangeOwnCustomDataAsUser(const Use ActionType::changeOwnCustomData); } -bool AuthorizationSessionImpl::isAuthorizedToListCollections(StringData dbname, - const BSONObj& cmdObj) { +StatusWith<PrivilegeVector> AuthorizationSessionImpl::checkAuthorizedToListCollections( + StringData dbname, const BSONObj& cmdObj) { if (cmdObj["authorizedCollections"].trueValue() && cmdObj["nameOnly"].trueValue() && AuthorizationSessionImpl::isAuthorizedForAnyActionOnAnyResourceInDB(dbname)) { - return true; + return PrivilegeVector(); } // Check for the listCollections ActionType on the database. - return AuthorizationSessionImpl::isAuthorizedForActionsOnResource( - ResourcePattern::forDatabaseName(dbname), ActionType::listCollections); + PrivilegeVector privileges = { + Privilege(ResourcePattern::forDatabaseName(dbname), ActionType::listCollections)}; + if (AuthorizationSessionImpl::isAuthorizedForPrivileges(privileges)) { + return privileges; + } + return Status(ErrorCodes::Unauthorized, "unauthorized"); } bool AuthorizationSessionImpl::isAuthenticatedAsUserWithRole(const RoleName& roleName) { diff --git a/src/mongo/db/auth/authorization_session_impl.h b/src/mongo/db/auth/authorization_session_impl.h index f8ca7c286db..0b432c3fe7d 100644 --- a/src/mongo/db/auth/authorization_session_impl.h +++ b/src/mongo/db/auth/authorization_session_impl.h @@ -122,9 +122,9 @@ public: Status checkAuthForKillCursors(const NamespaceString& cursorNss, UserNameIterator cursorOwner) override; - Status checkAuthForAggregate(const NamespaceString& ns, - const BSONObj& cmdObj, - bool isMongos) override; + StatusWith<PrivilegeVector> getPrivilegesForAggregate(const NamespaceString& ns, + const BSONObj& cmdObj, + bool isMongos) override; Status checkAuthForCreate(const NamespaceString& ns, const BSONObj& cmdObj, @@ -134,6 +134,9 @@ public: const BSONObj& cmdObj, bool isMongos) override; + StatusWith<PrivilegeVector> checkAuthorizedToListCollections(StringData dbname, + const BSONObj& cmdObj) override; + Status checkAuthorizedToGrantPrivilege(const Privilege& privilege) override; Status checkAuthorizedToRevokePrivilege(const Privilege& privilege) override; @@ -152,8 +155,6 @@ public: bool isAuthorizedToChangeOwnPasswordAsUser(const UserName& userName) override; - bool isAuthorizedToListCollections(StringData dbname, const BSONObj& cmdObj) override; - bool isAuthorizedToChangeOwnCustomDataAsUser(const UserName& userName) override; bool isAuthenticatedAsUserWithRole(const RoleName& roleName) override; diff --git a/src/mongo/db/auth/authorization_session_test.cpp b/src/mongo/db/auth/authorization_session_test.cpp index e842e27a06c..b492704232a 100644 --- a/src/mongo/db/auth/authorization_session_test.cpp +++ b/src/mongo/db/auth/authorization_session_test.cpp @@ -640,16 +640,19 @@ TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineIsNotAnArra authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); BSONObj cmdObjIntPipeline = BSON("aggregate" << testFooNss.coll() << "pipeline" << 7); - ASSERT_EQ(ErrorCodes::TypeMismatch, - authzSession->checkAuthForAggregate(testFooNss, cmdObjIntPipeline, false)); + ASSERT_EQ( + ErrorCodes::TypeMismatch, + authzSession->getPrivilegesForAggregate(testFooNss, cmdObjIntPipeline, false).getStatus()); BSONObj cmdObjObjPipeline = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONObj()); - ASSERT_EQ(ErrorCodes::TypeMismatch, - authzSession->checkAuthForAggregate(testFooNss, cmdObjObjPipeline, false)); + ASSERT_EQ( + ErrorCodes::TypeMismatch, + authzSession->getPrivilegesForAggregate(testFooNss, cmdObjObjPipeline, false).getStatus()); BSONObj cmdObjNoPipeline = BSON("aggregate" << testFooNss.coll()); - ASSERT_EQ(ErrorCodes::TypeMismatch, - authzSession->checkAuthForAggregate(testFooNss, cmdObjNoPipeline, false)); + ASSERT_EQ( + ErrorCodes::TypeMismatch, + authzSession->getPrivilegesForAggregate(testFooNss, cmdObjNoPipeline, false).getStatus()); } TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineFirstStageIsNotAnObject) { @@ -658,19 +661,22 @@ TEST_F(AuthorizationSessionTest, CheckAuthForAggregateFailsIfPipelineFirstStageI BSONObj cmdObjFirstStageInt = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSON_ARRAY(7)); ASSERT_EQ(ErrorCodes::TypeMismatch, - authzSession->checkAuthForAggregate(testFooNss, cmdObjFirstStageInt, false)); + authzSession->getPrivilegesForAggregate(testFooNss, cmdObjFirstStageInt, false) + .getStatus()); BSONObj cmdObjFirstStageArray = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSON_ARRAY(BSONArray())); ASSERT_EQ(ErrorCodes::TypeMismatch, - authzSession->checkAuthForAggregate(testFooNss, cmdObjFirstStageArray, false)); + authzSession->getPrivilegesForAggregate(testFooNss, cmdObjFirstStageArray, false) + .getStatus()); } TEST_F(AuthorizationSessionTest, CannotAggregateEmptyPipelineWithoutFindAction) { BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONArray() << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateEmptyPipelineWithFindAction) { @@ -678,7 +684,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateEmptyPipelineWithFindAction) { BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << BSONArray() << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateWithoutFindActionIfFirstStageNotIndexOrCollStats) { @@ -689,8 +697,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateWithoutFindActionIfFirstStageNot << BSON("$indexStats" << BSONObj())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateWithFindActionIfPipelineContainsIndexOrCollStats) { @@ -699,8 +708,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateWithFindActionIfPipelineContains << BSON("$indexStats" << BSONObj())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateCollStatsWithoutCollStatsAction) { @@ -709,8 +719,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCollStatsWithoutCollStatsAction) BSONArray pipeline = BSON_ARRAY(BSON("$collStats" << BSONObj())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateCollStatsWithCollStatsAction) { @@ -719,7 +730,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateCollStatsWithCollStatsAction) { BSONArray pipeline = BSON_ARRAY(BSON("$collStats" << BSONObj())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateIndexStatsWithoutIndexStatsAction) { @@ -728,8 +741,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateIndexStatsWithoutIndexStatsActio BSONArray pipeline = BSON_ARRAY(BSON("$indexStats" << BSONObj())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateIndexStatsWithIndexStatsAction) { @@ -738,7 +752,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateIndexStatsWithIndexStatsAction) { BSONArray pipeline = BSON_ARRAY(BSON("$indexStats" << BSONObj())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersFalseWithoutInprogActionOnMongoD) { @@ -747,7 +763,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersFalseWithoutInprog BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseWithoutInprogActionOnMongoS) { @@ -756,17 +774,16 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseWithoutInp BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseIfNotAuthenticatedOnMongoD) { BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthenticated()); } TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseIfNotAuthenticatedOnMongoS) { @@ -774,8 +791,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersFalseIfNotAuthe BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersTrueWithoutInprogActionOnMongoD) { @@ -784,8 +802,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersTrueWithoutInpr BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersTrueWithoutInprogActionOnMongoS) { @@ -794,8 +813,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateCurrentOpAllUsersTrueWithoutInpr BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersTrueWithInprogActionOnMongoD) { @@ -805,7 +825,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersTrueWithInprogActi BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersTrueWithInprogActionOnMongoS) { @@ -815,7 +837,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateCurrentOpAllUsersTrueWithInprogActi BSONArray pipeline = BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << true))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotSpoofAllUsersTrueWithoutInprogActionOnMongoD) { @@ -825,8 +849,9 @@ TEST_F(AuthorizationSessionTest, CannotSpoofAllUsersTrueWithoutInprogActionOnMon BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false << "allUsers" << true))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotSpoofAllUsersTrueWithoutInprogActionOnMongoS) { @@ -836,8 +861,9 @@ TEST_F(AuthorizationSessionTest, CannotSpoofAllUsersTrueWithoutInprogActionOnMon BSON_ARRAY(BSON("$currentOp" << BSON("allUsers" << false << "allUsers" << true))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, true)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, AddPrivilegesForStageFailsIfOutNamespaceIsNotValid) { @@ -847,7 +873,7 @@ TEST_F(AuthorizationSessionTest, AddPrivilegesForStageFailsIfOutNamespaceIsNotVa << "")); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_THROWS_CODE(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false), + ASSERT_THROWS_CODE(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false), AssertionException, ErrorCodes::InvalidNamespace); } @@ -859,20 +885,19 @@ TEST_F(AuthorizationSessionTest, CannotAggregateOutWithoutInsertAndRemoveOnTarge BSONArray pipeline = BSON_ARRAY(BSON("$out" << testBarNss.coll())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); // We have insert but not remove on the $out namespace. authzSession->assumePrivilegesForDB({Privilege(testFooCollResource, {ActionType::find}), Privilege(testBarCollResource, {ActionType::insert})}); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); // We have remove but not insert on the $out namespace. authzSession->assumePrivilegesForDB({Privilege(testFooCollResource, {ActionType::find}), Privilege(testBarCollResource, {ActionType::remove})}); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateOutWithInsertAndRemoveOnTargetNamespace) { @@ -883,15 +908,18 @@ TEST_F(AuthorizationSessionTest, CanAggregateOutWithInsertAndRemoveOnTargetNames BSONArray pipeline = BSON_ARRAY(BSON("$out" << testBarNss.coll())); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); BSONObj cmdObjNoBypassDocumentValidation = BSON( "aggregate" << testFooNss.coll() << "pipeline" << pipeline << "bypassDocumentValidation" << false << "cursor" << BSONObj()); - ASSERT_OK( - authzSession->checkAuthForAggregate(testFooNss, cmdObjNoBypassDocumentValidation, false)); + privileges = uassertStatusOK(authzSession->getPrivilegesForAggregate( + testFooNss, cmdObjNoBypassDocumentValidation, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, @@ -905,8 +933,9 @@ TEST_F(AuthorizationSessionTest, BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj() << "bypassDocumentValidation" << true); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, @@ -922,7 +951,9 @@ TEST_F(AuthorizationSessionTest, BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj() << "bypassDocumentValidation" << true); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnJoinedNamespace) { @@ -931,8 +962,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnJoinedNamespa BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << testBarNss.coll()))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnJoinedNamespace) { @@ -942,7 +974,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnJoinedNamespace) { BSONArray pipeline = BSON_ARRAY(BSON("$lookup" << BSON("from" << testBarNss.coll()))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } @@ -955,8 +989,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateLookupWithoutFindOnNestedJoinedN BSON("$lookup" << BSON("from" << testBarNss.coll() << "pipeline" << nestedPipeline))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnNestedJoinedNamespace) { @@ -969,7 +1004,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateLookupWithFindOnNestedJoinedNamespa BSON("$lookup" << BSON("from" << testBarNss.coll() << "pipeline" << nestedPipeline))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CheckAuthForAggregateWithDeeplyNestedLookup) { @@ -1010,7 +1047,9 @@ TEST_F(AuthorizationSessionTest, CheckAuthForAggregateWithDeeplyNestedLookup) { pipelineBuilder.doneFast(); cmdBuilder << "cursor" << BSONObj(); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdBuilder.obj(), false)); + PrivilegeVector privileges = uassertStatusOK( + authzSession->getPrivilegesForAggregate(testFooNss, cmdBuilder.obj(), false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } @@ -1020,8 +1059,9 @@ TEST_F(AuthorizationSessionTest, CannotAggregateGraphLookupWithoutFindOnJoinedNa BSONArray pipeline = BSON_ARRAY(BSON("$graphLookup" << BSON("from" << testBarNss.coll()))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, CanAggregateGraphLookupWithFindOnJoinedNamespace) { @@ -1031,7 +1071,9 @@ TEST_F(AuthorizationSessionTest, CanAggregateGraphLookupWithFindOnJoinedNamespac BSONArray pipeline = BSON_ARRAY(BSON("$graphLookup" << BSON("from" << testBarNss.coll()))); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, @@ -1044,20 +1086,19 @@ TEST_F(AuthorizationSessionTest, "[{$graphLookup: {from: 'qux'}}]}}")); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); // We have find on the $lookup namespace but not on the $graphLookup namespace. authzSession->assumePrivilegesForDB({Privilege(testFooCollResource, {ActionType::find}), Privilege(testBarCollResource, {ActionType::find})}); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); // We have find on the $graphLookup namespace but not on the $lookup namespace. authzSession->assumePrivilegesForDB({Privilege(testFooCollResource, {ActionType::find}), Privilege(testQuxCollResource, {ActionType::find})}); - ASSERT_EQ(ErrorCodes::Unauthorized, - authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + ASSERT_FALSE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, @@ -1071,7 +1112,9 @@ TEST_F(AuthorizationSessionTest, "[{$graphLookup: {from: 'qux'}}]}}")); BSONObj cmdObj = BSON("aggregate" << testFooNss.coll() << "pipeline" << pipeline << "cursor" << BSONObj()); - ASSERT_OK(authzSession->checkAuthForAggregate(testFooNss, cmdObj, false)); + PrivilegeVector privileges = + uassertStatusOK(authzSession->getPrivilegesForAggregate(testFooNss, cmdObj, true)); + ASSERT_TRUE(authzSession->isAuthorizedForPrivileges(privileges)); } TEST_F(AuthorizationSessionTest, UnauthorizedSessionIsCoauthorizedWithEmptyUserSet) { @@ -1225,9 +1268,12 @@ TEST_F(AuthorizationSessionTest, TEST_F(AuthorizationSessionTest, CannotListCollectionsWithoutListCollectionsPrivilege) { BSONObj cmd = BSON("listCollections" << 1); // With no privileges, there is not authorization to list collections - ASSERT_FALSE(authzSession->isAuthorizedToListCollections(testFooNss.db(), cmd)); - ASSERT_FALSE(authzSession->isAuthorizedToListCollections(testBarNss.db(), cmd)); - ASSERT_FALSE(authzSession->isAuthorizedToListCollections(testQuxNss.db(), cmd)); + ASSERT_EQ(ErrorCodes::Unauthorized, + authzSession->checkAuthorizedToListCollections(testFooNss.db(), cmd).getStatus()); + ASSERT_EQ(ErrorCodes::Unauthorized, + authzSession->checkAuthorizedToListCollections(testBarNss.db(), cmd).getStatus()); + ASSERT_EQ(ErrorCodes::Unauthorized, + authzSession->checkAuthorizedToListCollections(testQuxNss.db(), cmd).getStatus()); } TEST_F(AuthorizationSessionTest, CanListCollectionsWithListCollectionsPrivilege) { @@ -1235,9 +1281,9 @@ TEST_F(AuthorizationSessionTest, CanListCollectionsWithListCollectionsPrivilege) // The listCollections privilege authorizes the list collections command. authzSession->assumePrivilegesForDB(Privilege(testDBResource, {ActionType::listCollections})); - ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testFooNss.db(), cmd)); - ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testBarNss.db(), cmd)); - ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testQuxNss.db(), cmd)); + ASSERT_OK(authzSession->checkAuthorizedToListCollections(testFooNss.db(), cmd).getStatus()); + ASSERT_OK(authzSession->checkAuthorizedToListCollections(testBarNss.db(), cmd).getStatus()); + ASSERT_OK(authzSession->checkAuthorizedToListCollections(testQuxNss.db(), cmd).getStatus()); } TEST_F(AuthorizationSessionTest, CanListOwnCollectionsWithPrivilege) { @@ -1246,11 +1292,12 @@ TEST_F(AuthorizationSessionTest, CanListOwnCollectionsWithPrivilege) { // The listCollections privilege authorizes the list collections command. authzSession->assumePrivilegesForDB(Privilege(testFooCollResource, {ActionType::find})); - ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testFooNss.db(), cmd)); - ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testBarNss.db(), cmd)); - ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testQuxNss.db(), cmd)); + ASSERT_OK(authzSession->checkAuthorizedToListCollections(testFooNss.db(), cmd).getStatus()); + ASSERT_OK(authzSession->checkAuthorizedToListCollections(testBarNss.db(), cmd).getStatus()); + ASSERT_OK(authzSession->checkAuthorizedToListCollections(testQuxNss.db(), cmd).getStatus()); - ASSERT_FALSE(authzSession->isAuthorizedToListCollections("other", cmd)); + ASSERT_EQ(ErrorCodes::Unauthorized, + authzSession->checkAuthorizedToListCollections("other", cmd).getStatus()); } TEST_F(AuthorizationSessionTest, CanCheckIfHasAnyPrivilegeOnResource) { diff --git a/src/mongo/db/clientcursor.cpp b/src/mongo/db/clientcursor.cpp index 41c2b814d5c..fef5105a9a5 100644 --- a/src/mongo/db/clientcursor.cpp +++ b/src/mongo/db/clientcursor.cpp @@ -90,6 +90,7 @@ ClientCursor::ClientCursor(ClientCursorParams params, _txnNumber(operationUsingCursor->getTxnNumber()), _readConcernArgs(params.readConcernArgs), _originatingCommand(params.originatingCommandObj), + _originatingPrivileges(std::move(params.originatingPrivileges)), _queryOptions(params.queryOptions), _lockPolicy(params.lockPolicy), _exec(std::move(params.exec)), diff --git a/src/mongo/db/clientcursor.h b/src/mongo/db/clientcursor.h index 8bbc5491ee2..e4f447ddc59 100644 --- a/src/mongo/db/clientcursor.h +++ b/src/mongo/db/clientcursor.h @@ -32,6 +32,7 @@ #include <boost/optional.hpp> +#include "mongo/db/auth/privilege.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/cursor_id.h" #include "mongo/db/jsobj.h" @@ -80,7 +81,8 @@ struct ClientCursorParams { UserNameIterator authenticatedUsersIter, repl::ReadConcernArgs readConcernArgs, BSONObj originatingCommandObj, - LockPolicy lockPolicy) + LockPolicy lockPolicy, + PrivilegeVector originatingPrivileges) : exec(std::move(planExecutor)), nss(std::move(nss)), readConcernArgs(readConcernArgs), @@ -88,7 +90,8 @@ struct ClientCursorParams { ? exec->getCanonicalQuery()->getQueryRequest().getOptions() : 0), originatingCommandObj(originatingCommandObj.getOwned()), - lockPolicy(lockPolicy) { + lockPolicy(lockPolicy), + originatingPrivileges(std::move(originatingPrivileges)) { while (authenticatedUsersIter.more()) { authenticatedUsers.emplace_back(authenticatedUsersIter.next()); } @@ -115,6 +118,7 @@ struct ClientCursorParams { int queryOptions = 0; BSONObj originatingCommandObj; const LockPolicy lockPolicy; + PrivilegeVector originatingPrivileges; }; /** @@ -186,11 +190,23 @@ public: return _queryOptions & QueryOption_AwaitData; } + /** + * Returns the original command object which created this cursor. + */ const BSONObj& getOriginatingCommandObj() const { return _originatingCommand; } /** + * Returns the privileges required to run a getMore against this cursor. This is the same as the + * set of privileges which would have been required to create the cursor in the first place. + */ + const PrivilegeVector& getOriginatingPrivileges() const& { + return _originatingPrivileges; + } + void getOriginatingPrivileges() && = delete; + + /** * Returns the total number of query results returned by the cursor so far. */ std::uint64_t nReturnedSoFar() const { @@ -369,6 +385,9 @@ private: // Holds an owned copy of the command specification received from the client. const BSONObj _originatingCommand; + // The privileges required for the _originatingCommand. + const PrivilegeVector _originatingPrivileges; + // See the QueryOptions enum in dbclientinterface.h. const int _queryOptions = 0; diff --git a/src/mongo/db/commands/count_cmd.cpp b/src/mongo/db/commands/count_cmd.cpp index a6b323b11f7..818ea1400e4 100644 --- a/src/mongo/db/commands/count_cmd.cpp +++ b/src/mongo/db/commands/count_cmd.cpp @@ -146,10 +146,13 @@ public: return viewAggRequest.getStatus(); } + // An empty PrivilegeVector is acceptable because these privileges are only checked on + // getMore and explain will not open a cursor. return runAggregate(opCtx, viewAggRequest.getValue().getNamespaceString(), viewAggRequest.getValue(), viewAggregation.getValue(), + PrivilegeVector(), result); } diff --git a/src/mongo/db/commands/current_op.cpp b/src/mongo/db/commands/current_op.cpp index 0e76cfcd4a9..28eed7396e7 100644 --- a/src/mongo/db/commands/current_op.cpp +++ b/src/mongo/db/commands/current_op.cpp @@ -70,8 +70,17 @@ public: rpc::OpMsgReplyBuilder replyBuilder; - auto status = runAggregate( - opCtx, request.getNamespaceString(), request, std::move(aggCmdObj), &replyBuilder); + PrivilegeVector privileges; + if (!aggCmdObj["$ownOps"].trueValue()) { + privileges = {Privilege(ResourcePattern::forClusterResource(), ActionType::inprog)}; + } + + auto status = runAggregate(opCtx, + request.getNamespaceString(), + request, + std::move(aggCmdObj), + privileges, + &replyBuilder); if (!status.isOK()) { return status; diff --git a/src/mongo/db/commands/distinct.cpp b/src/mongo/db/commands/distinct.cpp index 4d0f1dd70ea..f26feb2cf80 100644 --- a/src/mongo/db/commands/distinct.cpp +++ b/src/mongo/db/commands/distinct.cpp @@ -144,8 +144,14 @@ public: return viewAggRequest.getStatus(); } - return runAggregate( - opCtx, nss, viewAggRequest.getValue(), viewAggregation.getValue(), result); + // An empty PrivilegeVector is acceptable because these privileges are only checked on + // getMore and explain will not open a cursor. + return runAggregate(opCtx, + nss, + viewAggRequest.getValue(), + viewAggregation.getValue(), + PrivilegeVector(), + result); } Collection* const collection = ctx->getCollection(); diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp index cfcdecc9fb8..e9a20684a6b 100644 --- a/src/mongo/db/commands/find_cmd.cpp +++ b/src/mongo/db/commands/find_cmd.cpp @@ -192,8 +192,10 @@ public: AggregationRequest::parseFromBSON(nss, viewAggregationCommand, verbosity)); try { - uassertStatusOK( - runAggregate(opCtx, nss, aggRequest, viewAggregationCommand, result)); + // An empty PrivilegeVector is acceptable because these privileges are only + // checked on getMore and explain will not open a cursor. + uassertStatusOK(runAggregate( + opCtx, nss, aggRequest, viewAggregationCommand, PrivilegeVector(), result)); } catch (DBException& error) { if (error.code() == ErrorCodes::InvalidPipelineOperator) { uasserted(ErrorCodes::InvalidPipelineOperator, @@ -404,7 +406,8 @@ public: AuthorizationSession::get(opCtx->getClient())->getAuthenticatedUserNames(), repl::ReadConcernArgs::get(opCtx), _request.body, - ClientCursorParams::LockPolicy::kLockExternally}); + ClientCursorParams::LockPolicy::kLockExternally, + {Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find)}}); cursorId = pinnedCursor.getCursor()->cursorid(); invariant(!exec); diff --git a/src/mongo/db/commands/getmore_cmd.cpp b/src/mongo/db/commands/getmore_cmd.cpp index 8db406d77e2..c91eef49164 100644 --- a/src/mongo/db/commands/getmore_cmd.cpp +++ b/src/mongo/db/commands/getmore_cmd.cpp @@ -363,13 +363,20 @@ public: // A user can only call getMore on their own cursor. If there were multiple users // authenticated when the cursor was created, then at least one of them must be // authenticated in order to run getMore on the cursor. - if (!AuthorizationSession::get(opCtx->getClient()) - ->isCoauthorizedWith(cursorPin->getAuthenticatedUsers())) { + auto authzSession = AuthorizationSession::get(opCtx->getClient()); + if (!authzSession->isCoauthorizedWith(cursorPin->getAuthenticatedUsers())) { uasserted(ErrorCodes::Unauthorized, str::stream() << "cursor id " << _request.cursorid << " was not created by the authenticated user"); } + // Ensure that the client still has the privileges to run the originating command. + if (!authzSession->isAuthorizedForPrivileges(cursorPin->getOriginatingPrivileges())) { + uasserted(ErrorCodes::Unauthorized, + str::stream() << "not authorized for getMore with cursor id " + << _request.cursorid); + } + if (_request.nss != cursorPin->nss()) { uasserted(ErrorCodes::Unauthorized, str::stream() << "Requested getMore on namespace '" << _request.nss.ns() diff --git a/src/mongo/db/commands/list_collections.cpp b/src/mongo/db/commands/list_collections.cpp index 4277b8455aa..a702a8f6807 100644 --- a/src/mongo/db/commands/list_collections.cpp +++ b/src/mongo/db/commands/list_collections.cpp @@ -233,7 +233,7 @@ public: AuthorizationSession* authzSession = AuthorizationSession::get(client); - if (authzSession->isAuthorizedToListCollections(dbname, cmdObj)) { + if (authzSession->checkAuthorizedToListCollections(dbname, cmdObj).isOK()) { return Status::OK(); } @@ -386,7 +386,9 @@ public: AuthorizationSession::get(opCtx->getClient())->getAuthenticatedUserNames(), repl::ReadConcernArgs::get(opCtx), jsobj, - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + uassertStatusOK(AuthorizationSession::get(opCtx->getClient()) + ->checkAuthorizedToListCollections(dbname, jsobj))}); appendCursorResponseObject( pinnedCursor.getCursor()->cursorid(), cursorNss.ns(), firstBatch.arr(), &result); diff --git a/src/mongo/db/commands/list_indexes.cpp b/src/mongo/db/commands/list_indexes.cpp index b1d2796bb31..2210639210d 100644 --- a/src/mongo/db/commands/list_indexes.cpp +++ b/src/mongo/db/commands/list_indexes.cpp @@ -226,7 +226,8 @@ public: AuthorizationSession::get(opCtx->getClient())->getAuthenticatedUserNames(), repl::ReadConcernArgs::get(opCtx), cmdObj, - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + {Privilege(ResourcePattern::forExactNamespace(nss), ActionType::listIndexes)}}); appendCursorResponseObject( pinnedCursor.getCursor()->cursorid(), nss.ns(), firstBatch.arr(), &result); diff --git a/src/mongo/db/commands/pipeline_command.cpp b/src/mongo/db/commands/pipeline_command.cpp index edcd4af42b1..ef5dc99606b 100644 --- a/src/mongo/db/commands/pipeline_command.cpp +++ b/src/mongo/db/commands/pipeline_command.cpp @@ -53,15 +53,24 @@ public: std::unique_ptr<CommandInvocation> parse(OperationContext* opCtx, const OpMsgRequest& opMsgRequest) override { // TODO: Parsing to a Pipeline and/or AggregationRequest here. - return std::make_unique<Invocation>(this, opMsgRequest); + + auto privileges = + uassertStatusOK(AuthorizationSession::get(opCtx->getClient()) + ->getPrivilegesForAggregate( + AggregationRequest::parseNs( + opMsgRequest.getDatabase().toString(), opMsgRequest.body), + opMsgRequest.body, + false)); + return std::make_unique<Invocation>(this, opMsgRequest, std::move(privileges)); } class Invocation final : public CommandInvocation { public: - Invocation(Command* cmd, const OpMsgRequest& request) + Invocation(Command* cmd, const OpMsgRequest& request, PrivilegeVector privileges) : CommandInvocation(cmd), _request(request), - _dbName(request.getDatabase().toString()) {} + _dbName(request.getDatabase().toString()), + _privileges(std::move(privileges)) {} private: bool supportsWriteConcern() const override { @@ -90,11 +99,11 @@ public: void run(OperationContext* opCtx, rpc::ReplyBuilderInterface* reply) override { const auto aggregationRequest = uassertStatusOK( AggregationRequest::parseFromBSON(_dbName, _request.body, boost::none)); - uassertStatusOK(runAggregate(opCtx, aggregationRequest.getNamespaceString(), aggregationRequest, _request.body, + _privileges, reply)); } @@ -112,17 +121,20 @@ public: aggregationRequest.getNamespaceString(), aggregationRequest, _request.body, + _privileges, result)); } void doCheckAuthorization(OperationContext* opCtx) const override { - const auto nss = ns(); - uassertStatusOK(AuthorizationSession::get(opCtx->getClient()) - ->checkAuthForAggregate(nss, _request.body, false)); + uassert(ErrorCodes::Unauthorized, + "unauthorized", + AuthorizationSession::get(opCtx->getClient()) + ->isAuthorizedForPrivileges(_privileges)); } const OpMsgRequest& _request; const std::string _dbName; + const PrivilegeVector _privileges; }; std::string help() const override { diff --git a/src/mongo/db/commands/repair_cursor.cpp b/src/mongo/db/commands/repair_cursor.cpp index b21d2c2e23c..04c4d29fc49 100644 --- a/src/mongo/db/commands/repair_cursor.cpp +++ b/src/mongo/db/commands/repair_cursor.cpp @@ -108,7 +108,8 @@ public: AuthorizationSession::get(opCtx->getClient())->getAuthenticatedUserNames(), repl::ReadConcernArgs::get(opCtx), cmdObj, - ClientCursorParams::LockPolicy::kLockExternally}); + ClientCursorParams::LockPolicy::kLockExternally, + {Privilege(parseResourcePattern(dbname, cmdObj), ActionType::find)}}); appendCursorResponseObject( pinnedCursor.getCursor()->cursorid(), ns.ns(), BSONArray(), &result); diff --git a/src/mongo/db/commands/run_aggregate.cpp b/src/mongo/db/commands/run_aggregate.cpp index 09ac63f0349..a8cefeb2caf 100644 --- a/src/mongo/db/commands/run_aggregate.cpp +++ b/src/mongo/db/commands/run_aggregate.cpp @@ -392,6 +392,7 @@ Status runAggregate(OperationContext* opCtx, const NamespaceString& origNss, const AggregationRequest& request, const BSONObj& cmdObj, + const PrivilegeVector& privileges, rpc::ReplyBuilderInterface* result) { // For operations on views, this will be the underlying namespace. NamespaceString nss = request.getNamespaceString(); @@ -516,7 +517,7 @@ Status runAggregate(OperationContext* opCtx, auto newRequest = resolvedView.asExpandedViewAggregation(request); auto newCmd = newRequest.serializeToCommandObj().toBson(); - auto status = runAggregate(opCtx, origNss, newRequest, newCmd, result); + auto status = runAggregate(opCtx, origNss, newRequest, newCmd, privileges, result); { // Set the namespace of the curop back to the view namespace so ctx records @@ -628,7 +629,6 @@ Status runAggregate(OperationContext* opCtx, p.deleteUnderlying(); } }); - for (size_t idx = 0; idx < execs.size(); ++idx) { ClientCursorParams cursorParams( std::move(execs[idx]), @@ -636,7 +636,8 @@ Status runAggregate(OperationContext* opCtx, AuthorizationSession::get(opCtx->getClient())->getAuthenticatedUserNames(), repl::ReadConcernArgs::get(opCtx), cmdObj, - ClientCursorParams::LockPolicy::kLocksInternally); + ClientCursorParams::LockPolicy::kLocksInternally, + privileges); if (expCtx->tailableMode == TailableModeEnum::kTailable) { cursorParams.setTailable(true); } else if (expCtx->tailableMode == TailableModeEnum::kTailableAndAwaitData) { diff --git a/src/mongo/db/commands/run_aggregate.h b/src/mongo/db/commands/run_aggregate.h index 941d6036009..983161ac568 100644 --- a/src/mongo/db/commands/run_aggregate.h +++ b/src/mongo/db/commands/run_aggregate.h @@ -32,6 +32,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" +#include "mongo/db/auth/privilege.h" #include "mongo/db/namespace_string.h" #include "mongo/db/operation_context.h" #include "mongo/db/pipeline/aggregation_request.h" @@ -45,12 +46,16 @@ namespace mongo { * The raw aggregate command parameters should be passed in 'cmdObj', and will be reported as the * originatingCommand in subsequent getMores on the resulting agg cursor. * + * 'privileges' contains the privileges that were required to run this aggregation, to be used later + * for re-checking privileges for getMore commands. + * * On success, fills out 'result' with the command response. */ Status runAggregate(OperationContext* opCtx, const NamespaceString& nss, const AggregationRequest& request, const BSONObj& cmdObj, + const PrivilegeVector& privileges, rpc::ReplyBuilderInterface* result); } // namespace mongo diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index f1cd1574494..b2b911c880a 100644 --- a/src/mongo/db/commands/user_management_commands.cpp +++ b/src/mongo/db/commands/user_management_commands.cpp @@ -1374,10 +1374,12 @@ public: rpc::OpMsgReplyBuilder replyBuilder; AggregationRequest aggRequest(AuthorizationManager::usersCollectionNamespace, std::move(pipeline)); + // Impose no cursor privilege requirements, as cursor is drained internally uassertStatusOK(runAggregate(opCtx, AuthorizationManager::usersCollectionNamespace, aggRequest, aggRequest.serializeToCommandObj().toBson(), + PrivilegeVector(), &replyBuilder)); auto bodyBuilder = replyBuilder.getBodyBuilder(); CommandHelpers::appendSimpleCommandStatus(bodyBuilder, true); diff --git a/src/mongo/db/query/find.cpp b/src/mongo/db/query/find.cpp index da3ff98fb0c..a6aa3fc48ac 100644 --- a/src/mongo/db/query/find.cpp +++ b/src/mongo/db/query/find.cpp @@ -676,7 +676,8 @@ std::string runQuery(OperationContext* opCtx, AuthorizationSession::get(opCtx->getClient())->getAuthenticatedUserNames(), readConcernArgs, upconvertedQuery, - ClientCursorParams::LockPolicy::kLockExternally}); + ClientCursorParams::LockPolicy::kLockExternally, + {Privilege(ResourcePattern::forExactNamespace(nss), ActionType::find)}}); ccId = pinnedCursor.getCursor()->cursorid(); LOG(5) << "caching executor with cursorid " << ccId << " after returning " << numResults diff --git a/src/mongo/dbtests/cursor_manager_test.cpp b/src/mongo/dbtests/cursor_manager_test.cpp index 866881009e0..6de2ba0b3c4 100644 --- a/src/mongo/dbtests/cursor_manager_test.cpp +++ b/src/mongo/dbtests/cursor_manager_test.cpp @@ -87,7 +87,8 @@ public: {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}; + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}; } ClientCursorPin makeCursor(OperationContext* opCtx) { @@ -136,7 +137,8 @@ TEST_F(CursorManagerTest, ShouldBeAbleToKillPinnedCursor) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); auto cursorId = cursorPin.getCursor()->cursorid(); ASSERT_OK(cursorManager->killCursor(_opCtx.get(), cursorId, shouldAudit)); @@ -161,7 +163,8 @@ TEST_F(CursorManagerTest, ShouldBeAbleToKillPinnedCursorMultiClient) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); auto cursorId = cursorPin.getCursor()->cursorid(); @@ -196,7 +199,8 @@ TEST_F(CursorManagerTest, InactiveCursorShouldTimeout) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); ASSERT_EQ(0UL, cursorManager->timeoutCursors(_opCtx.get(), Date_t())); @@ -210,7 +214,8 @@ TEST_F(CursorManagerTest, InactiveCursorShouldTimeout) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); ASSERT_EQ(1UL, cursorManager->timeoutCursors(_opCtx.get(), Date_t::max())); ASSERT_EQ(0UL, cursorManager->numCursors()); } @@ -229,7 +234,8 @@ TEST_F(CursorManagerTest, InactivePinnedCursorShouldNotTimeout) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); // The pin is still in scope, so it should not time out. clock->advance(getDefaultCursorTimeoutMillis()); @@ -252,7 +258,8 @@ TEST_F(CursorManagerTest, MarkedAsKilledCursorsShouldBeDeletedOnCursorPin) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); auto cursorId = cursorPin->cursorid(); // A cursor will stay alive, but be marked as killed, if it is interrupted with a code other @@ -284,7 +291,8 @@ TEST_F(CursorManagerTest, InactiveKilledCursorsShouldTimeout) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); // A cursor will stay alive, but be marked as killed, if it is interrupted with a code other // than ErrorCodes::Interrupted or ErrorCodes::CursorKilled and then unpinned. @@ -315,7 +323,8 @@ TEST_F(CursorManagerTest, UsingACursorShouldUpdateTimeOfLastUse) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); auto usedCursorId = cursorPin.getCursor()->cursorid(); cursorPin.release(); @@ -327,7 +336,8 @@ TEST_F(CursorManagerTest, UsingACursorShouldUpdateTimeOfLastUse) { {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); // Advance the clock to simulate time passing. clock->advance(Milliseconds(1)); @@ -363,7 +373,8 @@ TEST_F(CursorManagerTest, CursorShouldNotTimeOutUntilIdleForLongEnoughAfterBeing {}, repl::ReadConcernArgs(repl::ReadConcernLevel::kLocalReadConcern), BSONObj(), - ClientCursorParams::LockPolicy::kLocksInternally}); + ClientCursorParams::LockPolicy::kLocksInternally, + PrivilegeVector()}); // Advance the clock to simulate time passing. clock->advance(getDefaultCursorTimeoutMillis() + Milliseconds(1)); diff --git a/src/mongo/embedded/embedded_auth_session.cpp b/src/mongo/embedded/embedded_auth_session.cpp index a3ea0de15d7..1171aa653b0 100644 --- a/src/mongo/embedded/embedded_auth_session.cpp +++ b/src/mongo/embedded/embedded_auth_session.cpp @@ -135,8 +135,10 @@ public: return Status::OK(); } - Status checkAuthForAggregate(const NamespaceString&, const BSONObj&, bool) override { - return Status::OK(); + StatusWith<PrivilegeVector> getPrivilegesForAggregate(const NamespaceString&, + const BSONObj&, + bool) override { + return PrivilegeVector(); } Status checkAuthForCreate(const NamespaceString&, const BSONObj&, bool) override { @@ -155,6 +157,11 @@ public: return Status::OK(); } + StatusWith<PrivilegeVector> checkAuthorizedToListCollections(StringData, + const BSONObj&) override { + return PrivilegeVector(); + } + bool isUsingLocalhostBypass() override { return false; } @@ -183,10 +190,6 @@ public: return true; } - bool isAuthorizedToListCollections(StringData, const BSONObj&) override { - return true; - } - bool isAuthorizedToChangeOwnCustomDataAsUser(const UserName&) override { return true; } diff --git a/src/mongo/s/commands/cluster_count_cmd.cpp b/src/mongo/s/commands/cluster_count_cmd.cpp index 58b54948b52..ce1f3dd5b96 100644 --- a/src/mongo/s/commands/cluster_count_cmd.cpp +++ b/src/mongo/s/commands/cluster_count_cmd.cpp @@ -274,10 +274,13 @@ public: } auto bodyBuilder = result->getBodyBuilder(); + // An empty PrivilegeVector is acceptable because these privileges are only checked on + // getMore and explain will not open a cursor. return ClusterAggregate::retryOnViewError(opCtx, aggRequestOnView.getValue(), *ex.extraInfo<ResolvedView>(), nss, + PrivilegeVector(), &bodyBuilder); } diff --git a/src/mongo/s/commands/cluster_current_op.cpp b/src/mongo/s/commands/cluster_current_op.cpp index 84e527fde77..20ae433ced4 100644 --- a/src/mongo/s/commands/cluster_current_op.cpp +++ b/src/mongo/s/commands/cluster_current_op.cpp @@ -77,7 +77,11 @@ private: BSONObjBuilder responseBuilder; auto status = ClusterAggregate::runAggregate( - opCtx, ClusterAggregate::Namespaces{nss, nss}, request, &responseBuilder); + opCtx, + ClusterAggregate::Namespaces{nss, nss}, + request, + {Privilege(ResourcePattern::forClusterResource(), ActionType::inprog)}, + &responseBuilder); if (!status.isOK()) { return status; diff --git a/src/mongo/s/commands/cluster_distinct_cmd.cpp b/src/mongo/s/commands/cluster_distinct_cmd.cpp index 46cfcaf1f95..0897f53dfa4 100644 --- a/src/mongo/s/commands/cluster_distinct_cmd.cpp +++ b/src/mongo/s/commands/cluster_distinct_cmd.cpp @@ -135,10 +135,13 @@ public: } auto bodyBuilder = result->getBodyBuilder(); + // An empty PrivilegeVector is acceptable because these privileges are only checked on + // getMore and explain will not open a cursor. return ClusterAggregate::retryOnViewError(opCtx, aggRequestOnView.getValue(), *ex.extraInfo<ResolvedView>(), nss, + PrivilegeVector(), &bodyBuilder); } diff --git a/src/mongo/s/commands/cluster_find_cmd.cpp b/src/mongo/s/commands/cluster_find_cmd.cpp index e838190e51a..64d5917db25 100644 --- a/src/mongo/s/commands/cluster_find_cmd.cpp +++ b/src/mongo/s/commands/cluster_find_cmd.cpp @@ -169,8 +169,14 @@ public: auto aggRequestOnView = uassertStatusOK( AggregationRequest::parseFromBSON(ns(), aggCmdOnView, verbosity)); - uassertStatusOK(ClusterAggregate::retryOnViewError( - opCtx, aggRequestOnView, *ex.extraInfo<ResolvedView>(), ns(), &bodyBuilder)); + // An empty PrivilegeVector is acceptable because these privileges are only checked + // on getMore and explain will not open a cursor. + uassertStatusOK(ClusterAggregate::retryOnViewError(opCtx, + aggRequestOnView, + *ex.extraInfo<ResolvedView>(), + ns(), + PrivilegeVector(), + &bodyBuilder)); } } @@ -215,7 +221,12 @@ public: auto bodyBuilder = result->getBodyBuilder(); uassertStatusOK(ClusterAggregate::retryOnViewError( - opCtx, aggRequestOnView, *ex.extraInfo<ResolvedView>(), ns(), &bodyBuilder)); + opCtx, + aggRequestOnView, + *ex.extraInfo<ResolvedView>(), + ns(), + {Privilege(ResourcePattern::forExactNamespace(ns()), ActionType::find)}, + &bodyBuilder)); } } diff --git a/src/mongo/s/commands/cluster_pipeline_cmd.cpp b/src/mongo/s/commands/cluster_pipeline_cmd.cpp index f1cb773895f..ce8e5dfd15e 100644 --- a/src/mongo/s/commands/cluster_pipeline_cmd.cpp +++ b/src/mongo/s/commands/cluster_pipeline_cmd.cpp @@ -47,16 +47,24 @@ public: std::unique_ptr<CommandInvocation> parse(OperationContext* opCtx, const OpMsgRequest& opMsgRequest) override { + auto privileges = + uassertStatusOK(AuthorizationSession::get(opCtx->getClient()) + ->getPrivilegesForAggregate( + AggregationRequest::parseNs( + opMsgRequest.getDatabase().toString(), opMsgRequest.body), + opMsgRequest.body, + true)); // TODO: Parsing to a Pipeline and/or AggregationRequest here. - return std::make_unique<Invocation>(this, opMsgRequest); + return std::make_unique<Invocation>(this, opMsgRequest, std::move(privileges)); } class Invocation final : public CommandInvocation { public: - Invocation(Command* cmd, const OpMsgRequest& request) + Invocation(Command* cmd, const OpMsgRequest& request, PrivilegeVector privileges) : CommandInvocation(cmd), _request(request), - _dbName(request.getDatabase().toString()) {} + _dbName(request.getDatabase().toString()), + _privileges(std::move(privileges)) {} private: bool supportsWriteConcern() const override { @@ -67,24 +75,31 @@ public: return true; } - static void _runAggCommand(OperationContext* opCtx, - const std::string& dbname, - const BSONObj& cmdObj, - boost::optional<ExplainOptions::Verbosity> verbosity, - BSONObjBuilder* result) { + void _runAggCommand(OperationContext* opCtx, + const std::string& dbname, + const BSONObj& cmdObj, + boost::optional<ExplainOptions::Verbosity> verbosity, + BSONObjBuilder* result) { const auto aggregationRequest = uassertStatusOK(AggregationRequest::parseFromBSON(dbname, cmdObj, verbosity)); - const auto& nss = aggregationRequest.getNamespaceString(); try { - uassertStatusOK(ClusterAggregate::runAggregate( - opCtx, ClusterAggregate::Namespaces{nss, nss}, aggregationRequest, result)); + uassertStatusOK( + ClusterAggregate::runAggregate(opCtx, + ClusterAggregate::Namespaces{nss, nss}, + aggregationRequest, + _privileges, + result)); } catch (const ExceptionFor<ErrorCodes::CommandOnShardedViewNotSupportedOnMongod>& ex) { // If the aggregation failed because the namespace is a view, re-run the command // with the resolved view pipeline and namespace. - uassertStatusOK(ClusterAggregate::retryOnViewError( - opCtx, aggregationRequest, *ex.extraInfo<ResolvedView>(), nss, result)); + uassertStatusOK(ClusterAggregate::retryOnViewError(opCtx, + aggregationRequest, + *ex.extraInfo<ResolvedView>(), + nss, + _privileges, + result)); } } @@ -101,9 +116,10 @@ public: } void doCheckAuthorization(OperationContext* opCtx) const override { - const auto nss = ns(); - uassertStatusOK(AuthorizationSession::get(opCtx->getClient()) - ->checkAuthForAggregate(nss, _request.body, true)); + uassert(ErrorCodes::Unauthorized, + "unauthorized", + AuthorizationSession::get(opCtx->getClient()) + ->isAuthorizedForPrivileges(_privileges)); } NamespaceString ns() const override { @@ -112,6 +128,7 @@ public: const OpMsgRequest& _request; const std::string _dbName; + const PrivilegeVector _privileges; }; std::string help() const override { diff --git a/src/mongo/s/commands/commands_public.cpp b/src/mongo/s/commands/commands_public.cpp index eb0a9f75704..d6f939f78b2 100644 --- a/src/mongo/s/commands/commands_public.cpp +++ b/src/mongo/s/commands/commands_public.cpp @@ -57,7 +57,8 @@ bool cursorCommandPassthrough(OperationContext* opCtx, const CachedDatabaseInfo& dbInfo, const BSONObj& cmdObj, const NamespaceString& nss, - BSONObjBuilder* out) { + BSONObjBuilder* out, + const PrivilegeVector& privileges) { auto response = executeCommandAgainstDatabasePrimary( opCtx, dbName, @@ -74,7 +75,8 @@ bool cursorCommandPassthrough(OperationContext* opCtx, cmdResponse.data, nss, Grid::get(opCtx)->getExecutorPool()->getArbitraryExecutor(), - Grid::get(opCtx)->getCursorManager())); + Grid::get(opCtx)->getCursorManager(), + privileges)); CommandHelpers::filterCommandReplyForPassthrough(transformedResponse, out); return true; @@ -338,7 +340,7 @@ public: const BSONObj& cmdObj) const final { AuthorizationSession* authzSession = AuthorizationSession::get(client); - if (authzSession->isAuthorizedToListCollections(dbname, cmdObj)) { + if (authzSession->checkAuthorizedToListCollections(dbname, cmdObj).isOK()) { return Status::OK(); } @@ -447,9 +449,15 @@ public: } return cursorCommandPassthrough( - opCtx, dbName, dbInfoStatus.getValue(), newCmd, nss, &result); + opCtx, + dbName, + dbInfoStatus.getValue(), + newCmd, + nss, + &result, + uassertStatusOK(AuthorizationSession::get(opCtx->getClient()) + ->checkAuthorizedToListCollections(dbName, cmdObj))); } - } cmdListCollections; class CmdListIndexes : public BasicCommand { @@ -498,7 +506,14 @@ public: const auto routingInfo = uassertStatusOK(Grid::get(opCtx)->catalogCache()->getCollectionRoutingInfo(opCtx, nss)); - return cursorCommandPassthrough(opCtx, nss.db(), routingInfo.db(), cmdObj, nss, &result); + return cursorCommandPassthrough( + opCtx, + nss.db(), + routingInfo.db(), + cmdObj, + nss, + &result, + {Privilege(ResourcePattern::forExactNamespace(nss), ActionType::listIndexes)}); } } cmdListIndexes; diff --git a/src/mongo/s/query/cluster_aggregate.cpp b/src/mongo/s/query/cluster_aggregate.cpp index 637df1c1b3c..f5de45fcec5 100644 --- a/src/mongo/s/query/cluster_aggregate.cpp +++ b/src/mongo/s/query/cluster_aggregate.cpp @@ -293,12 +293,12 @@ Shard::CommandResponse establishMergingShardCursor(OperationContext* opCtx, opCtx, ReadPreferenceSetting::get(opCtx), nss.db().toString(), mergeCmdObj, retryPolicy)); } -BSONObj establishMergingMongosCursor( - OperationContext* opCtx, - const AggregationRequest& request, - const NamespaceString& requestedNss, - const LiteParsedPipeline& liteParsedPipeline, - std::unique_ptr<Pipeline, PipelineDeleter> pipelineForMerging) { +BSONObj establishMergingMongosCursor(OperationContext* opCtx, + const AggregationRequest& request, + const NamespaceString& requestedNss, + const LiteParsedPipeline& liteParsedPipeline, + std::unique_ptr<Pipeline, PipelineDeleter> pipelineForMerging, + const PrivilegeVector& privileges) { ClusterClientCursorParams params(requestedNss, ReadPreferenceSetting::get(opCtx)); @@ -312,6 +312,7 @@ BSONObj establishMergingMongosCursor( : boost::optional<long long>(request.getBatchSize()); params.lsid = opCtx->getLogicalSessionId(); params.txnNumber = opCtx->getTxnNumber(); + params.originatingPrivileges = privileges; if (TransactionRouter::get(opCtx)) { params.isAutoCommit = false; @@ -577,7 +578,8 @@ Status runPipelineOnMongoS(const boost::intrusive_ptr<ExpressionContext>& expCtx const AggregationRequest& request, const LiteParsedPipeline& litePipe, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, - BSONObjBuilder* result) { + BSONObjBuilder* result, + const PrivilegeVector& privileges) { // We should never receive a pipeline which cannot run on mongoS. invariant(!expCtx->explain); invariant(pipeline->canRunOnMongos()); @@ -593,8 +595,8 @@ Status runPipelineOnMongoS(const boost::intrusive_ptr<ExpressionContext>& expCtx !pipeline->getSources().front()->constraints().requiresInputDocSource); // Register the new mongoS cursor, and retrieve the initial batch of results. - auto cursorResponse = - establishMergingMongosCursor(opCtx, request, requestedNss, litePipe, std::move(pipeline)); + auto cursorResponse = establishMergingMongosCursor( + opCtx, request, requestedNss, litePipe, std::move(pipeline), privileges); // We don't need to storePossibleCursor or propagate writeConcern errors; an $out pipeline // can never run on mongoS. Filter the command response and return immediately. @@ -609,7 +611,8 @@ Status dispatchMergingPipeline( const LiteParsedPipeline& litePipe, const boost::optional<CachedCollectionRoutingInfo>& routingInfo, sharded_agg_helpers::DispatchShardPipelineResults&& shardDispatchResults, - BSONObjBuilder* result) { + BSONObjBuilder* result, + const PrivilegeVector& privileges) { // We should never be in a situation where we call this function on a non-merge pipeline. invariant(shardDispatchResults.splitPipeline); auto* mergePipeline = shardDispatchResults.splitPipeline->mergePipeline.get(); @@ -640,7 +643,8 @@ Status dispatchMergingPipeline( request, litePipe, std::move(shardDispatchResults.splitPipeline->mergePipeline), - result); + result, + privileges); } // If we are not merging on mongoS, then this is not a $changeStream aggregation, and we @@ -662,8 +666,12 @@ Status dispatchMergingPipeline( auto mergeResponse = establishMergingShardCursor( opCtx, namespaces.executionNss, request, mergeCmdObj, mergingShardId); - auto mergeCursorResponse = uassertStatusOK(storePossibleCursor( - opCtx, namespaces.requestedNss, mergingShardId, mergeResponse, expCtx->tailableMode)); + auto mergeCursorResponse = uassertStatusOK(storePossibleCursor(opCtx, + namespaces.requestedNss, + mergingShardId, + mergeResponse, + privileges, + expCtx->tailableMode)); // Ownership for the shard cursors has been transferred to the merging shard. Dismiss the // ownership in the current merging pipeline such that when it goes out of scope it does not @@ -690,6 +698,7 @@ void appendEmptyResultSetWithStatus(OperationContext* opCtx, Status ClusterAggregate::runAggregate(OperationContext* opCtx, const Namespaces& namespaces, const AggregationRequest& request, + const PrivilegeVector& privileges, BSONObjBuilder* result) { uassert(51028, "Cannot specify exchange option to a mongos", !request.getExchangeSpec()); auto executionNsRoutingInfoStatus = @@ -739,7 +748,8 @@ Status ClusterAggregate::runAggregate(OperationContext* opCtx, litePipe.allowedToForwardFromMongos() && litePipe.allowedToPassthroughFromMongos() && !involvesShardedCollections) { const auto primaryShardId = routingInfo->db().primary()->getId(); - return aggPassthrough(opCtx, namespaces, primaryShardId, request, litePipe, result); + return aggPassthrough( + opCtx, namespaces, primaryShardId, request, litePipe, privileges, result); } // Populate the collection UUID and the appropriate collation to use. @@ -768,7 +778,7 @@ Status ClusterAggregate::runAggregate(OperationContext* opCtx, } return runPipelineOnMongoS( - expCtx, namespaces, request, litePipe, std::move(pipeline), result); + expCtx, namespaces, request, litePipe, std::move(pipeline), result, privileges); } // If not, split the pipeline as necessary and dispatch to the relevant shards. @@ -790,8 +800,11 @@ Status ClusterAggregate::runAggregate(OperationContext* opCtx, invariant(shardDispatchResults.remoteCursors.size() == 1); auto&& remoteCursor = std::move(shardDispatchResults.remoteCursors.front()); const auto shardId = remoteCursor->getShardId().toString(); - const auto reply = uassertStatusOK(storePossibleCursor( - opCtx, namespaces.requestedNss, std::move(remoteCursor), expCtx->tailableMode)); + const auto reply = uassertStatusOK(storePossibleCursor(opCtx, + namespaces.requestedNss, + std::move(remoteCursor), + privileges, + expCtx->tailableMode)); return appendCursorResponseToCommandResult(shardId, reply, result); } @@ -812,7 +825,8 @@ Status ClusterAggregate::runAggregate(OperationContext* opCtx, litePipe, routingInfo, std::move(shardDispatchResults), - result); + result, + privileges); } void ClusterAggregate::uassertAllShardsSupportExplain( @@ -839,6 +853,7 @@ Status ClusterAggregate::aggPassthrough(OperationContext* opCtx, const ShardId& shardId, const AggregationRequest& aggRequest, const LiteParsedPipeline& liteParsedPipeline, + const PrivilegeVector& privileges, BSONObjBuilder* out) { // Temporary hack. See comment on declaration for details. auto swShard = Grid::get(opCtx)->shardRegistry()->getShard(opCtx, shardId); @@ -878,7 +893,7 @@ Status ClusterAggregate::aggPassthrough(OperationContext* opCtx, ? TailableModeEnum::kTailableAndAwaitData : TailableModeEnum::kNormal; result = uassertStatusOK(storePossibleCursor( - opCtx, namespaces.requestedNss, shard->getId(), cmdResponse, tailMode)); + opCtx, namespaces.requestedNss, shard->getId(), cmdResponse, privileges, tailMode)); } // First append the properly constructed writeConcernError. It will then be skipped @@ -896,6 +911,7 @@ Status ClusterAggregate::retryOnViewError(OperationContext* opCtx, const AggregationRequest& request, const ResolvedView& resolvedView, const NamespaceString& requestedNss, + const PrivilegeVector& privileges, BSONObjBuilder* result, unsigned numberRetries) { if (numberRetries >= kMaxViewRetries) { @@ -918,7 +934,8 @@ Status ClusterAggregate::retryOnViewError(OperationContext* opCtx, nsStruct.requestedNss = requestedNss; nsStruct.executionNss = resolvedView.getNamespace(); - auto status = ClusterAggregate::runAggregate(opCtx, nsStruct, resolvedAggRequest, result); + auto status = + ClusterAggregate::runAggregate(opCtx, nsStruct, resolvedAggRequest, privileges, result); // If the underlying namespace was changed to a view during retry, then re-run the aggregation // on the new resolved namespace. @@ -927,6 +944,7 @@ Status ClusterAggregate::retryOnViewError(OperationContext* opCtx, resolvedAggRequest, *status.extraInfo<ResolvedView>(), requestedNss, + privileges, result, numberRetries + 1); } diff --git a/src/mongo/s/query/cluster_aggregate.h b/src/mongo/s/query/cluster_aggregate.h index 822ad3cc078..c2647d65656 100644 --- a/src/mongo/s/query/cluster_aggregate.h +++ b/src/mongo/s/query/cluster_aggregate.h @@ -77,23 +77,31 @@ public: * over which the aggregation will actually execute. Typically these two namespaces are the * same, but they may differ in the case of a query on a view. * + * 'privileges' contains the privileges that were required to run this aggregation, to be used + * later for re-checking privileges for GetMore commands. + * * On success, fills out 'result' with the command response. */ static Status runAggregate(OperationContext* opCtx, const Namespaces& namespaces, const AggregationRequest& request, + const PrivilegeVector& privileges, BSONObjBuilder* result); /** * Retries a command that was previously run on a view by resolving the view as an aggregation * against the underlying collection. * + * 'privileges' contains the privileges that were required to run this aggregation, to be used + * later for re-checking privileges for GetMore commands. + * * On success, populates 'result' with the command response. */ static Status retryOnViewError(OperationContext* opCtx, const AggregationRequest& request, const ResolvedView& resolvedView, const NamespaceString& requestedNss, + const PrivilegeVector& privileges, BSONObjBuilder* result, unsigned numberRetries = 0); @@ -106,6 +114,7 @@ private: const ShardId&, const AggregationRequest&, const LiteParsedPipeline&, + const PrivilegeVector& privileges, BSONObjBuilder* result); }; diff --git a/src/mongo/s/query/cluster_client_cursor.h b/src/mongo/s/query/cluster_client_cursor.h index 06d7efb4abf..e9e3afb6b0f 100644 --- a/src/mongo/s/query/cluster_client_cursor.h +++ b/src/mongo/s/query/cluster_client_cursor.h @@ -113,6 +113,13 @@ public: virtual BSONObj getOriginatingCommand() const = 0; /** + * Returns the privileges required to run a getMore against this cursor. This is the same as the + * set of privileges which would have been required to create the cursor in the first place. + */ + virtual const PrivilegeVector& getOriginatingPrivileges() const& = 0; + void getOriginatingPrivileges() && = delete; + + /** * Returns a reference to the vector of remote hosts involved in this operation. */ virtual std::size_t getNumRemotes() const = 0; diff --git a/src/mongo/s/query/cluster_client_cursor_impl.cpp b/src/mongo/s/query/cluster_client_cursor_impl.cpp index 3c620b77133..1d2e236ccb6 100644 --- a/src/mongo/s/query/cluster_client_cursor_impl.cpp +++ b/src/mongo/s/query/cluster_client_cursor_impl.cpp @@ -158,6 +158,10 @@ BSONObj ClusterClientCursorImpl::getOriginatingCommand() const { return _params.originatingCommandObj; } +const PrivilegeVector& ClusterClientCursorImpl::getOriginatingPrivileges() const& { + return _params.originatingPrivileges; +} + std::size_t ClusterClientCursorImpl::getNumRemotes() const { return _root->getNumRemotes(); } diff --git a/src/mongo/s/query/cluster_client_cursor_impl.h b/src/mongo/s/query/cluster_client_cursor_impl.h index c01cd89dfe3..77bc3d2b4e4 100644 --- a/src/mongo/s/query/cluster_client_cursor_impl.h +++ b/src/mongo/s/query/cluster_client_cursor_impl.h @@ -116,6 +116,9 @@ public: BSONObj getOriginatingCommand() const final; + const PrivilegeVector& getOriginatingPrivileges() const& final; + void getOriginatingPrivileges() && = delete; + std::size_t getNumRemotes() const final; BSONObj getPostBatchResumeToken() const final; diff --git a/src/mongo/s/query/cluster_client_cursor_mock.cpp b/src/mongo/s/query/cluster_client_cursor_mock.cpp index 9fdc048819d..55c06c8cbcf 100644 --- a/src/mongo/s/query/cluster_client_cursor_mock.cpp +++ b/src/mongo/s/query/cluster_client_cursor_mock.cpp @@ -71,6 +71,10 @@ BSONObj ClusterClientCursorMock::getOriginatingCommand() const { return _originatingCommand; } +const PrivilegeVector& ClusterClientCursorMock::getOriginatingPrivileges() const& { + return _originatingPrivileges; +} + std::size_t ClusterClientCursorMock::getNumRemotes() const { MONGO_UNREACHABLE; } diff --git a/src/mongo/s/query/cluster_client_cursor_mock.h b/src/mongo/s/query/cluster_client_cursor_mock.h index 960cb59d218..a7ff38efbd0 100644 --- a/src/mongo/s/query/cluster_client_cursor_mock.h +++ b/src/mongo/s/query/cluster_client_cursor_mock.h @@ -72,6 +72,9 @@ public: BSONObj getOriginatingCommand() const final; + const PrivilegeVector& getOriginatingPrivileges() const& final; + void getOriginatingPrivileges() && = delete; + std::size_t getNumRemotes() const final; BSONObj getPostBatchResumeToken() const final; @@ -120,6 +123,9 @@ private: // Originating command object. BSONObj _originatingCommand; + // Privileges of originating command + PrivilegeVector _originatingPrivileges; + // Number of returned documents. long long _numReturnedSoFar = 0; diff --git a/src/mongo/s/query/cluster_client_cursor_params.h b/src/mongo/s/query/cluster_client_cursor_params.h index 7db927ed78e..37a0c9d2bbd 100644 --- a/src/mongo/s/query/cluster_client_cursor_params.h +++ b/src/mongo/s/query/cluster_client_cursor_params.h @@ -37,6 +37,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/client/read_preference.h" +#include "mongo/db/auth/privilege.h" #include "mongo/db/auth/user_name.h" #include "mongo/db/cursor_id.h" #include "mongo/db/namespace_string.h" @@ -110,6 +111,9 @@ struct ClusterClientCursorParams { // The original command object which generated this cursor. Must either be empty or owned. BSONObj originatingCommandObj; + // The privileges required for the originatingCommand. + PrivilegeVector originatingPrivileges; + // Per-remote node data. std::vector<RemoteCursor> remotes; diff --git a/src/mongo/s/query/cluster_cursor_manager.cpp b/src/mongo/s/query/cluster_cursor_manager.cpp index 98cbb05f2b5..511217eea8c 100644 --- a/src/mongo/s/query/cluster_cursor_manager.cpp +++ b/src/mongo/s/query/cluster_cursor_manager.cpp @@ -149,6 +149,11 @@ BSONObj ClusterCursorManager::PinnedCursor::getOriginatingCommand() const { return _cursor->getOriginatingCommand(); } +const PrivilegeVector& ClusterCursorManager::PinnedCursor::getOriginatingPrivileges() const& { + invariant(_cursor); + return _cursor->getOriginatingPrivileges(); +} + std::size_t ClusterCursorManager::PinnedCursor::getNumRemotes() const { invariant(_cursor); return _cursor->getNumRemotes(); diff --git a/src/mongo/s/query/cluster_cursor_manager.h b/src/mongo/s/query/cluster_cursor_manager.h index 9e7d7a7b61c..886a43bce90 100644 --- a/src/mongo/s/query/cluster_cursor_manager.h +++ b/src/mongo/s/query/cluster_cursor_manager.h @@ -194,6 +194,13 @@ public: BSONObj getOriginatingCommand() const; /** + * Returns the privleges for the original command object which created this cursor. + */ + + const PrivilegeVector& getOriginatingPrivileges() const&; + void getOriginatingPrivileges() && = delete; + + /** * Returns a reference to the vector of remote hosts involved in this operation. */ std::size_t getNumRemotes() const; diff --git a/src/mongo/s/query/cluster_find.cpp b/src/mongo/s/query/cluster_find.cpp index e9f55a271e6..4673003816e 100644 --- a/src/mongo/s/query/cluster_find.cpp +++ b/src/mongo/s/query/cluster_find.cpp @@ -233,6 +233,8 @@ CursorId runQueryWithoutRetrying(OperationContext* opCtx, params.isAllowPartialResults = query.getQueryRequest().isAllowPartialResults(); params.lsid = opCtx->getLogicalSessionId(); params.txnNumber = opCtx->getTxnNumber(); + params.originatingPrivileges = { + Privilege(ResourcePattern::forExactNamespace(query.nss()), ActionType::find)}; if (TransactionRouter::get(opCtx)) { params.isAutoCommit = false; @@ -565,6 +567,14 @@ StatusWith<CursorResponse> ClusterFind::runGetMore(OperationContext* opCtx, validateOperationSessionInfo(opCtx, request, &pinnedCursor.getValue()); + // Ensure that the client still has the privileges to run the originating command. + if (!authzSession->isAuthorizedForPrivileges( + pinnedCursor.getValue().getOriginatingPrivileges())) { + uasserted(ErrorCodes::Unauthorized, + str::stream() << "not authorized for getMore with cursor id " + << request.cursorid); + } + // Set the originatingCommand object and the cursorID in CurOp. { CurOp::get(opCtx)->debug().nShards = pinnedCursor.getValue().getNumRemotes(); diff --git a/src/mongo/s/query/store_possible_cursor.cpp b/src/mongo/s/query/store_possible_cursor.cpp index c0f04976bf9..28c052299b3 100644 --- a/src/mongo/s/query/store_possible_cursor.cpp +++ b/src/mongo/s/query/store_possible_cursor.cpp @@ -50,6 +50,7 @@ namespace mongo { StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const NamespaceString& requestedNss, OwnedRemoteCursor&& remoteCursor, + PrivilegeVector privileges, TailableModeEnum tailableMode) { auto executorPool = Grid::get(opCtx)->getExecutorPool(); auto result = storePossibleCursor( @@ -60,6 +61,7 @@ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, requestedNss, executorPool->getArbitraryExecutor(), Grid::get(opCtx)->getCursorManager(), + std::move(privileges), tailableMode); // On success, release ownership of the cursor because it has been registered with the cursor @@ -72,6 +74,7 @@ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const NamespaceString& requestedNss, const ShardId& shardId, const Shard::CommandResponse& commandResponse, + PrivilegeVector privileges, TailableModeEnum tailableMode) { invariant(commandResponse.hostAndPort); auto executorPool = Grid::get(opCtx)->getExecutorPool(); @@ -82,6 +85,7 @@ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, requestedNss, executorPool->getArbitraryExecutor(), Grid::get(opCtx)->getCursorManager(), + std::move(privileges), tailableMode); } @@ -92,6 +96,7 @@ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const NamespaceString& requestedNss, executor::TaskExecutor* executor, ClusterCursorManager* cursorManager, + PrivilegeVector privileges, TailableModeEnum tailableMode) { if (!cmdResult["ok"].trueValue() || !cmdResult.hasField("cursor")) { return cmdResult; @@ -126,6 +131,7 @@ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, params.tailableMode = tailableMode; params.lsid = opCtx->getLogicalSessionId(); params.txnNumber = opCtx->getTxnNumber(); + params.originatingPrivileges = std::move(privileges); if (TransactionRouter::get(opCtx)) { params.isAutoCommit = false; diff --git a/src/mongo/s/query/store_possible_cursor.h b/src/mongo/s/query/store_possible_cursor.h index 75c90738fe6..d145c76bcbf 100644 --- a/src/mongo/s/query/store_possible_cursor.h +++ b/src/mongo/s/query/store_possible_cursor.h @@ -30,6 +30,7 @@ #pragma once +#include "mongo/db/auth/privilege.h" #include "mongo/db/namespace_string.h" #include "mongo/db/operation_context.h" #include "mongo/db/query/tailable_mode.h" @@ -70,6 +71,8 @@ class TaskExecutor; * differ from the execution namespace if the command was issued on a view) * @ executor the TaskExecutor to store in the resulting ClusterClientCursor * @ cursorManager the ClusterCursorManager on which to register the resulting ClusterClientCursor + * @ privileges the PrivilegeVector of privileges needed for the original command, to be used for + * auth checking by GetMore */ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const ShardId& shardId, @@ -78,25 +81,30 @@ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const NamespaceString& requestedNss, executor::TaskExecutor* executor, ClusterCursorManager* cursorManager, + PrivilegeVector privileges, TailableModeEnum tailableMode = TailableModeEnum::kNormal); /** * Convenience function which extracts all necessary information from the passed RemoteCursor, and * stores a ClusterClientCursor based on it. The ownership of the remote cursor is transferred to - * this function, and will handle killing it upon failure. + * this function, and will handle killing it upon failure. 'privileges' contains the required + * privileges for the command, to be used by GetMore for auth checks. */ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const NamespaceString& requestedNss, OwnedRemoteCursor&& remoteCursor, + PrivilegeVector privileges, TailableModeEnum tailableMode); /** * Convenience function which extracts all necessary information from the passed CommandResponse, - * and stores a ClusterClientCursor based on it. + * and stores a ClusterClientCursor based on it. 'privileges' contains the required privileges for + * the command, to be used by GetMore for auth checks. */ StatusWith<BSONObj> storePossibleCursor(OperationContext* opCtx, const NamespaceString& requestedNss, const ShardId& shardId, const Shard::CommandResponse& commandResponse, + PrivilegeVector privileges, TailableModeEnum tailableMode); } // namespace mongo diff --git a/src/mongo/s/query/store_possible_cursor_test.cpp b/src/mongo/s/query/store_possible_cursor_test.cpp index 83eef02a50e..d0081d17d77 100644 --- a/src/mongo/s/query/store_possible_cursor_test.cpp +++ b/src/mongo/s/query/store_possible_cursor_test.cpp @@ -79,7 +79,8 @@ TEST_F(StorePossibleCursorTest, ReturnsValidCursorResponse) { cursorResponse.toBSON(CursorResponse::ResponseType::InitialResponse), nss, nullptr, // TaskExecutor - getManager()); + getManager(), + PrivilegeVector()); ASSERT_OK(outgoingCursorResponse.getStatus()); auto parsedOutgoingResponse = CursorResponse::parseFromBSON(outgoingCursorResponse.getValue()); @@ -99,7 +100,8 @@ TEST_F(StorePossibleCursorTest, FailsGracefullyOnBadCursorResponseDocument) { fromjson("{ok: 1, cursor: {}}"), nss, nullptr, // TaskExecutor - getManager()); + getManager(), + PrivilegeVector()); ASSERT_NOT_OK(outgoingCursorResponse.getStatus()); ASSERT_EQ(ErrorCodes::TypeMismatch, outgoingCursorResponse.getStatus()); } @@ -115,7 +117,8 @@ TEST_F(StorePossibleCursorTest, PassesUpCommandResultIfItDoesNotDescribeACursor) notACursorObj, nss, nullptr, // TaskExecutor - getManager()); + getManager(), + PrivilegeVector()); ASSERT_OK(outgoingCursorResponse.getStatus()); ASSERT_BSONOBJ_EQ(notACursorObj, outgoingCursorResponse.getValue()); } |