diff options
-rw-r--r-- | jstests/auth/auth_schema_upgrade.js | 86 | ||||
-rw-r--r-- | jstests/auth/lib/commands_lib.js | 19 | ||||
-rw-r--r-- | jstests/multiVersion/libs/auth_helpers.js | 4 | ||||
-rw-r--r-- | src/mongo/db/auth/action_types.txt | 1 | ||||
-rw-r--r-- | src/mongo/db/auth/role_graph_builtin_roles.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/auth/user_management_commands_parser.cpp | 42 | ||||
-rw-r--r-- | src/mongo/db/auth/user_management_commands_parser.h | 14 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.cpp | 222 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands.h | 1 | ||||
-rw-r--r-- | src/mongo/db/commands/user_management_commands_common.cpp | 8 | ||||
-rw-r--r-- | src/mongo/s/commands/cluster_user_management_commands.cpp | 95 |
11 files changed, 494 insertions, 1 deletions
diff --git a/jstests/auth/auth_schema_upgrade.js b/jstests/auth/auth_schema_upgrade.js new file mode 100644 index 00000000000..001e5c04a1b --- /dev/null +++ b/jstests/auth/auth_schema_upgrade.js @@ -0,0 +1,86 @@ +// Standalone test of authSchemaUpgrade +load('./jstests/multiVersion/libs/auth_helpers.js'); + +var setupCRUsers = function(conn){ + jsTest.log("setting up legacy users"); + var adminDB = conn.getDB('admin'); + + adminDB.system.version.update({_id:"authSchema"},{"currentVersion":3},{upsert:true}) + + adminDB.createUser({user: 'user1', pwd: 'pass', + roles: jsTest.adminUserRoles}); + assert(adminDB.auth({mechanism: 'MONGODB-CR', + user: 'user1', pwd: 'pass'})); + + adminDB.createUser({user: 'user2', pwd: 'pass', + roles: jsTest.adminUserRoles}); + assert(adminDB.auth({mechanism: 'MONGODB-CR', + user: 'user2', pwd: 'pass'})); + + // Add $external no-op user to verify that it does not affect + // authSchemaUpgrade SERVER-18475 + adminDB.getSiblingDB('$external').createUser({user: "evil", roles: []}); + + jsTest.log("Verifying user documents before upgrading"); + + // We haven't run authSchemaUpgrade so there shouldn't be + // any stored SCRAM-SHA-1 credentials. + verifyUserDoc(adminDB, 'user1', true, false); + verifyUserDoc(adminDB, 'user2', true, false); + verifyUserDoc(adminDB.getSiblingDB('$external'), "evil", false, false, true); + + adminDB.updateUser('user1', {pwd: 'newpass', + roles: jsTest.adminUserRoles}); + verifyAuth(adminDB, 'user1', 'newpass', true, true); + + verifyUserDoc(adminDB, 'user1', true, false); +} + +var verifySchemaUpgrade = function(adminDB) { + // All users should only have SCRAM credentials. + verifyUserDoc(adminDB, 'user1', false, true); + verifyUserDoc(adminDB, 'user2', false, true); + verifyUserDoc(adminDB.getSiblingDB('$external'), "evil", false, false, true); + + // After authSchemaUpgrade MONGODB-CR no longer works. + verifyAuth(adminDB, 'user1', 'newpass', false, true); + verifyAuth(adminDB, 'user2', 'pass', false, true); +} + +var runAndVerifySchemaUpgrade = function(conn){ + jsTest.log("run authSchemaUpgrade"); + var adminDB = conn.getDB('admin'); + + assert.commandWorked(adminDB.runCommand('authSchemaUpgrade')); + verifySchemaUpgrade(adminDB); +} + +var testAuthSchemaUpgrade = function(conn) { + setupCRUsers(conn); + runAndVerifySchemaUpgrade(conn); +} + +// Test authSchemaUpgrade and upgrade shards +var testUpgradeShards = function(mongos, shard) { + setupCRUsers(shard); + + assert.commandWorked(mongos.adminCommand({"authSchemaUpgrade":1,"upgradeShards":1})); + verifySchemaUpgrade(shard.getDB('admin')); +} + +jsTest.log('Test authSchemUpgrade standalone'); +var conn = MongoRunner.runMongod(); +testAuthSchemaUpgrade(conn); +MongoRunner.stopMongod(conn); + +jsTest.log('Test authSchemUpgrade sharded'); +var dopts = { smallfiles: "", nopreallocj: ""} +var st = new ShardingTest( + { shards: 1, + mongos: 1, + config: 1, + useHostname: false, // Needed when relying on the localhost exception + other: { shardOptions: dopts, configOptions: dopts, mongosOptions: { verbose: 1 } } } ); +testAuthSchemaUpgrade(st.s); +testUpgradeShards(st.s, st.shard0); +st.stop(); diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js index 2ce024ade88..f677f5a2ebc 100644 --- a/jstests/auth/lib/commands_lib.js +++ b/jstests/auth/lib/commands_lib.js @@ -374,6 +374,25 @@ var authCommandsLib = { ] }, { + testname: "authSchemaUpgrade", + command: {authSchemaUpgrade: 1}, + testcases: [ + { + runOnDb: adminDbName, + roles: { + userAdminAnyDatabase: 1, + root: 1, + __system: 1 + }, + privileges: [ + { resource: {cluster: true}, actions: ["authSchemaUpgrade"] } + ] + }, + { runOnDb: firstDbName, roles: {} }, + { runOnDb: secondDbName, roles: {} } + ] + }, + { testname: "buildInfo", command: {buildInfo: 1}, testcases: [ diff --git a/jstests/multiVersion/libs/auth_helpers.js b/jstests/multiVersion/libs/auth_helpers.js index 39566da3d1b..cebb2b03e5f 100644 --- a/jstests/multiVersion/libs/auth_helpers.js +++ b/jstests/multiVersion/libs/auth_helpers.js @@ -9,10 +9,11 @@ var getUserDoc = function(db, username){ // Verify that the user document for username in db // has MONGODB-CR credentials (or not) and SCRAM-SHA-1 // credentials (or not). -var verifyUserDoc = function(db, username, hasCR, hasSCRAM){ +var verifyUserDoc = function(db, username, hasCR, hasSCRAM, hasExternal = false){ var userDoc = getUserDoc(db, username); assert.eq(hasCR, 'MONGODB-CR' in userDoc.credentials); assert.eq(hasSCRAM, 'SCRAM-SHA-1' in userDoc.credentials); + assert.eq(hasExternal, 'external' in userDoc.credentials); } // Verify that that we can authenticate (or not) using MONGODB-CR @@ -22,4 +23,5 @@ var verifyAuth = function(db, username, password, passCR, passSCRAM){ user: username, pwd: password})); assert.eq(passSCRAM, db.auth({mechanism: 'SCRAM-SHA-1', user: username, pwd: password})); + db.logout(); } diff --git a/src/mongo/db/auth/action_types.txt b/src/mongo/db/auth/action_types.txt index 0e2c2aed753..2d55b878687 100644 --- a/src/mongo/db/auth/action_types.txt +++ b/src/mongo/db/auth/action_types.txt @@ -11,6 +11,7 @@ "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. +"authSchemaUpgrade", "bypassDocumentValidation", # For bypassDocumentValidation command option. "changeCustomData", "changePassword", diff --git a/src/mongo/db/auth/role_graph_builtin_roles.cpp b/src/mongo/db/auth/role_graph_builtin_roles.cpp index eff0ce4f9fa..94e3b157346 100644 --- a/src/mongo/db/auth/role_graph_builtin_roles.cpp +++ b/src/mongo/db/auth/role_graph_builtin_roles.cpp @@ -291,6 +291,9 @@ void addUserAdminAnyDbPrivileges(PrivilegeVector* privileges) { privileges, Privilege(ResourcePattern::forClusterResource(), ActionType::listDatabases)); Privilege::addPrivilegeToPrivilegeVector( privileges, + Privilege(ResourcePattern::forClusterResource(), ActionType::authSchemaUpgrade)); + Privilege::addPrivilegeToPrivilegeVector( + privileges, Privilege(ResourcePattern::forClusterResource(), ActionType::invalidateUserCache)); diff --git a/src/mongo/db/auth/user_management_commands_parser.cpp b/src/mongo/db/auth/user_management_commands_parser.cpp index 82130e2a530..54109d14bf7 100644 --- a/src/mongo/db/auth/user_management_commands_parser.cpp +++ b/src/mongo/db/auth/user_management_commands_parser.cpp @@ -662,5 +662,47 @@ Status parseMergeAuthzCollectionsCommand(const BSONObj& cmdObj, return Status::OK(); } +Status parseAuthSchemaUpgradeCommand(const BSONObj& cmdObj, + const std::string& dbname, + AuthSchemaUpgradeArgs* parsedArgs) { + static const int minUpgradeSteps = 1; + static const int maxUpgradeSteps = 2; + + unordered_set<std::string> validFieldNames; + validFieldNames.insert("authSchemaUpgrade"); + validFieldNames.insert("maxSteps"); + validFieldNames.insert("upgradeShards"); + validFieldNames.insert("writeConcern"); + + Status status = _checkNoExtraFields(cmdObj, "authSchemaUpgrade", validFieldNames); + if (!status.isOK()) { + return status; + } + + status = bsonExtractBooleanFieldWithDefault( + cmdObj, "upgradeShards", true, &parsedArgs->shouldUpgradeShards); + if (!status.isOK()) { + return status; + } + + long long steps; + status = bsonExtractIntegerFieldWithDefault(cmdObj, "maxSteps", maxUpgradeSteps, &steps); + if (!status.isOK()) + return status; + if (steps < minUpgradeSteps || steps > maxUpgradeSteps) { + return Status(ErrorCodes::BadValue, + mongoutils::str::stream() << "Legal values for \"maxSteps\" are at least " + << minUpgradeSteps << " and no more than " + << maxUpgradeSteps << "; found " << steps); + } + parsedArgs->maxSteps = static_cast<int>(steps); + + status = _extractWriteConcern(cmdObj, &parsedArgs->writeConcern); + if (!status.isOK()) { + return status; + } + + return Status::OK(); +} } // namespace auth } // namespace mongo diff --git a/src/mongo/db/auth/user_management_commands_parser.h b/src/mongo/db/auth/user_management_commands_parser.h index c55210e8978..2bc278feecb 100644 --- a/src/mongo/db/auth/user_management_commands_parser.h +++ b/src/mongo/db/auth/user_management_commands_parser.h @@ -223,5 +223,19 @@ struct MergeAuthzCollectionsArgs { Status parseMergeAuthzCollectionsCommand(const BSONObj& cmdObj, MergeAuthzCollectionsArgs* parsedArgs); +struct AuthSchemaUpgradeArgs { + int maxSteps = 3; + bool shouldUpgradeShards = true; + BSONObj writeConcern; +}; + +/** + * Takes a command object describing an invocation of the "authSchemaUpgrade" command and + * parses out the write concern, maximum steps to take and whether or not shard servers should + * also be upgraded, in the sharded deployment case. + */ +Status parseAuthSchemaUpgradeCommand(const BSONObj& cmdObj, + const std::string& dbname, + AuthSchemaUpgradeArgs* parsedArgs); } // namespace auth } // namespace mongo diff --git a/src/mongo/db/commands/user_management_commands.cpp b/src/mongo/db/commands/user_management_commands.cpp index cb63736f7c0..a5b1d2373b2 100644 --- a/src/mongo/db/commands/user_management_commands.cpp +++ b/src/mongo/db/commands/user_management_commands.cpp @@ -2722,4 +2722,226 @@ public: } cmdMergeAuthzCollections; +/** + * Logs that the auth schema upgrade failed because of "status" and returns "status". + */ +Status logUpgradeFailed(const Status& status) { + log() << "Auth schema upgrade failed with " << status; + return status; +} + +/** + * Updates a single user document from MONGODB-CR to SCRAM credentials. + * + * Throws a DBException on errors. + */ +void updateUserCredentials(OperationContext* txn, + const StringData& sourceDB, + const BSONObj& userDoc, + const BSONObj& writeConcern) { + // Skip users in $external, SERVER-18475 + if (userDoc["db"].String() == "$external") { + return; + } + + BSONElement credentialsElement = userDoc["credentials"]; + uassert(18806, + mongoutils::str::stream() + << "While preparing to upgrade user doc from " + "2.6/3.0 user data schema to the 3.0+ SCRAM only schema, found a user doc " + "with missing or incorrectly formatted credentials: " << userDoc.toString(), + credentialsElement.type() == Object); + + BSONObj credentialsObj = credentialsElement.Obj(); + BSONElement mongoCRElement = credentialsObj["MONGODB-CR"]; + BSONElement scramElement = credentialsObj["SCRAM-SHA-1"]; + + // Ignore any user documents that already have SCRAM credentials. This should only + // occur if a previous authSchemaUpgrade was interrupted halfway. + if (!scramElement.eoo()) { + return; + } + + uassert(18744, + mongoutils::str::stream() + << "While preparing to upgrade user doc from " + "2.6/3.0 user data schema to the 3.0+ SCRAM only schema, found a user doc " + "missing MONGODB-CR credentials :" << userDoc.toString(), + !mongoCRElement.eoo()); + + std::string hashedPassword = mongoCRElement.String(); + + BSONObj query = BSON("_id" << userDoc["_id"].String()); + BSONObjBuilder updateBuilder; + { + BSONObjBuilder toSetBuilder(updateBuilder.subobjStart("$set")); + toSetBuilder << "credentials" + << BSON("SCRAM-SHA-1" << scram::generateCredentials( + hashedPassword, saslGlobalParams.scramIterationCount)); + } + + uassertStatusOK(updateOneAuthzDocument(txn, + NamespaceString("admin", "system.users"), + query, + updateBuilder.obj(), + true, + writeConcern)); +} + +/** Loop through all the user documents in the admin.system.users collection. + * For each user document: + * 1. Compute SCRAM credentials based on the MONGODB-CR hash + * 2. Remove the MONGODB-CR hash + * 3. Add SCRAM credentials to the user document credentials section + */ +Status updateCredentials(OperationContext* txn, const BSONObj& writeConcern) { + // Loop through and update the user documents in admin.system.users. + Status status = queryAuthzDocument( + txn, + NamespaceString("admin", "system.users"), + BSONObj(), + BSONObj(), + stdx::bind(updateUserCredentials, txn, "admin", stdx::placeholders::_1, writeConcern)); + if (!status.isOK()) + return logUpgradeFailed(status); + + // Update the schema version document. + status = + updateOneAuthzDocument(txn, + AuthorizationManager::versionCollectionNamespace, + AuthorizationManager::versionDocumentQuery, + BSON("$set" << BSON(AuthorizationManager::schemaVersionFieldName + << AuthorizationManager::schemaVersion28SCRAM)), + true, + writeConcern); + if (!status.isOK()) + return logUpgradeFailed(status); + + return Status::OK(); +} + +/** + * Performs one step in the process of upgrading the stored authorization data to the + * newest schema. + * + * On success, returns Status::OK(), and *isDone will indicate whether there are more + * steps to perform. + * + * If the authorization data is already fully upgraded, returns Status::OK and sets *isDone + * to true, so this is safe to call on a fully upgraded system. + * + * On failure, returns a status other than Status::OK(). In this case, is is typically safe + * to try again. + */ +Status upgradeAuthSchemaStep(OperationContext* txn, + AuthorizationManager* authzManager, + const BSONObj& writeConcern, + bool* isDone) { + int authzVersion; + Status status = authzManager->getAuthorizationVersion(txn, &authzVersion); + if (!status.isOK()) { + return status; + } + + switch (authzVersion) { + case AuthorizationManager::schemaVersion26Final: + case AuthorizationManager::schemaVersion28SCRAM: { + Status status = updateCredentials(txn, writeConcern); + if (status.isOK()) + *isDone = true; + return status; + } + default: + return Status(ErrorCodes::AuthSchemaIncompatible, + mongoutils::str::stream() + << "Do not know how to upgrade auth schema from version " + << authzVersion); + } +} + +/** + * Performs up to maxSteps steps in the process of upgrading the stored authorization data + * to the newest schema. Behaves as if by repeatedly calling upgradeSchemaStep up to + * maxSteps times until either it completes the upgrade or returns a non-OK status. + * + * Invalidates the user cache before the first step and after each attempted step. + * + * Returns Status::OK() to indicate that the upgrade process has completed successfully. + * Returns ErrorCodes::OperationIncomplete to indicate that progress was made, but that more + * steps must be taken to complete the process. Other returns indicate a failure to make + * progress performing the upgrade, and the specific code and message in the returned status + * may provide additional information. + */ +Status upgradeAuthSchema(OperationContext* txn, + AuthorizationManager* authzManager, + int maxSteps, + const BSONObj& writeConcern) { + if (maxSteps < 1) { + return Status(ErrorCodes::BadValue, + "Minimum value for maxSteps parameter to upgradeAuthSchema is 1"); + } + authzManager->invalidateUserCache(); + for (int i = 0; i < maxSteps; ++i) { + bool isDone; + Status status = upgradeAuthSchemaStep(txn, authzManager, writeConcern, &isDone); + authzManager->invalidateUserCache(); + if (!status.isOK() || isDone) { + return status; + } + } + return Status(ErrorCodes::OperationIncomplete, + mongoutils::str::stream() << "Auth schema upgrade incomplete after " << maxSteps + << " successful steps."); +} + +class CmdAuthSchemaUpgrade : public Command { +public: + CmdAuthSchemaUpgrade() : Command("authSchemaUpgrade") {} + + virtual bool slaveOk() const { + return false; + } + + virtual bool adminOnly() const { + return true; + } + + virtual bool isWriteCommandForConfigServer() const { + return true; + } + + virtual void help(stringstream& ss) const { + ss << "Upgrades the auth data storage schema"; + } + + virtual Status checkAuthForCommand(ClientBasic* client, + const std::string& dbname, + const BSONObj& cmdObj) { + return auth::checkAuthForAuthSchemaUpgradeCommand(client); + } + + virtual bool run(OperationContext* txn, + const string& dbname, + BSONObj& cmdObj, + int options, + string& errmsg, + BSONObjBuilder& result) { + auth::AuthSchemaUpgradeArgs parsedArgs; + Status status = auth::parseAuthSchemaUpgradeCommand(cmdObj, dbname, &parsedArgs); + if (!status.isOK()) { + return appendCommandStatus(result, status); + } + + ServiceContext* serviceContext = txn->getClient()->getServiceContext(); + AuthorizationManager* authzManager = AuthorizationManager::get(serviceContext); + + stdx::lock_guard<stdx::mutex> lk(getAuthzDataMutex(serviceContext)); + + status = upgradeAuthSchema(txn, authzManager, parsedArgs.maxSteps, parsedArgs.writeConcern); + if (status.isOK()) + result.append("done", true); + return appendCommandStatus(result, status); + } + +} cmdAuthSchemaUpgrade; } // namespace mongo diff --git a/src/mongo/db/commands/user_management_commands.h b/src/mongo/db/commands/user_management_commands.h index 63c7a7d8d6c..8ae4e233db9 100644 --- a/src/mongo/db/commands/user_management_commands.h +++ b/src/mongo/db/commands/user_management_commands.h @@ -141,6 +141,7 @@ Status checkAuthForGetUserCacheGenerationCommand(ClientBasic* client); Status checkAuthForMergeAuthzCollectionsCommand(ClientBasic* client, const BSONObj& cmdObj); +Status checkAuthForAuthSchemaUpgradeCommand(ClientBasic* client); } // namespace auth } // namespace mongo diff --git a/src/mongo/db/commands/user_management_commands_common.cpp b/src/mongo/db/commands/user_management_commands_common.cpp index 64aee9eca51..09445c5aea7 100644 --- a/src/mongo/db/commands/user_management_commands_common.cpp +++ b/src/mongo/db/commands/user_management_commands_common.cpp @@ -522,6 +522,14 @@ Status checkAuthForMergeAuthzCollectionsCommand(ClientBasic* client, const BSONO return Status::OK(); } +Status checkAuthForAuthSchemaUpgradeCommand(ClientBasic* client) { + AuthorizationSession* authzSession = AuthorizationSession::get(client); + if (!authzSession->isAuthorizedForActionsOnResource(ResourcePattern::forClusterResource(), + ActionType::authSchemaUpgrade)) { + return Status(ErrorCodes::Unauthorized, "Not authorized to run authSchemaUpgrade command."); + } + return Status::OK(); +} } // namespace auth } // namespace mongo diff --git a/src/mongo/s/commands/cluster_user_management_commands.cpp b/src/mongo/s/commands/cluster_user_management_commands.cpp index 75fca86dc7a..afb2559aa7c 100644 --- a/src/mongo/s/commands/cluster_user_management_commands.cpp +++ b/src/mongo/s/commands/cluster_user_management_commands.cpp @@ -42,6 +42,8 @@ #include "mongo/db/commands.h" #include "mongo/db/jsobj.h" #include "mongo/s/catalog/catalog_manager.h" +#include "mongo/s/catalog/type_shard.h" +#include "mongo/s/client/shard_registry.h" #include "mongo/s/grid.h" namespace mongo { @@ -803,4 +805,97 @@ public: } cmdMergeAuthzCollections; +namespace { +/** + * Runs the authSchemaUpgrade on all shards, with the given maxSteps and writeConcern + * parameters. + * + * Upgrades each shard serially, and stops on first failure. Returned error indicates that + * failure. + */ +Status runUpgradeOnAllShards(OperationContext* txn, int maxSteps, const BSONObj& writeConcern) { + BSONObjBuilder cmdObjBuilder; + cmdObjBuilder.append("authSchemaUpgrade", 1); + cmdObjBuilder.append("maxSteps", maxSteps); + if (!writeConcern.isEmpty()) { + cmdObjBuilder.append("writeConcern", writeConcern); + } + const BSONObj cmdObj = cmdObjBuilder.done(); + + // Upgrade each shard in turn, stopping on first failure. + vector<ShardType> allShards; + Status status = grid.catalogManager(txn)->getAllShards(txn, &allShards); + if (!status.isOK()) { + return status; + } + auto shardRegistry = grid.shardRegistry(); + for (const auto& shardEntry : allShards) { + auto cmdResult = shardRegistry->runCommandWithNotMasterRetries( + txn, shardEntry.getName(), "admin", cmdObj); + + if (!cmdResult.isOK()) { + return Status(status.code(), + str::stream() << "Failed to run authSchemaUpgrade on shard " + << shardEntry.getName() << causedBy(status)); + } + } + + return Status::OK(); +} +} // namespace + +class CmdAuthSchemaUpgrade : public Command { +public: + CmdAuthSchemaUpgrade() : Command("authSchemaUpgrade") {} + + virtual bool slaveOk() const { + return false; + } + + virtual bool adminOnly() const { + return true; + } + + virtual bool isWriteCommandForConfigServer() const { + return true; + } + + virtual void help(stringstream& ss) const { + ss << "Upgrades the auth data storage schema"; + } + + virtual Status checkAuthForCommand(ClientBasic* client, + const std::string& dbname, + const BSONObj& cmdObj) { + return auth::checkAuthForAuthSchemaUpgradeCommand(client); + } + + bool run(OperationContext* txn, + const string& dbname, + BSONObj& cmdObj, + int options, + string& errmsg, + BSONObjBuilder& result) { + // Run the authSchemaUpgrade command on the config servers + if (!grid.catalogManager(txn) + ->runUserManagementWriteCommand(txn, this->name, dbname, cmdObj, &result)) { + return false; + } + + auth::AuthSchemaUpgradeArgs parsedArgs; + Status status = auth::parseAuthSchemaUpgradeCommand(cmdObj, dbname, &parsedArgs); + if (!status.isOK()) { + return appendCommandStatus(result, status); + } + + // Optionally run the authSchemaUpgrade command on the individual shards + if (parsedArgs.shouldUpgradeShards) { + status = runUpgradeOnAllShards(txn, parsedArgs.maxSteps, parsedArgs.writeConcern); + if (!status.isOK()) + return appendCommandStatus(result, status); + } + return true; + } +} cmdAuthSchemaUpgrade; + } // namespace mongo |