summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSara Golemon <sara.golemon@mongodb.com>2017-07-21 14:48:00 -0400
committerSara Golemon <sara.golemon@mongodb.com>2017-07-24 21:46:43 -0400
commit090d9cf938b5825cf4ae7eb0a2642be10e53b81a (patch)
tree795349f61807953f85e1f2fd1c52860fa7e6a794
parentd8094ad9be285cb5bf588faff20af785bb76ce4b (diff)
downloadmongo-090d9cf938b5825cf4ae7eb0a2642be10e53b81a.tar.gz
SERVER-29180 Add restriction support to the createRole
-rw-r--r--jstests/auth/authentication-restrictions-role.js207
-rw-r--r--src/mongo/db/auth/address_restriction.cpp39
-rw-r--r--src/mongo/db/auth/address_restriction.h7
-rw-r--r--src/mongo/db/auth/user_management_commands_parser.cpp18
-rw-r--r--src/mongo/db/auth/user_management_commands_parser.h1
-rw-r--r--src/mongo/db/commands/user_management_commands.cpp5
6 files changed, 277 insertions, 0 deletions
diff --git a/jstests/auth/authentication-restrictions-role.js b/jstests/auth/authentication-restrictions-role.js
new file mode 100644
index 00000000000..ae3c33ef0a5
--- /dev/null
+++ b/jstests/auth/authentication-restrictions-role.js
@@ -0,0 +1,207 @@
+/**
+ * This test checks that authentication restrictions can be set on roles and respected.
+ */
+
+(function() {
+ 'use strict';
+
+ function testConnection(
+ conn, eventuallyConsistentConn, sleepUntilUserDataPropagated, sleepUntilUserDataRefreshed) {
+ load("jstests/libs/host_ipaddr.js");
+
+ // Create a session which observes an eventually consistent view of user data
+ var eventualDb = eventuallyConsistentConn.getDB("admin");
+
+ // Create a session for modifying user data during the life of the test
+ var adminSession = new Mongo("127.0.0.1:" + conn.port);
+ var admin = adminSession.getDB("admin");
+ assert.commandWorked(admin.runCommand(
+ {createUser: "admin", pwd: "admin", roles: [{role: "root", db: "admin"}]}));
+ assert(admin.auth("admin", "admin"));
+
+ // Create a strongly consistent session for consuming user data
+ var db = conn.getDB("admin");
+
+ // Create a strongly consistent session for consuming user data, with a non-localhost
+ // source IP.
+ var externalMongo = new Mongo(get_ipaddr() + ":" + conn.port);
+ var externalDb = externalMongo.getDB("admin");
+
+ print("=== Feature compatibility tests");
+ assert.commandWorked(admin.runCommand({setFeatureCompatibilityVersion: "3.4"}));
+
+ print(
+ "Given a server with 3.4 featureCompatibilityVersions, it is not possible to make a role with authenticationRestrictions");
+ assert.commandFailed(admin.runCommand({
+ createRole: "role1",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions: [{clientSource: ["127.0.0.1"]}]
+ }));
+ assert.commandWorked(admin.runCommand({createRole: "role1", roles: [], privileges: []}));
+
+ print(
+ "When the server is upgraded to 3.6 featureCompatibilityVersion, roles may be created with authenticationRestrictions");
+ assert.commandWorked(admin.runCommand({setFeatureCompatibilityVersion: "3.6"}));
+ assert.commandWorked(admin.runCommand({
+ createRole: "role2",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions: [{clientSource: ["127.0.0.1"]}]
+ }));
+ assert(Object.keys(admin.system.roles.findOne({role: "role2"}))
+ .includes("authenticationRestrictions"));
+ assert.commandWorked(admin.runCommand({createRole: "role3", roles: [], privileges: []}));
+
+ print("=== Role creation tests");
+ print(
+ "When a client creates roles with empty authenticationRestrictions, the operation succeeds, though it has no effect");
+ assert.commandWorked(admin.runCommand(
+ {createRole: "role4", roles: [], privileges: [], authenticationRestrictions: []}));
+ assert(!Object.keys(admin.system.roles.findOne({role: "role4"}))
+ .includes("authenticationRestrictions"));
+
+ print(
+ "When a client creates roles, it may use clientSource and serverAddress authenticationRestrictions");
+ assert.commandWorked(admin.runCommand({
+ createRole: "role6",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions: [{clientSource: ["127.0.0.1"]}]
+ }));
+ assert(Object.keys(admin.system.roles.findOne({role: "role6"}))
+ .includes("authenticationRestrictions"));
+ assert.commandWorked(admin.runCommand({
+ createRole: "role7",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions: [{serverAddress: ["127.0.0.1"]}]
+ }));
+ assert(Object.keys(admin.system.roles.findOne({role: "role7"}))
+ .includes("authenticationRestrictions"));
+ assert.commandWorked(admin.runCommand({
+ createRole: "role8",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions:
+ [{clientSource: ["127.0.0.1"], serverAddress: ["127.0.0.1"]}]
+ }));
+ assert(Object.keys(admin.system.roles.findOne({role: "role8"}))
+ .includes("authenticationRestrictions"));
+ assert.commandWorked(admin.runCommand({
+ createRole: "role9",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions:
+ [{clientSource: ["127.0.0.1"]}, {serverAddress: ["127.0.0.1"]}]
+ }));
+ assert(Object.keys(admin.system.roles.findOne({role: "role9"}))
+ .includes("authenticationRestrictions"));
+ assert.commandFailed(admin.runCommand({
+ createRole: "role10",
+ roles: [],
+ privileges: [],
+ authenticationRestrictions: [{invalidRestriction: ["127.0.0.1"]}]
+ }));
+
+ print("=== Localhost access tests");
+ print(
+ "When a client on the loopback authenticates to a user with {clientSource: \"127.0.0.1\"}, it will succeed");
+ assert.commandWorked(
+ admin.runCommand({createUser: "user6", pwd: "user", roles: ["role6"]}));
+ assert(db.auth("user6", "user"));
+
+ print(
+ "When a client on the loopback authenticates to a user with {serverAddress: \"127.0.0.1\"}, it will succeed");
+ assert.commandWorked(
+ admin.runCommand({createUser: "user7", pwd: "user", roles: ["role7"]}));
+ assert(db.auth("user7", "user"));
+
+ print(
+ "When a client on the loopback authenticates to a user with {clientSource: \"127.0.0.1\", serverAddress: \"127.0.0.1\"}, it will succeed");
+ assert.commandWorked(
+ admin.runCommand({createUser: "user8", pwd: "user", roles: ["role8"]}));
+ assert(db.auth("user8", "user"));
+
+ print("=== Remote access tests");
+ print(
+ "When a client on the external interface authenticates to a user with {clientSource: \"127.0.0.1\"}, it will fail");
+ assert(!externalDb.auth("user6", "user"));
+
+ print(
+ "When a client on the external interface authenticates to a user with {serverAddress: \"127.0.0.1\"}, it will fail");
+ assert(!externalDb.auth("user7", "user"));
+
+ print(
+ "When a client on the external interface authenticates to a user with {clientSource: \"127.0.0.1\", serverAddress: \"127.0.0.1\"}, it will fail");
+ assert(!externalDb.auth("user8", "user"));
+
+ print(
+ "When a client downgrades featureCompatibilityVersion, roles with featureCompatibilityVersions become unusable.");
+ assert.commandWorked(admin.runCommand({
+ createRole: "role13",
+ roles: [],
+ privileges: [{resource: {db: "test", collection: "foo"}, actions: ["find"]}],
+ authenticationRestrictions: [{clientSource: ["127.0.0.1"]}]
+ }));
+ assert.commandWorked(
+ admin.runCommand({createUser: "user13", pwd: "user", roles: ["role13"]}));
+ assert(db.auth("user13", "user"));
+ assert.commandWorked(db.getSiblingDB("test").runCommand({find: "foo", batchSize: 0}));
+
+ sleepUntilUserDataPropagated();
+ assert(eventualDb.auth("user13", "user"));
+ assert.commandWorked(
+ eventualDb.getSiblingDB("test").runCommand({find: "foo", batchSize: 0}));
+
+ assert.commandWorked(admin.runCommand({setFeatureCompatibilityVersion: "3.4"}));
+
+ sleepUntilUserDataRefreshed();
+ assert.commandFailed(db.getSiblingDB("test").runCommand({find: "foo", batchSize: 0}));
+ assert.commandFailed(
+ eventualDb.getSiblingDB("test").runCommand({find: "foo", batchSize: 0}));
+ }
+
+ print("Testing standalone");
+ var conn = MongoRunner.runMongod({bind_ip_all: "", auth: ""});
+ testConnection(conn, conn, function() {}, function() {});
+ MongoRunner.stopMongod(conn);
+
+ var keyfile = "jstests/libs/key1";
+
+ print("Testing replicaset");
+ var rst = new ReplSetTest(
+ {name: 'testset', nodes: 2, nodeOptions: {bind_ip_all: "", auth: ""}, keyFile: keyfile});
+ var nodes = rst.startSet();
+ rst.initiate();
+ rst.awaitSecondaryNodes();
+ var awaitReplication = function() {
+ authutil.asCluster(nodes, "jstests/libs/key1", function() {
+ rst.awaitReplication();
+ });
+ };
+
+ testConnection(rst.getPrimary(), rst.getSecondary(), awaitReplication, awaitReplication);
+ rst.stopSet();
+
+ print("Testing sharded cluster");
+ var st = new ShardingTest({
+ mongos: 2,
+ config: 3,
+ shard: 1,
+ keyFile: keyfile,
+ other: {
+ mongosOptions: {bind_ip_all: "", auth: null},
+ configOptions: {auth: null},
+ shardOptions: {auth: null}
+ }
+ });
+ testConnection(st.s0,
+ st.s1,
+ function() {},
+ function() {
+ sleep(40 * 1000); // Wait for mongos user cache invalidation
+ });
+ st.stop();
+
+}());
diff --git a/src/mongo/db/auth/address_restriction.cpp b/src/mongo/db/auth/address_restriction.cpp
index e8b5c64a881..607aa3056e9 100644
--- a/src/mongo/db/auth/address_restriction.cpp
+++ b/src/mongo/db/auth/address_restriction.cpp
@@ -98,3 +98,42 @@ mongo::StatusWith<mongo::SharedRestrictionDocument> mongo::parseAuthenticationRe
return std::make_shared<document_type>(std::move(doc));
}
+
+mongo::StatusWith<mongo::BSONArray> mongo::getRawAuthenticationRestrictions(
+ const BSONArray& arr) noexcept try {
+ BSONArrayBuilder builder;
+
+ if (serverGlobalParams.featureCompatibility.version.load() <
+ ServerGlobalParams::FeatureCompatibility::Version::k36) {
+ return Status(ErrorCodes::UnsupportedFormat,
+ "'authenticationRestrictions' requires 3.6 feature compatibility version");
+ }
+
+ for (auto const& elem : arr) {
+ if (elem.type() != Object) {
+ return Status(ErrorCodes::UnsupportedFormat,
+ "'authenticationRestrictions' array sub-documents must be address "
+ "restriction objects");
+ }
+ IDLParserErrorContext ctx("address restriction");
+ auto const ar = Address_restriction::parse(ctx, elem.Obj());
+ if (auto const&& client = ar.getClientSource()) {
+ // Validate
+ ClientSourceRestriction(client.get());
+ }
+ if (auto const&& server = ar.getServerAddress()) {
+ // Validate
+ ServerAddressRestriction(server.get());
+ }
+ if (!ar.getClientSource() && !ar.getServerAddress()) {
+ return Status(ErrorCodes::CollectionIsEmpty,
+ "At least one of 'clientSource' and/or 'serverAddress' must be set");
+ }
+ builder.append(ar.toBSON());
+ }
+ return builder.arr();
+} catch (const DBException& e) {
+ return Status(ErrorCodes::BadValue, e.what());
+} catch (const std::exception& e) {
+ return Status(ErrorCodes::InternalError, e.what());
+}
diff --git a/src/mongo/db/auth/address_restriction.h b/src/mongo/db/auth/address_restriction.h
index 73881651024..05930e7340d 100644
--- a/src/mongo/db/auth/address_restriction.h
+++ b/src/mongo/db/auth/address_restriction.h
@@ -190,6 +190,13 @@ StatusWith<RestrictionSet<>> parseAddressRestrictionSet(const BSONObj& obj);
*/
StatusWith<SharedRestrictionDocument> parseAuthenticationRestriction(const BSONArray& arr);
+/**
+ * Parse and validate a BSONArray containing AuthenticationRestrictions
+ * and return a new BSONArray representing a sanitized portion thereof.
+ */
+StatusWith<BSONArray> getRawAuthenticationRestrictions(const BSONArray& arr) noexcept;
+
+
template <>
inline BSONObjBuilder& BSONObjBuilderValueStream::operator<<<ClientSourceRestriction>(
ClientSourceRestriction value) {
diff --git a/src/mongo/db/auth/user_management_commands_parser.cpp b/src/mongo/db/auth/user_management_commands_parser.cpp
index e712ad27cba..9a7742d14a4 100644
--- a/src/mongo/db/auth/user_management_commands_parser.cpp
+++ b/src/mongo/db/auth/user_management_commands_parser.cpp
@@ -37,6 +37,7 @@
#include "mongo/base/status.h"
#include "mongo/bson/util/bson_extract.h"
#include "mongo/db/auth/action_type.h"
+#include "mongo/db/auth/address_restriction.h"
#include "mongo/db/auth/authorization_manager.h"
#include "mongo/db/auth/privilege.h"
#include "mongo/db/auth/privilege_parser.h"
@@ -489,6 +490,7 @@ Status parseCreateOrUpdateRoleCommands(const BSONObj& cmdObj,
validFieldNames.insert(cmdName.toString());
validFieldNames.insert("privileges");
validFieldNames.insert("roles");
+ validFieldNames.insert("authenticationRestrictions");
Status status = _checkNoExtraFields(cmdObj, cmdName, validFieldNames);
if (!status.isOK()) {
@@ -536,6 +538,22 @@ Status parseCreateOrUpdateRoleCommands(const BSONObj& cmdObj,
}
parsedArgs->hasRoles = true;
}
+
+ // Parse restrictions
+ if (cmdObj.hasField("authenticationRestrictions")) {
+ BSONElement restrictionsElement;
+ status = bsonExtractTypedField(
+ cmdObj, "authenticationRestrictions", Array, &restrictionsElement);
+ if (!status.isOK()) {
+ return status;
+ }
+ auto restrictions = getRawAuthenticationRestrictions(BSONArray(restrictionsElement.Obj()));
+ if (!restrictions.isOK()) {
+ return restrictions.getStatus();
+ }
+ parsedArgs->authenticationRestrictions = restrictions.getValue();
+ }
+
return Status::OK();
}
diff --git a/src/mongo/db/auth/user_management_commands_parser.h b/src/mongo/db/auth/user_management_commands_parser.h
index 5c942a97a09..2ccd6524ae9 100644
--- a/src/mongo/db/auth/user_management_commands_parser.h
+++ b/src/mongo/db/auth/user_management_commands_parser.h
@@ -134,6 +134,7 @@ struct CreateOrUpdateRoleArgs {
std::vector<RoleName> roles;
bool hasPrivileges = false;
PrivilegeVector privileges;
+ boost::optional<BSONArray> authenticationRestrictions;
};
/**
diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp
index 293c6d83fd4..016d44ed577 100644
--- a/src/mongo/db/commands/user_management_commands.cpp
+++ b/src/mongo/db/commands/user_management_commands.cpp
@@ -1357,6 +1357,11 @@ public:
roleObjBuilder.append("roles", rolesVectorToBSONArray(args.roles));
+ if (args.authenticationRestrictions && !args.authenticationRestrictions->isEmpty()) {
+ roleObjBuilder.append("authenticationRestrictions",
+ args.authenticationRestrictions.get());
+ }
+
ServiceContext* serviceContext = opCtx->getClient()->getServiceContext();
stdx::lock_guard<stdx::mutex> lk(getAuthzDataMutex(serviceContext));