SERVER-123367 Handle auth for new fastcount oplog entries (#52165)
GitOrigin-RevId: 34e5de0398f32b7597239bddd98f8cedb652c93e
This commit is contained in:
parent
03622f2bfb
commit
27c039d8a9
113
jstests/auth/init_replicated_fast_count.js
Normal file
113
jstests/auth/init_replicated_fast_count.js
Normal 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();
|
||||
@ -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"},
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -551,8 +551,6 @@ roles:
|
||||
- insert
|
||||
- performRawDataOperations
|
||||
- updateSearchIndex
|
||||
- containerInsert
|
||||
- containerDelete
|
||||
- matchType: database
|
||||
db: "config"
|
||||
actions: *restoreRoleWriteActions
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user