diff options
-rw-r--r-- | jstests/auth/lib/commands_lib.js | 205 | ||||
-rw-r--r-- | jstests/auth/views_authz.js | 86 | ||||
-rw-r--r-- | src/mongo/db/commands/dbcommands.cpp | 85 |
3 files changed, 363 insertions, 13 deletions
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 0c4288607e1..35c43c3889b 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -226,6 +226,29 @@ var authCommandsLib = { ] }, { + testname: "aggregate_readonly_views", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + teardown: function(db) { + db.view.drop(); + }, + command: {aggregate: "view", pipeline: []}, + testcases: [ + { + runOnDb: firstDbName, + roles: roles_read, + privileges: [{resource: {db: firstDbName, collection: "view"}, actions: ["find"]}] + }, + { + runOnDb: secondDbName, + roles: roles_readAny, + privileges: + [{resource: {db: secondDbName, collection: "view"}, actions: ["find"]}] + } + ] + }, + { testname: "aggregate_explain", command: {aggregate: "foo", explain: true, pipeline: [{$match: {bar: 1}}]}, testcases: [ @@ -243,6 +266,29 @@ var authCommandsLib = { ] }, { + testname: "aggregate_explain_views", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + teardown: function(db) { + db.view.drop(); + }, + command: {aggregate: "view", explain: true, pipeline: [{$match: {bar: 1}}]}, + testcases: [ + { + runOnDb: firstDbName, + roles: roles_read, + privileges: [{resource: {db: firstDbName, collection: "view"}, actions: ["find"]}] + }, + { + runOnDb: secondDbName, + roles: roles_readAny, + privileges: + [{resource: {db: secondDbName, collection: "view"}, actions: ["find"]}] + } + ] + }, + { testname: "aggregate_write", command: {aggregate: "foo", pipeline: [{$out: "foo_out"}]}, testcases: [ @@ -267,6 +313,68 @@ var authCommandsLib = { ] }, { + testname: "aggregate_readView_writeCollection", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + teardown: function(db) { + db.view.drop(); + }, + command: {aggregate: "view", pipeline: [{$out: "view_out"}]}, + testcases: [ + { + runOnDb: firstDbName, + roles: {readWrite: 1, readWriteAnyDatabase: 1, dbOwner: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "view"}, actions: ["find"]}, + {resource: {db: firstDbName, collection: "view_out"}, actions: ["insert"]}, + {resource: {db: firstDbName, collection: "view_out"}, actions: ["remove"]} + ] + }, + { + runOnDb: secondDbName, + roles: {readWriteAnyDatabase: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: secondDbName, collection: "view"}, actions: ["find"]}, + {resource: {db: secondDbName, collection: "view_out"}, actions: ["insert"]}, + {resource: {db: secondDbName, collection: "view_out"}, actions: ["remove"]} + ] + } + ] + }, + { + testname: "aggregate_writeView", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + teardown: function(db) { + db.view.drop(); + }, + command: {aggregate: "foo", pipeline: [{$out: "view"}]}, + testcases: [ + { + runOnDb: firstDbName, + roles: {readWrite: 1, readWriteAnyDatabase: 1, dbOwner: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: firstDbName, collection: "foo"}, actions: ["find"]}, + {resource: {db: firstDbName, collection: "view"}, actions: ["insert"]}, + {resource: {db: firstDbName, collection: "view"}, actions: ["remove"]} + ], + expectFail: true // Cannot write to a view. + }, + { + runOnDb: secondDbName, + roles: {readWriteAnyDatabase: 1, root: 1, __system: 1}, + privileges: [ + {resource: {db: secondDbName, collection: "foo"}, actions: ["find"]}, + {resource: {db: secondDbName, collection: "view"}, actions: ["insert"]}, + {resource: {db: secondDbName, collection: "view"}, actions: ["remove"]} + ], + expectFail: true // Cannot write to a view. + } + ] + }, + { testname: "aggregate_indexStats", command: {aggregate: "foo", pipeline: [{$indexStats: {}}]}, setup: function(db) { @@ -621,6 +729,30 @@ var authCommandsLib = { ] }, { + testname: "collMod_views", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + teardown: function(db) { + db.view.drop(); + }, + command: {collMod: "view", viewOn: "collection2", pipeline: [{$limit: 7}]}, + testcases: [ + { + runOnDb: firstDbName, + roles: Object.extend({restore: 1}, roles_dbAdmin), + privileges: + [{resource: {db: firstDbName, collection: "view"}, actions: ["collMod"]}] + }, + { + runOnDb: secondDbName, + roles: Object.extend({restore: 1}, roles_dbAdminAny), + privileges: + [{resource: {db: secondDbName, collection: "view"}, actions: ["collMod"]}] + } + ] + }, + { testname: "collStats", command: {collStats: "bar", scale: 1}, setup: function(db) { @@ -892,6 +1024,31 @@ var authCommandsLib = { ] }, { + testname: "create_views", + command: {create: "view", viewOn: "collection", pipeline: [{$match: {}}]}, + teardown: function(db) { + db.view.drop(); + }, + testcases: [ + { + runOnDb: firstDbName, + roles: Object.extend({restore: 1}, roles_writeDbAdmin), + privileges: [{ + resource: {db: firstDbName, collection: "view"}, + actions: ["createCollection"] + }] + }, + { + runOnDb: secondDbName, + roles: Object.extend({restore: 1}, roles_writeDbAdminAny), + privileges: [{ + resource: {db: secondDbName, collection: "view"}, + actions: ["createCollection"] + }] + }, + ] + }, + { testname: "create_capped", command: {create: "x", capped: true, size: 1000}, teardown: function(db) { @@ -1135,6 +1292,31 @@ var authCommandsLib = { ] }, { + testname: "drop_views", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + command: {drop: "view"}, + testcases: [ + { + runOnDb: firstDbName, + roles: Object.extend({restore: 1}, roles_writeDbAdmin), + privileges: [{ + resource: {db: firstDbName, collection: "view"}, + actions: ["dropCollection"] + }] + }, + { + runOnDb: secondDbName, + roles: Object.extend({restore: 1}, roles_writeDbAdminAny), + privileges: [{ + resource: {db: secondDbName, collection: "view"}, + actions: ["dropCollection"] + }] + } + ] + }, + { testname: "dropDatabase", command: {dropDatabase: 1}, setup: function(db) { @@ -1268,6 +1450,29 @@ var authCommandsLib = { ] }, { + testname: "find_views", + setup: function(db) { + db.createView("view", "collection", [{$match: {}}]); + }, + teardown: function(db) { + db.view.drop(); + }, + command: {find: "view"}, + testcases: [ + { + runOnDb: firstDbName, + roles: roles_read, + privileges: [{resource: {db: firstDbName, collection: "view"}, actions: ["find"]}] + }, + { + runOnDb: secondDbName, + roles: roles_readAny, + privileges: + [{resource: {db: secondDbName, collection: "view"}, actions: ["find"]}] + } + ] + }, + { testname: "findWithTerm", command: {find: "foo", limit: -1, term: NumberLong(1)}, testcases: [ diff --git a/jstests/auth/views_authz.js b/jstests/auth/views_authz.js new file mode 100644 index 00000000000..e187dff9c90 --- /dev/null +++ b/jstests/auth/views_authz.js @@ -0,0 +1,86 @@ +/** + * Tests authorization special cases with views. These are special exceptions that prohibit certain + * operations on views even if the user has an explicit privilege on that view. + */ +(function() { + "use strict"; + + let mongod = MongoRunner.runMongod({auth: "", bind_ip: "127.0.0.1"}); + + // Create the admin user. + let adminDB = mongod.getDB("admin"); + assert.commandWorked(adminDB.runCommand({createUser: "admin", pwd: "admin", roles: ["root"]})); + assert.eq(1, adminDB.auth("admin", "admin")); + + const viewsDBName = "views_authz"; + let viewsDB = adminDB.getSiblingDB(viewsDBName); + viewsDB.dropAllUsers(); + viewsDB.logout(); + + // Create a user who can read, create and modify a view 'view' and a read a namespace + // 'permitted' but does not have access to 'forbidden'. + assert.commandWorked(viewsDB.runCommand({ + createRole: "readWriteView", + privileges: [ + { + resource: {db: viewsDBName, collection: "view"}, + actions: ["find", "createCollection", "collMod"] + }, + {resource: {db: viewsDBName, collection: "permitted"}, actions: ["find"]} + ], + roles: [] + })); + assert.commandWorked( + viewsDB.runCommand({createUser: "viewUser", pwd: "pwd", roles: ["readWriteView"]})); + + adminDB.logout(); + assert.eq(1, viewsDB.auth("viewUser", "pwd")); + + const lookupStage = {$lookup: {from: "forbidden", localField: "x", foreignField: "x", as: "y"}}; + const graphLookupStage = { + $graphLookup: { + from: "forbidden", + startWith: [], + connectFromField: "x", + connectToField: "x", + as: "y" + } + }; + + // You cannot create a view if you have both the 'createCollection' and 'find' actions on that + // view but not the 'find' action on all of the dependent namespaces. + assert.commandFailedWithCode(viewsDB.createView("view", "forbidden", []), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode(viewsDB.createView("view", "permitted", [lookupStage]), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode(viewsDB.createView("view", "permitted", [graphLookupStage]), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode( + viewsDB.createView("view", "permitted", [{$facet: {a: [lookupStage]}}]), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode( + viewsDB.createView("view", "permitted", [{$facet: {b: [graphLookupStage]}}]), + ErrorCodes.Unauthorized); + + assert.commandWorked(viewsDB.createView("view", "permitted", [{$match: {x: 1}}])); + + // You cannot modify a view if you have both the 'collMod' and 'find' actions on that view but + // not the 'find' action on all of the dependent namespaces. + assert.commandFailedWithCode( + viewsDB.runCommand({collMod: "view", viewOn: "forbidden", pipeline: [{$match: {}}]}), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode( + viewsDB.runCommand({collMod: "view", viewOn: "permitted", pipeline: [lookupStage]}), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode( + viewsDB.runCommand({collMod: "view", viewOn: "permitted", pipeline: [graphLookupStage]}), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode( + viewsDB.runCommand( + {collMod: "view", viewOn: "permitted", pipeline: [{$facet: {a: [lookupStage]}}]}), + ErrorCodes.Unauthorized); + assert.commandFailedWithCode( + viewsDB.runCommand( + {collMod: "view", viewOn: "permitted", pipeline: [{$facet: {b: [graphLookupStage]}}]}), + ErrorCodes.Unauthorized); +}()); diff --git a/src/mongo/db/commands/dbcommands.cpp b/src/mongo/db/commands/dbcommands.cpp index cc089ffea92..bd848af59b6 100644 --- a/src/mongo/db/commands/dbcommands.cpp +++ b/src/mongo/db/commands/dbcommands.cpp @@ -76,6 +76,7 @@ #include "mongo/db/namespace_string.h" #include "mongo/db/op_observer.h" #include "mongo/db/ops/insert.h" +#include "mongo/db/pipeline/pipeline.h" #include "mongo/db/query/get_executor.h" #include "mongo/db/query/internal_plans.h" #include "mongo/db/query/query_planner.h" @@ -115,6 +116,41 @@ using std::string; using std::stringstream; using std::unique_ptr; +namespace { +/** + * Checks for additional required privileges when creating or modifying a view. Call this function + * after verifying that the user has the "createCollection" or "collMod" action, respectively. + * + * 'cmdObj' must have a String field named 'viewOn'. + */ +Status canCreateOrModifyView(Client* client, + const std::string& dbname, + const BSONObj& cmdObj, + ResourcePattern resource) { + AuthorizationSession* authzSession = AuthorizationSession::get(client); + + // It's safe to allow a user to create or modify a view if they can't read it anyway. + if (!authzSession->isAuthorizedForActionsOnResource(resource, ActionType::find)) { + return Status::OK(); + } + + // The user can read the view they're trying to create/modify, so we must ensure that they also + // have the find action on all namespaces in "viewOn" and "pipeline". If "pipeline" is not + // specified, default to the empty pipeline. + auto viewPipeline = + cmdObj.hasField("pipeline") ? BSONArray(cmdObj["pipeline"].Obj()) : BSONArray(); + + // This check ignores some invalid pipeline specifications. For example, if a user specifies a + // view definition with an invalid specification like {$lookup: "blah"}, the authorization check + // will succeed but the pipeline will fail to parse later in Command::run(). + return Pipeline::checkAuthForCommand( + client, + dbname, + BSON("aggregate" << cmdObj["viewOn"].checkAndGetStringData() << "pipeline" + << viewPipeline)); +} +} // namespace + class CmdShutdownMongoD : public CmdShutdown { public: virtual void help(stringstream& help) const { @@ -521,18 +557,31 @@ public: const std::string& dbname, const BSONObj& cmdObj) { AuthorizationSession* authzSession = AuthorizationSession::get(client); + auto cmdNsResource = parseResourcePattern(dbname, cmdObj); if (cmdObj["capped"].trueValue()) { - if (!authzSession->isAuthorizedForActionsOnResource( - parseResourcePattern(dbname, cmdObj), ActionType::convertToCapped)) { + if (!authzSession->isAuthorizedForActionsOnResource(cmdNsResource, + ActionType::convertToCapped)) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + } + + const bool hasCreateCollectionAction = authzSession->isAuthorizedForActionsOnResource( + cmdNsResource, ActionType::createCollection); + + // If attempting to create a view, check for additional required privileges. + if (cmdObj["viewOn"]) { + // You need the createCollection action on this namespace; the insert action is not + // sufficient. + if (!hasCreateCollectionAction) { return Status(ErrorCodes::Unauthorized, "unauthorized"); } + return canCreateOrModifyView(client, dbname, cmdObj, cmdNsResource); } - // ActionType::createCollection or ActionType::insert are both acceptable - if (authzSession->isAuthorizedForActionsOnResource(parseResourcePattern(dbname, cmdObj), - ActionType::createCollection) || - authzSession->isAuthorizedForActionsOnResource(parseResourcePattern(dbname, cmdObj), - ActionType::insert)) { + // To create a regular collection, ActionType::createCollection or ActionType::insert are + // both acceptable. + if (hasCreateCollectionAction || + authzSession->isAuthorizedForActionsOnResource(cmdNsResource, ActionType::insert)) { return Status::OK(); } @@ -1013,12 +1062,22 @@ public: "Example: { collMod: 'foo', index: {name: 'bar', expireAfterSeconds: 600} }\n"; } - virtual void addRequiredPrivileges(const std::string& dbname, - const BSONObj& cmdObj, - std::vector<Privilege>* out) { - ActionSet actions; - actions.addAction(ActionType::collMod); - out->push_back(Privilege(parseResourcePattern(dbname, cmdObj), actions)); + virtual Status checkAuthForCommand(Client* client, + const std::string& dbname, + const BSONObj& cmdObj) { + AuthorizationSession* authzSession = AuthorizationSession::get(client); + if (!authzSession->isAuthorizedForActionsOnResource(parseResourcePattern(dbname, cmdObj), + ActionType::collMod)) { + return Status(ErrorCodes::Unauthorized, "unauthorized"); + } + + // Check for additional required privileges if attempting to modify a view. + if (cmdObj["viewOn"] || cmdObj["pipeline"]) { + return canCreateOrModifyView( + client, dbname, cmdObj, parseResourcePattern(dbname, cmdObj)); + } + + return Status::OK(); } bool run(OperationContext* txn, |