diff options
-rw-r--r-- | jstests/auth/commands_builtin_roles.js | 8 | ||||
-rw-r--r-- | jstests/auth/commands_user_defined_roles.js | 18 | ||||
-rw-r--r-- | jstests/auth/lib/commands_lib.js | 93 | ||||
-rw-r--r-- | src/mongo/db/auth/action_types.txt | 1 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session.cpp | 15 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session.h | 4 | ||||
-rw-r--r-- | src/mongo/db/auth/authorization_session_test.cpp | 29 | ||||
-rw-r--r-- | src/mongo/db/auth/role_graph_builtin_roles.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/commands/count_cmd.cpp | 22 | ||||
-rw-r--r-- | src/mongo/db/commands/find_cmd.cpp | 15 | ||||
-rw-r--r-- | src/mongo/db/commands/list_indexes.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/commands/parallel_collection_scan.cpp | 24 |
12 files changed, 208 insertions, 28 deletions
diff --git a/jstests/auth/commands_builtin_roles.js b/jstests/auth/commands_builtin_roles.js index 6de64cc5495..14da86a505b 100644 --- a/jstests/auth/commands_builtin_roles.js +++ b/jstests/auth/commands_builtin_roles.js @@ -48,10 +48,14 @@ function testProperAuthorization(conn, t, testcase, r) { var out = ""; var runOnDb = conn.getDB(testcase.runOnDb); - authCommandsLib.setup(conn, t, runOnDb); + var state = authCommandsLib.setup(conn, t, runOnDb); assert(r.db.auth("user|" + r.key, "password")); authCommandsLib.authenticatedSetup(t, runOnDb); - var res = runOnDb.runCommand(t.command); + var command = t.command; + if (typeof(command) === "function") { + command = t.command(state); + } + var res = runOnDb.runCommand(command); if (testcase.roles[r.key]) { if (res.ok == 0 && res.code == authErrCode) { diff --git a/jstests/auth/commands_user_defined_roles.js b/jstests/auth/commands_user_defined_roles.js index 6f5fd96b87b..3c8de27192f 100644 --- a/jstests/auth/commands_user_defined_roles.js +++ b/jstests/auth/commands_user_defined_roles.js @@ -23,7 +23,7 @@ function testProperAuthorization(conn, t, testcase, privileges) { var firstDb = conn.getDB(firstDbName); var adminDb = conn.getDB(adminDbName); - authCommandsLib.setup(conn, t, runOnDb); + var state = authCommandsLib.setup(conn, t, runOnDb); adminDb.auth("admin", "password"); assert.commandWorked(adminDb.runCommand({updateRole: testRole, privileges: privileges})); @@ -32,7 +32,12 @@ function testProperAuthorization(conn, t, testcase, privileges) { assert(adminDb.auth(testUser, "password")); authCommandsLib.authenticatedSetup(t, runOnDb); - var res = runOnDb.runCommand(t.command); + + var command = t.command; + if (typeof(command) === "function") { + command = t.command(state); + } + var res = runOnDb.runCommand(command); if (!testcase.expectFail && res.ok != 1 && res.code != commandNotSupportedCode) { // don't error if the test failed with code commandNotSupported since @@ -56,7 +61,7 @@ function testInsufficientPrivileges(conn, t, testcase, privileges) { var firstDb = conn.getDB(firstDbName); var adminDb = conn.getDB(adminDbName); - authCommandsLib.setup(conn, t, runOnDb); + var state = authCommandsLib.setup(conn, t, runOnDb); adminDb.auth("admin", "password"); assert.commandWorked(adminDb.runCommand({updateRole: testRole, privileges: privileges})); @@ -65,7 +70,12 @@ function testInsufficientPrivileges(conn, t, testcase, privileges) { assert(adminDb.auth(testUser, "password")); authCommandsLib.authenticatedSetup(t, runOnDb); - var res = runOnDb.runCommand(t.command); + + var command = t.command; + if (typeof(command) === "function") { + command = t.command(state); + } + var res = runOnDb.runCommand(command); if (res.ok == 1 || res.code != authErrCode) { out = "expected authorization failure " + " but received " + tojson(res) + diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 7cde9677e96..279b0d7f41d 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -163,6 +163,8 @@ var roles_all = { __system: 1 }; +load("jstests/libs/uuid_util.js"); + var authCommandsLib = { /************* TEST CASES ****************/ @@ -1731,6 +1733,31 @@ var authCommandsLib = { ] }, { + testname: "countWithUUID", + command: function(state) { + return {count: state.uuid}; + }, + skipSharded: true, + setup: function(db) { + db.runCommand({create: "foo"}); + + return {uuid: getUUIDFromListCollections(db, db.foo.getName())}; + }, + teardown: function(db) { + db.foo.drop(); + }, + testcases: [ + { + runOnDb: firstDbName, + roles: {__system: 1, root: 1, backup: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + {resource: {cluster: true}, actions: ["useUUID"]} + ], + }, + ] + }, + { testname: "_configsvrCommitChunkMerge", command: {_configsvrCommitChunkMerge: "x.y"}, skipSharded: true, @@ -2647,6 +2674,31 @@ var authCommandsLib = { ] }, { + testname: "findWithUUID", + command: function(state) { + return {find: state.uuid}; + }, + skipSharded: true, + setup: function(db) { + db.runCommand({create: "foo"}); + + return {uuid: getUUIDFromListCollections(db, db.foo.getName())}; + }, + teardown: function(db) { + db.foo.drop(); + }, + testcases: [ + { + runOnDb: firstDbName, + roles: {__system: 1, root: 1, backup: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + {resource: {cluster: true}, actions: ["useUUID"]} + ], + }, + ], + }, + { testname: "findAndModify", command: {findAndModify: "x", query: {_id: "abc"}, update: {$inc: {n: 1}}}, setup: function(db) { @@ -3341,6 +3393,42 @@ var authCommandsLib = { ] }, { + testname: "listIndexesWithUUID", + command: function(state) { + return {listIndexes: state.uuid}; + }, + skipSharded: true, + setup: function(db) { + db.x.insert({_id: 5}); + db.x.insert({_id: 6}); + + return {uuid: getUUIDFromListCollections(db, db.x.getName())}; + }, + teardown: function(db) { + db.x.drop(); + }, + testcases: [ + { + runOnDb: firstDbName, + roles: {backup: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: ""}, actions: ["listIndexes"]}, + {resource: {cluster: true}, actions: ["useUUID"]} + ] + }, + // Test legacy (pre 3.0) way of authorizing listIndexes. + { + runOnDb: firstDbName, + privileges: [ + {resource: {db: firstDbName, collection: "system.indexes"}, actions: ["find"]}, + {resource: {cluster: true}, actions: ["useUUID"]} + + ] + } + ] + }, + + { testname: "listShards", command: {listShards: 1}, skipStandalone: true, @@ -4511,10 +4599,13 @@ var authCommandsLib = { var adminDb = conn.getDB(adminDbName); if (t.setup) { adminDb.auth("admin", "password"); - t.setup(runOnDb); + var state = t.setup(runOnDb); runOnDb.getLastError(); adminDb.logout(); + return state; } + + return {}; }, authenticatedSetup: function(t, runOnDb) { diff --git a/src/mongo/db/auth/action_types.txt b/src/mongo/db/auth/action_types.txt index 1f57ba6084b..88d777a4950 100644 --- a/src/mongo/db/auth/action_types.txt +++ b/src/mongo/db/auth/action_types.txt @@ -106,6 +106,7 @@ "top", "touch", "unlock", +"useUUID", "update", "updateRole", # Not used for permissions checks, but to id the event in logs. "updateUser", # Not used for permissions checks, but to id the event in logs. diff --git a/src/mongo/db/auth/authorization_session.cpp b/src/mongo/db/auth/authorization_session.cpp index 023d5fc02a7..15ee3480a88 100644 --- a/src/mongo/db/auth/authorization_session.cpp +++ b/src/mongo/db/auth/authorization_session.cpp @@ -601,6 +601,21 @@ Status AuthorizationSession::checkAuthorizedToRevokePrivilege(const Privilege& p return Status::OK(); } +bool AuthorizationSession::isAuthorizedToParseNamespaceElement(const BSONElement& element) { + const bool isUUID = element.type() == BinData && element.binDataType() == BinDataType::newUUID; + + uassert(ErrorCodes::InvalidNamespace, + "Failed to parse namespace element", + element.type() == String || isUUID); + + if (isUUID) { + return isAuthorizedForActionsOnResource(ResourcePattern::forClusterResource(), + ActionType::useUUID); + } + + return true; +} + bool AuthorizationSession::isAuthorizedToCreateRole( const struct auth::CreateOrUpdateRoleArgs& args) { // A user is allowed to create a role under either of two conditions. diff --git a/src/mongo/db/auth/authorization_session.h b/src/mongo/db/auth/authorization_session.h index 1f41180df18..a1e159c80de 100644 --- a/src/mongo/db/auth/authorization_session.h +++ b/src/mongo/db/auth/authorization_session.h @@ -227,6 +227,10 @@ public: // Checks if this connection is using the localhost bypass bool isUsingLocalhostBypass(); + // Checks if this connection has the privileges necessary to parse a namespace from a + // given BSONElement. + bool isAuthorizedToParseNamespaceElement(const BSONElement& elem); + // Checks if this connection has the privileges necessary to create a new role bool isAuthorizedToCreateRole(const auth::CreateOrUpdateRoleArgs& args); diff --git a/src/mongo/db/auth/authorization_session_test.cpp b/src/mongo/db/auth/authorization_session_test.cpp index 308156c31ee..0ec66e94598 100644 --- a/src/mongo/db/auth/authorization_session_test.cpp +++ b/src/mongo/db/auth/authorization_session_test.cpp @@ -1278,5 +1278,34 @@ TEST_F(AuthorizationSessionTest, CanListCollectionsWithListCollectionsPrivilege) ASSERT_TRUE(authzSession->isAuthorizedToListCollections(testQuxNss.db())); } +TEST_F(AuthorizationSessionTest, CanUseUUIDNamespacesWithPrivilege) { + BSONObj stringObj = BSON("a" + << "string"); + BSONObj uuidObj = BSON("a" << UUID::gen()); + BSONObj invalidObj = BSON("a" << 12); + + // Strings require no privileges + ASSERT_TRUE(authzSession->isAuthorizedToParseNamespaceElement(stringObj.firstElement())); + + // UUIDs cannot be parsed with default privileges + ASSERT_FALSE(authzSession->isAuthorizedToParseNamespaceElement(uuidObj.firstElement())); + + // Element must be either a string, or a UUID + ASSERT_THROWS_CODE(authzSession->isAuthorizedToParseNamespaceElement(invalidObj.firstElement()), + AssertionException, + ErrorCodes::InvalidNamespace); + + // The useUUID privilege allows UUIDs to be parsed + authzSession->assumePrivilegesForDB( + Privilege(ResourcePattern::forClusterResource(), {ActionType::useUUID})); + + ASSERT_TRUE(authzSession->isAuthorizedToParseNamespaceElement(stringObj.firstElement())); + ASSERT_TRUE(authzSession->isAuthorizedToParseNamespaceElement(uuidObj.firstElement())); + ASSERT_THROWS_CODE(authzSession->isAuthorizedToParseNamespaceElement(invalidObj.firstElement()), + AssertionException, + ErrorCodes::InvalidNamespace); +} + + } // namespace } // namespace mongo diff --git a/src/mongo/db/auth/role_graph_builtin_roles.cpp b/src/mongo/db/auth/role_graph_builtin_roles.cpp index 31e8e1234ec..94b14014549 100644 --- a/src/mongo/db/auth/role_graph_builtin_roles.cpp +++ b/src/mongo/db/auth/role_graph_builtin_roles.cpp @@ -194,6 +194,7 @@ MONGO_INITIALIZER(AuthorizationBuiltinRoles)(InitializerContext* context) { << ActionType::replSetGetStatus // clusterManager gets this also << ActionType::serverStatus << ActionType::top + << ActionType::useUUID << ActionType::inprog << ActionType::shardingState; @@ -499,7 +500,7 @@ void addQueryableBackupPrivileges(PrivilegeVector* privileges) { ActionSet clusterActions; clusterActions << ActionType::getParameter // To check authSchemaVersion - << ActionType::listDatabases; + << ActionType::listDatabases << ActionType::useUUID; Privilege::addPrivilegeToPrivilegeVector( privileges, Privilege(ResourcePattern::forClusterResource(), clusterActions)); diff --git a/src/mongo/db/commands/count_cmd.cpp b/src/mongo/db/commands/count_cmd.cpp index 9ce6c54675e..d70c9ce6c80 100644 --- a/src/mongo/db/commands/count_cmd.cpp +++ b/src/mongo/db/commands/count_cmd.cpp @@ -30,6 +30,7 @@ #include "mongo/platform/basic.h" +#include "mongo/db/auth/authorization_session.h" #include "mongo/db/client.h" #include "mongo/db/commands.h" #include "mongo/db/commands/run_aggregate.h" @@ -91,12 +92,21 @@ public: help << "count objects in collection"; } - virtual void addRequiredPrivileges(const std::string& dbname, - const BSONObj& cmdObj, - std::vector<Privilege>* out) { - ActionSet actions; - actions.addAction(ActionType::find); - out->push_back(Privilege(parseResourcePattern(dbname, cmdObj), actions)); + Status checkAuthForOperation(OperationContext* opCtx, + const std::string& dbname, + const BSONObj& cmdObj) override { + AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + + if (!authSession->isAuthorizedToParseNamespaceElement(cmdObj.firstElement())) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + const NamespaceString nss(parseNsOrUUID(opCtx, dbname, cmdObj)); + if (!authSession->isAuthorizedForActionsOnNamespace(nss, ActionType::find)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + return Status::OK(); } virtual Status explain(OperationContext* opCtx, diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp index cbe0ce20d55..d3d9aafc096 100644 --- a/src/mongo/db/commands/find_cmd.cpp +++ b/src/mongo/db/commands/find_cmd.cpp @@ -123,12 +123,17 @@ public: return false; } - Status checkAuthForCommand(Client* client, - const std::string& dbname, - const BSONObj& cmdObj) override { - const NamespaceString nss(parseNs(dbname, cmdObj)); + Status checkAuthForOperation(OperationContext* opCtx, + const std::string& dbname, + const BSONObj& cmdObj) override { + AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + + if (!authSession->isAuthorizedToParseNamespaceElement(cmdObj.firstElement())) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + const NamespaceString nss(parseNsOrUUID(opCtx, dbname, cmdObj)); auto hasTerm = cmdObj.hasField(kTermField); - return AuthorizationSession::get(client)->checkAuthForFind(nss, hasTerm); + return authSession->checkAuthForFind(nss, hasTerm); } Status explain(OperationContext* opCtx, diff --git a/src/mongo/db/commands/list_indexes.cpp b/src/mongo/db/commands/list_indexes.cpp index 75d79099c39..d4e77888247 100644 --- a/src/mongo/db/commands/list_indexes.cpp +++ b/src/mongo/db/commands/list_indexes.cpp @@ -98,6 +98,10 @@ public: const BSONObj& cmdObj) { AuthorizationSession* authzSession = AuthorizationSession::get(client); + if (!authzSession->isAuthorizedToParseNamespaceElement(cmdObj.firstElement())) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + // Check for the listIndexes ActionType on the database, or find on system.indexes for pre // 3.0 systems. const NamespaceString ns(parseNsOrUUID(client->getOperationContext(), dbname, cmdObj)); diff --git a/src/mongo/db/commands/parallel_collection_scan.cpp b/src/mongo/db/commands/parallel_collection_scan.cpp index 6b84996369c..533daa6ab94 100644 --- a/src/mongo/db/commands/parallel_collection_scan.cpp +++ b/src/mongo/db/commands/parallel_collection_scan.cpp @@ -74,15 +74,21 @@ public: return ReadWriteType::kCommand; } - virtual Status checkAuthForCommand(Client* client, - const std::string& dbname, - const BSONObj& cmdObj) { - ActionSet actions; - actions.addAction(ActionType::find); - Privilege p(parseResourcePattern(dbname, cmdObj), actions); - if (AuthorizationSession::get(client)->isAuthorizedForPrivilege(p)) - return Status::OK(); - return Status(ErrorCodes::Unauthorized, "Unauthorized"); + Status checkAuthForOperation(OperationContext* opCtx, + const std::string& dbname, + const BSONObj& cmdObj) override { + AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + + if (!authSession->isAuthorizedToParseNamespaceElement(cmdObj.firstElement())) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + const NamespaceString ns(parseNsOrUUID(opCtx, dbname, cmdObj)); + if (!authSession->isAuthorizedForActionsOnNamespace(ns, ActionType::find)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + + return Status::OK(); } virtual bool run(OperationContext* opCtx, |