diff options
author | Moustafa Maher <m.maher@10gen.com> | 2021-03-03 00:08:53 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-08-04 23:13:58 +0000 |
commit | cbec187266a9f902b3906ae8ccef2bbda0c5b27b (patch) | |
tree | bfdc7763f15aa6c75c8a76736c4f18a2dc5e7639 | |
parent | ae14292c2cbb577c32eb2d10beacf7dd30bfb2c8 (diff) | |
download | mongo-cbec187266a9f902b3906ae8ccef2bbda0c5b27b.tar.gz |
SERVER-36263 Bypassing operation validation in applyOps should require special privilege
-rw-r--r-- | jstests/auth/applyOps_privilege.js | 92 | ||||
-rw-r--r-- | jstests/auth/lib/commands_lib.js | 67 | ||||
-rw-r--r-- | src/mongo/db/auth/action_types.txt | 1 | ||||
-rw-r--r-- | src/mongo/db/auth/role_graph_builtin_roles.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/commands/oplog_application_checks.cpp | 5 |
5 files changed, 134 insertions, 33 deletions
diff --git a/jstests/auth/applyOps_privilege.js b/jstests/auth/applyOps_privilege.js new file mode 100644 index 00000000000..26423c52b24 --- /dev/null +++ b/jstests/auth/applyOps_privilege.js @@ -0,0 +1,92 @@ +// Tests that a user can only run a applyops while having applyOps privilege. +(function() { +"use strict"; + +// Special privilege required to run applyOps command. +// Role dbAdminAnyDatabase has this privilege. +const applyOps_priv = { + resource: {cluster: true}, + actions: ["applyOps"] +}; + +const testUser = "testUser"; +const testUserWithDbAdminAnyDatabaseRole = "testUserWithDbAdminAnyDatabaseRole"; +const testRole = "testRole"; +const testDBName = "test_applyOps_auth"; +const adminDbName = "admin"; +const authErrCode = 13; + +const command = { + applyOps: [{ + "op": "c", + "ns": testDBName + ".$cmd", + "o": { + "create": "x", + } + }] +}; + +function createUsers(conn) { + let adminDb = conn.getDB(adminDbName); + // Create the admin user. + assert.commandWorked( + adminDb.runCommand({createUser: "admin", pwd: "password", roles: ["__system"]})); + + assert(adminDb.auth("admin", "password")); + assert.commandWorked(adminDb.runCommand({createRole: testRole, privileges: [], roles: []})); + + let testDb = adminDb.getSiblingDB(testDBName); + assert.commandWorked(testDb.runCommand( + {createUser: testUser, pwd: "password", roles: [{role: testRole, db: adminDbName}]})); + + assert.commandWorked(testDb.runCommand({ + createUser: testUserWithDbAdminAnyDatabaseRole, + pwd: "password", + roles: [{role: "dbAdminAnyDatabase", db: adminDbName}, {role: testRole, db: adminDbName}] + })); + + adminDb.logout(); +} + +function testAuthorization(conn, privileges, user, shouldSucceed) { + let testDb = conn.getDB(testDBName); + let adminDb = conn.getDB(adminDbName); + + assert(adminDb.auth("admin", "password")); + assert.commandWorked(adminDb.runCommand({updateRole: testRole, privileges: privileges})); + adminDb.logout(); + + assert(testDb.auth(user, "password")); + if (shouldSucceed) { + assert.commandWorked(testDb.runCommand(command)); + } else { + var res = testDb.runCommand(command); + if (res.ok == 1 || res.code != authErrCode) { + let msg = "expected authorization failure " + + " but received " + tojson(res) + " with privileges " + tojson(privileges); + assert(false, msg); + } + } + + testDb.logout(); +} + +function runTest(conn) { + createUsers(conn); + let privileges = [{resource: {db: testDBName, collection: "x"}, actions: ["createCollection"]}]; + + // Test applyOps failed without applyOps privilege or dbAdminAnyDatabase role. + testAuthorization(conn, privileges, testUser, false); + + // Test applyOps succeed with applyOps privilege. + testAuthorization(conn, privileges.concat(applyOps_priv), testUser, true); + + // Test applyOps succeed with dbAdminAnyDatabase role. + testAuthorization(conn, privileges, testUserWithDbAdminAnyDatabaseRole, true); +} + +// Run the test on a standalone. +let conn = MongoRunner.runMongod({auth: ""}); +runTest(conn); +MongoRunner.stopMongod(conn); +}()); diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 4ccf1be3f53..36ff03aac01 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -272,6 +272,7 @@ var authCommandsLib = { actions: ["appendOplogNote"], removeWhenTestingAuthzFailure: false }, + {resource: {cluster: true}, actions: ["applyOps"]}, ], }, ] @@ -296,17 +297,13 @@ var authCommandsLib = { { runOnDb: adminDbName, roles: { - readWrite: 1, - dbAdmin: 1, - dbOwner: 1, - readWriteAnyDatabase: 1, dbAdminAnyDatabase: 1, root: 1, - restore: 1, __system: 1 }, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["createCollection"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ] }, ] @@ -332,10 +329,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: {root: 1, restore: 1, __system: 1}, + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["createCollection"]}, - {resource: {cluster: true}, actions: ["useUUID", "forceUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "forceUUID", "applyOps"]}, ] }, ] @@ -363,7 +360,7 @@ var authCommandsLib = { privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["createCollection"]}, // Do not have forceUUID. - {resource: {cluster: true}, actions: ["useUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "applyOps"]}, ] }] }, @@ -389,17 +386,13 @@ var authCommandsLib = { { runOnDb: adminDbName, roles: { - readWrite: 1, - dbAdmin: 1, - dbOwner: 1, - readWriteAnyDatabase: 1, dbAdminAnyDatabase: 1, root: 1, - restore: 1, __system: 1 }, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["dropCollection"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ] }, ] @@ -434,10 +427,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: {root: 1, restore: 1, __system: 1}, + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["dropCollection"]}, - {resource: {cluster: true}, actions: ["useUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "applyOps"]}, ] }, ] @@ -474,7 +467,8 @@ var authCommandsLib = { expectAuthzFailure: true, runOnDb: adminDbName, privileges: [ - {resource: {db: firstDbName, collection: "x"}, actions: ["dropCollection"]} + {resource: {db: firstDbName, collection: "x"}, actions: ["dropCollection"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, // don't have useUUID privilege. ] }, @@ -488,13 +482,13 @@ var authCommandsLib = { { runOnDb: adminDbName, privileges: [ - {resource: {cluster: true}, actions: ["appendOplogNote"]}, + {resource: {cluster: true}, actions: ["appendOplogNote", "applyOps"]}, ], }, { runOnDb: firstDbName, privileges: [ - {resource: {cluster: true}, actions: ["appendOplogNote"]}, + {resource: {cluster: true}, actions: ["appendOplogNote", "applyOps"]}, ], expectFailure: true } @@ -527,7 +521,7 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: {readWriteAnyDatabase: 1, root: 1, __system: 1}, + roles: {__system: 1, root: 1}, privileges: [ { resource: {db: firstDbName, collection: "x"}, @@ -536,7 +530,8 @@ var authCommandsLib = { { resource: {db: secondDbName, collection: "y"}, actions: ["insert", "createIndex"] - } + }, + {resource: {cluster: true}, actions: ["applyOps"]}, ] }, ] @@ -560,9 +555,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: roles_write, + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["insert"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ], }, ] @@ -595,10 +591,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: {root: 1, restore: 1, __system: 1}, + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["insert"]}, - {resource: {cluster: true}, actions: ["useUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "applyOps"]}, ], }, ] @@ -635,10 +631,10 @@ var authCommandsLib = { // failure. expectFail: true, runOnDb: adminDbName, - roles: {root: 1, restore: 1, __system: 1}, + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["insert"]}, - {resource: {cluster: true}, actions: ["useUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "applyOps"]}, ], }, ] @@ -674,6 +670,7 @@ var authCommandsLib = { runOnDb: adminDbName, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["insert"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, // Don't have useUUID privilege. ], }, @@ -715,7 +712,7 @@ var authCommandsLib = { runOnDb: adminDbName, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["insert"]}, - {resource: {cluster: true}, actions: ["useUUID", "forceUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "forceUUID", "applyOps"]}, // Require universal privilege set. ], }, @@ -755,7 +752,7 @@ var authCommandsLib = { actions: ["createCollection", "insert"] }, {resource: {db: firstDbName, collection: "y"}, actions: ["createCollection"]}, - {resource: {cluster: true}, actions: ["useUUID", "forceUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "forceUUID", "applyOps"]}, ], }, ] @@ -795,7 +792,7 @@ var authCommandsLib = { resource: {db: firstDbName, collection: "y"}, actions: ["createCollection", "insert"] }, - {resource: {cluster: true}, actions: ["useUUID", "forceUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "forceUUID", "applyOps"]}, ], }, ] @@ -820,9 +817,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: Object.merge(roles_write, {restore: 0}, true), + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["update", "insert"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ], }, ] @@ -848,9 +846,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: Object.merge(roles_write, {restore: 0}, true), + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["update"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ], }, ] @@ -885,10 +884,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: {root: 1, __system: 1}, + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["update"]}, - {resource: {cluster: true}, actions: ["useUUID"]} + {resource: {cluster: true}, actions: ["useUUID", "applyOps"]}, ], }, ] @@ -926,6 +925,7 @@ var authCommandsLib = { runOnDb: adminDbName, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["update"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ], }, ] @@ -943,9 +943,10 @@ var authCommandsLib = { testcases: [ { runOnDb: adminDbName, - roles: Object.merge(roles_write, {restore: 0}, true), + roles: {__system: 1, root: 1}, privileges: [ {resource: {db: firstDbName, collection: "x"}, actions: ["remove"]}, + {resource: {cluster: true}, actions: ["applyOps"]}, ], }, ] diff --git a/src/mongo/db/auth/action_types.txt b/src/mongo/db/auth/action_types.txt index 3861511bad6..d54ec6dd664 100644 --- a/src/mongo/db/auth/action_types.txt +++ b/src/mongo/db/auth/action_types.txt @@ -9,6 +9,7 @@ "anyAction", # Special ActionType that represents *all* actions "appendOplogNote", "applicationMessage", +"applyOps", "auditLogRotate", # Not used for permissions checks, but to id the event in logs. "authCheck", # Not used for permissions checks, but to id the authorization-checking event in logs. "authenticate", # Not used for permission checks, but to id authentication events in logs. diff --git a/src/mongo/db/auth/role_graph_builtin_roles.cpp b/src/mongo/db/auth/role_graph_builtin_roles.cpp index 8ed78e4e455..12d6b0d8d7d 100644 --- a/src/mongo/db/auth/role_graph_builtin_roles.cpp +++ b/src/mongo/db/auth/role_graph_builtin_roles.cpp @@ -428,6 +428,8 @@ void addDbAdminAnyDbPrivileges(PrivilegeVector* privileges) { Privilege::addPrivilegeToPrivilegeVector( privileges, Privilege(ResourcePattern::forCollectionName("system.profile"), profileActions)); + Privilege::addPrivilegeToPrivilegeVector( + privileges, Privilege(ResourcePattern::forClusterResource(), ActionType::applyOps)); } void addClusterMonitorPrivileges(PrivilegeVector* privileges) { diff --git a/src/mongo/db/commands/oplog_application_checks.cpp b/src/mongo/db/commands/oplog_application_checks.cpp index dccff183ba8..c217ce5b03a 100644 --- a/src/mongo/db/commands/oplog_application_checks.cpp +++ b/src/mongo/db/commands/oplog_application_checks.cpp @@ -204,6 +204,11 @@ Status OplogApplicationChecks::checkAuthForCommand(OperationContext* opCtx, const BSONObj& cmdObj, OplogApplicationValidity validity) { AuthorizationSession* authSession = AuthorizationSession::get(opCtx->getClient()); + if (!authSession->isAuthorizedForActionsOnResource(ResourcePattern::forClusterResource(), + ActionType::applyOps)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + if (validity == OplogApplicationValidity::kNeedsSuperuser) { std::vector<Privilege> universalPrivileges; RoleGraph::generateUniversalPrivileges(&universalPrivileges); |