From b7946587136b5dcbd28bc8f7b8592f8a7daade73 Mon Sep 17 00:00:00 2001 From: Varun Ravichandran Date: Mon, 18 Nov 2024 15:36:18 -0500 Subject: [PATCH] SERVER-95701 Write authorization schema document on FCV downgrade when necessary (#29054) GitOrigin-RevId: 5b6630666e29cd473b68edadd9556bddad78e0a6 --- .../authz_schema_version.js | 126 ++++++++++++++++++ src/mongo/db/auth/authorization_manager.cpp | 3 + src/mongo/db/auth/authorization_manager.h | 11 ++ ..._feature_compatibility_version_command.cpp | 44 ++++++ 4 files changed, 184 insertions(+) create mode 100644 jstests/multiVersion/targetedTestsLastLtsFeatures/authz_schema_version.js diff --git a/jstests/multiVersion/targetedTestsLastLtsFeatures/authz_schema_version.js b/jstests/multiVersion/targetedTestsLastLtsFeatures/authz_schema_version.js new file mode 100644 index 00000000000..6a66d1cf334 --- /dev/null +++ b/jstests/multiVersion/targetedTestsLastLtsFeatures/authz_schema_version.js @@ -0,0 +1,126 @@ +// Test that binary upgrade/downgrade works with auth schema version. +// Test that initial sync with users works with latest primary last-lts secondaries and vice +// versa. + +import "jstests/multiVersion/libs/multi_rs.js"; + +import {ReplSetTest} from "jstests/libs/replsettest.js"; + +const keyfile = "jstests/libs/key1"; +const authSchemaColl = "system.version"; +const schemaVersion28SCRAM = 5; + +// We need to upgrade the TestData's privileges so that the test itself can perform the +// necessary commands within rst.upgradeSet. +TestData.auth = true; +TestData.keyFile = keyfile; +TestData.authUser = "__system"; +TestData.keyFileData = "foopdedoop"; +TestData.authenticationDatabase = "local"; + +function testChangeBinariesWithAuthzSchemaDoc(originalBinVersion, updatedBinVersion) { + const rst = new ReplSetTest( + {nodes: 2, nodeOptions: {binVersion: originalBinVersion, auth: ''}, keyFile: keyfile}); + rst.startSet(); + rst.initiate(); + + // Create a user, which is a necessary precondition for requiring authorization schema document + // on lower binVersion binaries . + let primary = rst.getPrimary(); + let adminDB = primary.getDB("admin"); + assert.commandWorked(adminDB.runCommand( + {createUser: "admin", pwd: "admin", roles: [{role: "root", db: "admin"}]})); + + // If updatedBinVersion is last-lts, then we need to downgrade FCV before downgrading the + // binary. + if (updatedBinVersion === 'last-lts') { + assert.commandWorked( + adminDB.runCommand({setFeatureCompatibilityVersion: lastLTSFCV, confirm: true})); + } + + // Query the authorization schema doc to ensure that it exists and is set to + // schemaVersion28SCRAM. On 'last-lts' binaries that are being upgraded, this doc + // should have been created during `createUser`. On `latest` binaries that are being + // downgraded, it should have been created during FCV downgrade since at least 1 user + // document (`admin`) existed. + const currentVersion = adminDB[authSchemaColl].findOne({_id: 'authSchema'}).currentVersion; + assert.eq(currentVersion, schemaVersion28SCRAM); + + // Change binaries to updatedBinVersion - this may constitute an upgrade or a downgrade. + // Last-lts binaries write and read the authorization schema document, while latest binaries + // should only write it down during FCV downgrade if there are any user or role docs on-disk. + // Latest binaries never read from it. + // Therefore, upgrade should always work without issues while downgrade + // should work as long as FCV is properly downgraded before binary downgrade. + rst.upgradeSet({binVersion: updatedBinVersion, keyFile: keyfile}); + + // Retrieve the new primary and run usersInfo to make sure everything works normally. + primary = rst.getPrimary(); + adminDB = primary.getDB("admin"); + const usersInfoReply = assert.commandWorked(adminDB.runCommand({usersInfo: 1})); + assert(usersInfoReply.hasOwnProperty("users")); + assert.eq(usersInfoReply.users.length, 1); + + rst.stopSet(); +} + +function testMixedVersionInitialSync(primaryBinVersion, newNodeBinVersion) { + // Create a single-node replica set with binVersion primaryBinVersion. + const rst = new ReplSetTest( + {nodes: 1, nodeOptions: {binVersion: primaryBinVersion, auth: ''}, keyFile: keyfile}); + rst.startSet(); + rst.initiate(); + + // Create a user, which should automatically create an authorization schema document. + let primary = rst.getPrimary(); + let adminDB = primary.getDB("admin"); + assert.commandWorked(adminDB.runCommand( + {createUser: "admin", pwd: "admin", roles: [{role: "root", db: "admin"}]})); + + // If newNodeBinVersion is 'last-lts', then we need to downgrade FCV prior to adding the new + // node. + if (newNodeBinVersion === 'last-lts') { + assert.commandWorked( + adminDB.runCommand({setFeatureCompatibilityVersion: lastLTSFCV, confirm: true})); + } + + // Query the authorization schema doc to ensure that it exists and is set to + // schemaVersion28SCRAM. On 'last-lts' binaries that are being upgraded, this doc + // should have been created during `createUser`. On `latest` binaries that are being + // downgraded, it should have been created during FCV downgrade. + const currentVersion = adminDB[authSchemaColl].findOne({_id: 'authSchema'}).currentVersion; + assert.eq(currentVersion, schemaVersion28SCRAM); + + // Add a newNodeBinVersion node to the replica set and check that initial sync succeeds. The new + // node will have no priority or + let secondary = rst.add( + {binVersion: newNodeBinVersion, keyFile: keyfile, rsConfig: {votes: 0, priority: 0}}); + + // Reinitiate the replica set and check that initial sync has completed on the secondary. + rst.reInitiate(); + assert.commandWorked( + primary.getDB("test").coll.insert({awaitRepl: true}, {writeConcern: {w: 2}})); + rst.awaitReplication(); + rst.awaitSecondaryNodes(); + + // Run usersInfo on the secondary node and check that 1 user (admin) has been replicated. + secondary = rst.getSecondary(); + const secondaryAdminDB = secondary.getDB("admin"); + const usersInfoReply = assert.commandWorked(secondaryAdminDB.runCommand({usersInfo: 1})); + assert(usersInfoReply.hasOwnProperty("users")); + assert.eq(usersInfoReply.users.length, 1); + + rst.stopSet(); +} + +// Upgrade +testChangeBinariesWithAuthzSchemaDoc('last-lts', 'latest'); + +// Downgrade +testChangeBinariesWithAuthzSchemaDoc('latest', 'last-lts'); + +// Initial sync for a last-lts node from an latest primary. +testMixedVersionInitialSync('latest', 'last-lts'); + +// Initial sync for a latest node from a last-lts primary. +testMixedVersionInitialSync('last-lts', 'latest'); diff --git a/src/mongo/db/auth/authorization_manager.cpp b/src/mongo/db/auth/authorization_manager.cpp index ae3a520f86b..cdce3385b03 100644 --- a/src/mongo/db/auth/authorization_manager.cpp +++ b/src/mongo/db/auth/authorization_manager.cpp @@ -53,4 +53,7 @@ constexpr StringData AuthorizationManager::V1_USER_SOURCE_FIELD_NAME; const Status AuthorizationManager::authenticationFailedStatus(ErrorCodes::AuthenticationFailed, "Authentication failed."); +const BSONObj AuthorizationManager::versionDocumentQuery = BSON("_id" + << "authSchema"); + } // namespace mongo diff --git a/src/mongo/db/auth/authorization_manager.h b/src/mongo/db/auth/authorization_manager.h index 5a5e5d76d93..92b6abfe52d 100644 --- a/src/mongo/db/auth/authorization_manager.h +++ b/src/mongo/db/auth/authorization_manager.h @@ -128,6 +128,17 @@ public: */ static const Status authenticationFailedStatus; + /** + * Query to match the auth schema version document in the versionCollectionNamespace while + * upserting it on FCV downgrade. + */ + static const BSONObj versionDocumentQuery; + + /** + * Name of the field in the auth schema version document containing the current schema version. + */ + static constexpr StringData schemaVersionFieldName = "currentVersion"_sd; + /** * Auth schema version for MongoDB 3.0 SCRAM only mode. * Users are stored in admin.system.users, roles in admin.system.roles. diff --git a/src/mongo/db/commands/set_feature_compatibility_version_command.cpp b/src/mongo/db/commands/set_feature_compatibility_version_command.cpp index f0252ce9d29..58226e19b46 100644 --- a/src/mongo/db/commands/set_feature_compatibility_version_command.cpp +++ b/src/mongo/db/commands/set_feature_compatibility_version_command.cpp @@ -74,6 +74,7 @@ #include "mongo/db/concurrency/exception_util.h" #include "mongo/db/concurrency/lock_manager_defs.h" #include "mongo/db/database_name.h" +#include "mongo/db/db_raii.h" #include "mongo/db/dbdirectclient.h" #include "mongo/db/dbhelpers.h" #include "mongo/db/drop_gen.h" @@ -1168,6 +1169,48 @@ private: } } + // Insert the authorization schema document in admin.system.version if there are any user or + // role documents on-disk. This must be performed on FCV downgrade since lower-version binaries + // assert that this document exists when users and/or roles exist during initial sync. + void _createAuthzSchemaVersionDocIfNeeded(OperationContext* opCtx) { + // Check if any user or role documents exist on-disk. + bool hasUserDocs, hasRoleDocs = false; + BSONObj userDoc, roleDoc; + { + AutoGetCollectionForReadCommandMaybeLockFree usersColl( + opCtx, NamespaceString::kAdminUsersNamespace); + hasUserDocs = Helpers::findOne(opCtx, usersColl.getCollection(), BSONObj(), userDoc); + } + + { + AutoGetCollectionForReadCommandMaybeLockFree rolesColl( + opCtx, NamespaceString::kAdminRolesNamespace); + hasRoleDocs = Helpers::findOne(opCtx, rolesColl.getCollection(), BSONObj(), roleDoc); + } + + // If they do, write an authorization schema document to disk set to schemaVersionSCRAM28. + if (hasUserDocs || hasRoleDocs) { + DBDirectClient client(opCtx); + auto result = client.update([&] { + write_ops::UpdateCommandRequest updateOp( + NamespaceString::kServerConfigurationNamespace); + updateOp.setUpdates({[&] { + write_ops::UpdateOpEntry entry; + entry.setQ(AuthorizationManager::versionDocumentQuery); + entry.setU(write_ops::UpdateModification::parseFromClassicUpdate( + BSON("$set" << BSON(AuthorizationManager::schemaVersionFieldName + << AuthorizationManager::schemaVersion28SCRAM)))); + entry.setMulti(false); + entry.setUpsert(true); + return entry; + }()}); + return updateOp; + }()); + + write_ops::checkWriteErrors(result); + } + } + // This helper function is for any internal server downgrade cleanup, such as dropping // collections or aborting. This cleanup will happen after user collection downgrade // cleanup. @@ -1231,6 +1274,7 @@ private: } _cleanUpClusterParameters(opCtx, requestedVersion); + _createAuthzSchemaVersionDocIfNeeded(opCtx); // Note the config server is also considered a shard, so the ConfigServer and ShardServer // roles aren't mutually exclusive. if (role && role->has(ClusterRole::ConfigServer)) {