From ecbdf9983cb952bc140b88af663b72a659d9d269 Mon Sep 17 00:00:00 2001 From: Tommaso Tocci <58224719+toto-dev@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:29:26 +0200 Subject: [PATCH] SERVER-118828 Allow applyOps to execute upgradeDowngradeViewlessTimeseries c-entry for system-authenticated users (#51066) GitOrigin-RevId: 3db433cd8fb97a5159e85947f45f76d53ffad6f4 --- .../timeseries_upgrade_downgrade_apply_ops.js | 92 ++++++++++++++++ jstests/noPassthrough/OWNERS.yml | 3 + .../timeseries_upgrade_downgrade_apply_ops.js | 102 ++++++++++++++++++ .../db/commands/oplog_application_checks.cpp | 20 +++- src/mongo/db/repl/oplog.cpp | 2 + 5 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 jstests/auth/timeseries_upgrade_downgrade_apply_ops.js create mode 100644 jstests/noPassthrough/timeseries_upgrade_downgrade_apply_ops.js diff --git a/jstests/auth/timeseries_upgrade_downgrade_apply_ops.js b/jstests/auth/timeseries_upgrade_downgrade_apply_ops.js new file mode 100644 index 00000000000..db39c0a233e --- /dev/null +++ b/jstests/auth/timeseries_upgrade_downgrade_apply_ops.js @@ -0,0 +1,92 @@ +/** + * Tests that the upgradeDowngradeViewlessTimeseries oplog entry applied via applyOps requires + * the __system role when auth is enabled. + * + * TODO(SERVER-114573): Remove once 9.0 becomes last-lts, as the oplog entry won't be + * supported anymore. + * + * @tags: [ + * featureFlagCreateViewlessTimeseriesCollections, + * ] + */ +import {ReplSetTest} from "jstests/libs/replsettest.js"; + +if (lastLTSFCV != "8.0") { + jsTest.log.info("Skipping test because last LTS FCV is no longer 8.0"); + quit(); +} + +const keyFile = "jstests/libs/key1"; +const rst = new ReplSetTest({nodes: 1, keyFile: keyFile}); +rst.startSet(); +rst.initiate(); + +const primary = rst.getPrimary(); +const adminDB = primary.getDB("admin"); + +adminDB.createUser({user: "admin", pwd: "pwd", roles: ["root"]}); +assert(adminDB.auth("admin", "pwd")); + +const dbName = jsTestName(); +const collName = "ts"; +const testDB = primary.getDB(dbName); +const bucketsColl = testDB.getCollection("system.buckets." + collName); + +const timeseriesOptions = { + timeField: "t", + granularity: "seconds", + bucketMaxSpanSeconds: 3600, +}; + +function makeCreateCmd(collectionName) { + return { + applyOps: [ + { + op: "c", + ns: dbName + ".$cmd", + o: { + create: collectionName, + clusteredIndex: true, + timeseries: timeseriesOptions, + }, + }, + ], + }; +} + +function makeUpgradeDowngradeCmd(isUpgrade, uuid) { + return { + applyOps: [ + { + op: "c", + ns: dbName + ".$cmd", + o: {upgradeDowngradeViewlessTimeseries: collName, isUpgrade: isUpgrade}, + ui: uuid, + }, + ], + }; +} + +// Create a legacy (viewful) timeseries collection via applyOps at FCV 9.0. +assert.commandWorked(testDB.adminCommand(makeCreateCmd("system.buckets." + collName))); +const collUUID = bucketsColl.getUUID(); + +// A root user should not be able to applyOps an internal-only oplog entry. +assert.commandFailedWithCode(testDB.adminCommand(makeUpgradeDowngradeCmd(true, collUUID)), ErrorCodes.Unauthorized); + +adminDB.logout(); + +// The __system role is required to applyOps internal-only oplog entries. +rst.asCluster(primary, () => { + // Upgrade: viewful -> viewless via applyOps. + assert.commandWorked(testDB.adminCommand(makeUpgradeDowngradeCmd(true, collUUID))); + assert.eq(null, bucketsColl.exists(), "Expected no system.buckets collection after upgrade"); + + // Downgrade: viewless -> viewful via applyOps. + // First downgrade FCV so the feature flag is disabled (required for downgrade path). + assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: lastLTSFCV, confirm: true})); + assert.commandWorked(testDB.adminCommand(makeUpgradeDowngradeCmd(false, collUUID))); + assert.neq(null, bucketsColl.exists(), "Expected system.buckets collection after downgrade"); +}); + +rst.stopSet(); diff --git a/jstests/noPassthrough/OWNERS.yml b/jstests/noPassthrough/OWNERS.yml index d6d0ace13b4..a35bbf649d7 100644 --- a/jstests/noPassthrough/OWNERS.yml +++ b/jstests/noPassthrough/OWNERS.yml @@ -24,3 +24,6 @@ filters: - "delinquent*": approvers: - 10gen/server-workload-resilience + - "timeseries_upgrade_downgrade_apply_ops.js": + approvers: + - 10gen/server-catalog-and-routing-shard-catalog diff --git a/jstests/noPassthrough/timeseries_upgrade_downgrade_apply_ops.js b/jstests/noPassthrough/timeseries_upgrade_downgrade_apply_ops.js new file mode 100644 index 00000000000..5d83d770238 --- /dev/null +++ b/jstests/noPassthrough/timeseries_upgrade_downgrade_apply_ops.js @@ -0,0 +1,102 @@ +/** + * Tests that the upgradeDowngradeViewlessTimeseries oplog entry can be applied via applyOps. + * + * TODO(SERVER-114573): Remove once 9.0 becomes last-lts, as the oplog entry won't be + * supported anymore. + * + * @tags: [ + * featureFlagCreateViewlessTimeseriesCollections, + * ] + */ +import {ReplSetTest} from "jstests/libs/replsettest.js"; + +if (lastLTSFCV != "8.0") { + jsTest.log.info("Skipping test because last LTS FCV is no longer 8.0"); + quit(); +} + +const rst = new ReplSetTest({nodes: 1}); +rst.startSet(); +rst.initiate(); + +const primary = rst.getPrimary(); +const adminDB = primary.getDB("admin"); +const dbName = jsTestName(); +const collName = "ts"; +const testDB = primary.getDB(dbName); +const bucketsColl = testDB.getCollection("system.buckets." + collName); +const mainColl = testDB.getCollection(collName); + +const timeseriesOptions = { + timeField: "t", + granularity: "seconds", + bucketMaxSpanSeconds: 3600, +}; + +function makeCreateCmd(collectionName) { + return { + applyOps: [ + { + op: "c", + ns: dbName + ".$cmd", + o: { + create: collectionName, + clusteredIndex: true, + timeseries: timeseriesOptions, + }, + }, + ], + }; +} + +function makeUpgradeDowngradeCmd(isUpgrade, uuid) { + return { + applyOps: [ + { + op: "c", + ns: dbName + ".$cmd", + o: {upgradeDowngradeViewlessTimeseries: collName, isUpgrade: isUpgrade}, + ui: uuid, + }, + ], + }; +} + +// Phase 1: Test upgrade path at FCV 9.0 (feature flag enabled). +// Use applyOps to create a legacy (viewful) timeseries collection, then upgrade it. +{ + jsTest.log("Phase 1: Testing timeseries upgrade via applyOps at latest FCV"); + + // Create a legacy system.buckets collection via applyOps. + assert.commandWorked(testDB.adminCommand(makeCreateCmd("system.buckets." + collName))); + const collUUID = bucketsColl.getUUID(); + assert.neq(null, bucketsColl.exists(), "system.buckets collection should exist after create"); + + // Upgrade: viewful -> viewless via applyOps. + assert.commandWorked(testDB.adminCommand(makeUpgradeDowngradeCmd(true, collUUID))); + assert.eq(null, bucketsColl.exists(), "Expected no system.buckets collection after upgrade"); + assert.neq(null, mainColl.exists(), "Expected main collection after upgrade"); + + // Clean up for phase 2. + assert.commandWorked(testDB.runCommand({drop: collName})); +} + +// Phase 2: Test downgrade path at FCV 8.0 (feature flag disabled). +// Use applyOps to create a viewless timeseries collection, then downgrade it. +{ + jsTest.log("Phase 2: Testing downgrade via applyOps at last-LTS FCV"); + + assert.commandWorked(adminDB.runCommand({setFeatureCompatibilityVersion: lastLTSFCV, confirm: true})); + + // Create a viewless timeseries collection via applyOps. + assert.commandWorked(testDB.adminCommand(makeCreateCmd(collName))); + const collUUID = mainColl.getUUID(); + assert.neq(null, mainColl.exists(), "Main collection should exist after create"); + assert.eq(null, bucketsColl.exists(), "system.buckets should not exist for viewless collection"); + + // Downgrade: viewless -> viewful via applyOps. + assert.commandWorked(testDB.adminCommand(makeUpgradeDowngradeCmd(false, collUUID))); + assert.neq(null, bucketsColl.exists(), "Expected system.buckets collection after downgrade"); +} + +rst.stopSet(); diff --git a/src/mongo/db/commands/oplog_application_checks.cpp b/src/mongo/db/commands/oplog_application_checks.cpp index 9b95a2f764c..03dea46bb6b 100644 --- a/src/mongo/db/commands/oplog_application_checks.cpp +++ b/src/mongo/db/commands/oplog_application_checks.cpp @@ -54,6 +54,7 @@ #include "mongo/rpc/op_msg.h" #include "mongo/util/assert_util.h" #include "mongo/util/str.h" +#include "mongo/util/string_map.h" #include #include @@ -111,6 +112,21 @@ Status OplogApplicationChecks::checkOperationAuthorization(OperationContext* opC StringData commandName = o.firstElement().fieldNameStringData(); Command* commandInOplogEntry = CommandHelpers::findCommand(opCtx, commandName); if (!commandInOplogEntry) { + // Some oplog entries are internal-only and not registered in the global command + // registry. Allow them through if the user has ActionType::internal (e.g. __system). + // TODO(SERVER-114573): Remove upgradeDowngradeViewlessTimeseries from this list once + // 9.0 becomes last-lts, as the oplog entry will no longer exist. + static const StringDataSet kInternalOplogCommands{ + "upgradeDowngradeViewlessTimeseries"_sd, + }; + if (kInternalOplogCommands.contains(commandName)) { + if (!authSession->isAuthorizedForActionsOnResource( + ResourcePattern::forClusterResource(dbName.tenantId()), + ActionType::internal)) { + return Status(ErrorCodes::Unauthorized, "Unauthorized"); + } + return Status::OK(); + } return Status(ErrorCodes::FailedToParse, "Unrecognized command in op"); } @@ -123,8 +139,8 @@ Status OplogApplicationChecks::checkOperationAuthorization(OperationContext* opC nss.tenantId(), "admin", SerializationContext::stateDefault()); } - // TODO reuse the parse result for when we run() later. Note that when running, - // we must use a potentially different dbname. + // TODO SERVER-123371 reuse the parse result for when we run() later. Note that when + // running, we must use a potentially different dbname. return [&] { try { boost::optional vts; diff --git a/src/mongo/db/repl/oplog.cpp b/src/mongo/db/repl/oplog.cpp index 873e1d0cf43..00f248672cd 100644 --- a/src/mongo/db/repl/oplog.cpp +++ b/src/mongo/db/repl/oplog.cpp @@ -1368,6 +1368,8 @@ const StringMap kOpsMap = { return Status::OK(); }, {ErrorCodes::NamespaceNotFound}}}, + // TODO(SERVER-114573): Remove once 9.0 becomes last-lts, as this oplog entry won't be + // supported anymore. {"upgradeDowngradeViewlessTimeseries", {[](OperationContext* opCtx, const ApplierOperation& op, OplogApplication::Mode mode) -> Status {