SERVER-123367 Handle auth for new fastcount oplog entries (#52165)

GitOrigin-RevId: 34e5de0398f32b7597239bddd98f8cedb652c93e
This commit is contained in:
Parker Felix 2026-04-20 16:15:09 -04:00 committed by MongoDB Bot
parent 03622f2bfb
commit 27c039d8a9
5 changed files with 169 additions and 6 deletions

View File

@ -0,0 +1,113 @@
/*
* Auth test for the initReplicatedFastcount command wrapped in an applyOps command, running against
* a standalone mongod and a replica set.
* @tags: [
* featureFlagReplicatedFastCount,
* requires_replication,
* requires_fcv_90
* ]
*/
import {ReplSetTest} from "jstests/libs/replsettest.js";
// Auth-test the initReplicatedFastCount oplog command, when executed inside an applyOps command.
export function runTest(mongod) {
const admin = mongod.getDB("admin");
admin.createUser({user: "admin", pwd: "pass", roles: jsTest.adminUserRoles});
assert(admin.auth("admin", "pass"));
// Create roles with database-level permissions for test1 and test2.
["test1", "test2"].forEach((dbName) => {
admin.createRole({
role: `${dbName}_insert`,
privileges: [
// applyOps privilege is needed to send a initReplicatedFastCount command.
{resource: {cluster: true}, actions: ["applyOps"]},
{resource: {db: dbName, collection: ""}, actions: ["insert"]},
],
roles: [],
});
});
// user1 has privileges on test1.
admin.createUser({
user: "user1",
pwd: "pass",
roles: ["test1_insert"],
});
// user2 has privileges on test2.
admin.createUser({
user: "user2",
pwd: "pass",
roles: ["test2_insert"],
});
// user3 has no privileges.
admin.createUser({
user: "user3",
pwd: "pass",
roles: [],
});
// user4 is a __system user with privileges on the admin database.
admin.createUser({
user: "user4",
pwd: "pass",
roles: ["__system"],
});
admin.logout();
const runAuthTest = function ({user, dbName, expectedError}) {
admin.auth(user, "pass");
const db = admin.getSiblingDB(dbName);
// Run initReplicatedFastCount command as part of an applyOps command. This is necessary
// because there is no user-facing initReplicatedFastCount command in the server.
const applyOpsCmd = {
applyOps: [
{
op: "c",
ns: `${dbName}.$cmd`,
o: {
"initReplicatedFastCount": 1,
},
// Don't need an o2 since the op will be rejected.
},
],
};
if (expectedError) {
assert.commandFailedWithCode(db.runCommand(applyOpsCmd), [expectedError]);
} else {
assert.commandWorked(db.runCommand(applyOpsCmd));
}
admin.logout();
};
// user1 and user2 have applyOps privilege (from their roles), so the applyOps auth check
// passes. The initReplicatedFastCount command is not a registered command, so
// CommandHelpers::findCommand returns null and we get FailedToParse regardless of which
// database the op targets.
runAuthTest({user: "user1", dbName: "test1", expectedError: ErrorCodes.FailedToParse});
runAuthTest({user: "user1", dbName: "test2", expectedError: ErrorCodes.FailedToParse});
runAuthTest({user: "user2", dbName: "test1", expectedError: ErrorCodes.FailedToParse});
runAuthTest({user: "user2", dbName: "test2", expectedError: ErrorCodes.FailedToParse});
// user3 has no privileges -> Unauthorized (fails the applyOps cluster-level check).
runAuthTest({user: "user3", dbName: "test1", expectedError: ErrorCodes.Unauthorized});
runAuthTest({user: "user3", dbName: "test2", expectedError: ErrorCodes.Unauthorized});
// user4 (__system) has all privileges -> FailedToParse on admin.
runAuthTest({user: "user4", dbName: "admin", expectedError: ErrorCodes.FailedToParse});
}
const mongod = MongoRunner.runMongod({auth: ""});
runTest(mongod);
MongoRunner.stopMongod(mongod);
const replTest = new ReplSetTest({name: jsTestName(), nodes: 2, keyFile: "jstests/libs/key1"});
replTest.startSet();
replTest.initiate();
runTest(replTest.getPrimary());
replTest.stopSet();

View File

@ -384,8 +384,8 @@ export const authCommandsLib = {
},
testcases: [
{runOnDb: firstDbName, roles: {__system: 1, root: 1, restore: 1}},
{runOnDb: "local", roles: {__system: 1, root: 1, restore: 1}},
{runOnDb: firstDbName, roles: {__system: 1}},
{runOnDb: "local", roles: {__system: 1}},
{
runOnDb: firstDbName,
privileges: [
@ -426,8 +426,8 @@ export const authCommandsLib = {
testcases: [
// Attempting to delete a nonexistent key from a container fails.
{runOnDb: firstDbName, roles: {__system: 1, root: 1, restore: 1}, expectFail: true},
{runOnDb: "local", roles: {__system: 1, root: 1, restore: 1}, expectFail: true},
{runOnDb: firstDbName, roles: {__system: 1}, expectFail: true},
{runOnDb: "local", roles: {__system: 1}, expectFail: true},
{
runOnDb: firstDbName,
privileges: [
@ -438,6 +438,49 @@ export const authCommandsLib = {
},
],
},
{
testname: "applyOps_container_update",
skipSharded: true,
skipTest: (conn) => !isFeatureEnabled(conn, "featureFlagContainerWrites") || !storageEngineIsWiredTiger(),
setup: function (db) {
const coll = "containerOpsColl";
db[coll].drop();
assert.commandWorked(db.createCollection(coll));
const uri = getUriForColl(db[coll]);
return {nss: db.getName() + "." + coll, coll, uri};
},
command: function (state) {
return {
applyOps: [
{
op: "cu",
ns: state.nss,
container: state.uri,
o: {k: BinData(0, "QQ=="), v: BinData(0, "Qg==")},
},
],
};
},
teardown: function (db, state) {
if (state && state.coll) db[state.coll].drop();
},
testcases: [
// Attempting to update a nonexistent key in a container fails.
{runOnDb: firstDbName, roles: {__system: 1}, expectFail: true},
{runOnDb: "local", roles: {__system: 1}, expectFail: true},
{
runOnDb: firstDbName,
privileges: [
{resource: {db: firstDbName, collection: ""}, actions: ["containerUpdate"]},
{resource: {cluster: true}, actions: ["applyOps"]},
],
expectFail: true,
},
],
},
{
testname: "abortMoveCollection",
command: {abortMoveCollection: "test.x"},

View File

@ -221,6 +221,7 @@ enums:
applyOps: "applyOps"
containerInsert: "containerInsert" # run "ci" ops in the applyOps command
containerDelete: "containerDelete" # run "cd" ops in the applyOps command
containerUpdate: "containerUpdate" # run "cu" ops in the applyOps command
# In 'MatchType' the extra_data field "serverlessActionTypes" is used
# by the AuthorizationSession while in multitenancy mode to determine
@ -287,6 +288,7 @@ enums:
- listSearchIndexes
- containerInsert
- containerDelete
- containerUpdate
- updateSearchIndex
- performRawDataOperations
- planCacheIndexFilter
@ -340,6 +342,7 @@ enums:
- updateSearchIndex
- containerInsert
- containerDelete
- containerUpdate
- performRawDataOperations
- planCacheIndexFilter
- planCacheRead
@ -428,6 +431,7 @@ enums:
- validate
- containerInsert
- containerDelete
- containerUpdate
# resource: { db: '', system_buckets: 'exact' }
kMatchSystemBucketInAnyDBResource:

View File

@ -551,8 +551,6 @@ roles:
- insert
- performRawDataOperations
- updateSearchIndex
- containerInsert
- containerDelete
- matchType: database
db: "config"
actions: *restoreRoleWriteActions

View File

@ -204,6 +204,11 @@ Status OplogApplicationChecks::checkOperationAuthorization(OperationContext* opC
return Status(ErrorCodes::Unauthorized, "Unauthorized");
}
return Status::OK();
} else if (opType == "cu"_sd) {
if (!authSession->isAuthorizedForActionsOnNamespace(nss, ActionType::containerUpdate)) {
return Status(ErrorCodes::Unauthorized, "Unauthorized");
}
return Status::OK();
}
return Status(ErrorCodes::FailedToParse, "Unrecognized opType");