summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyle Suarez <kyle.suarez@mongodb.com>2016-08-01 14:33:15 -0400
committerKyle Suarez <kyle.suarez@mongodb.com>2016-08-11 11:07:41 -0400
commiteb15955c67b8a13455b91a6848f8750447fb0f44 (patch)
tree8a347bd0a5d33ce1fd4ab67ed1e17ce4c09c78e0
parentb4b3cda7501f1d4a292f3b02e13631fae8d4de7f (diff)
downloadmongo-eb15955c67b8a13455b91a6848f8750447fb0f44.tar.gz
SERVER-24724 authz for views
Adds special authorization logic to prevent a user from reading normally-inaccessible collections via a view.
-rw-r--r--jstests/auth/lib/commands_lib.js205
-rw-r--r--jstests/auth/views_authz.js86
-rw-r--r--src/mongo/db/commands/dbcommands.cpp85
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,