diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2017-07-21 14:48:00 -0400 |
---|---|---|
committer | Sara Golemon <sara.golemon@mongodb.com> | 2017-07-24 21:46:43 -0400 |
commit | 090d9cf938b5825cf4ae7eb0a2642be10e53b81a (patch) | |
tree | 795349f61807953f85e1f2fd1c52860fa7e6a794 | |
parent | d8094ad9be285cb5bf588faff20af785bb76ce4b (diff) | |
download | mongo-090d9cf938b5825cf4ae7eb0a2642be10e53b81a.tar.gz |
SERVER-29180 Add restriction support to the createRole
-rw-r--r-- | jstests/auth/authentication-restrictions-role.js | 207 | ||||
-rw-r--r-- | src/mongo/db/auth/address_restriction.cpp | 39 | ||||
-rw-r--r-- | src/mongo/db/auth/address_restriction.h | 7 | ||||
-rw-r--r-- | src/mongo/db/auth/user_management_commands_parser.cpp | 18 | ||||
-rw-r--r-- | src/mongo/db/auth/user_management_commands_parser.h | 1 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.cpp | 5 |
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)); |