SERVER-101941 Require cluster user privileges to append dollar audit on mongos (#33713)

GitOrigin-RevId: 39bc10e94fe285cc50b9e7c0138fcaab1b624f0c
This commit is contained in:
Varun Ravichandran 2025-03-18 13:26:46 -07:00 committed by MongoDB Bot
parent 0bb936d5e7
commit 7435f68e0e
4 changed files with 133 additions and 37 deletions

View File

@ -2,9 +2,10 @@
// @tags: [requires_replication]
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";
function testMongod(mongod, systemuserpwd = undefined) {
const admin = mongod.getDB('admin');
function runTest(conn, keyFile = undefined) {
const admin = conn.getDB('admin');
admin.createUser({user: 'admin', pwd: 'admin', roles: ['root']});
function assertError(cmd, msg, code) {
@ -56,15 +57,14 @@ function testMongod(mongod, systemuserpwd = undefined) {
ErrorCodes.Unauthorized);
admin.logout();
if (systemuserpwd !== undefined) {
// On a ReplSet, our impersonation payload should be fine with cluster user.
if (keyFile !== undefined) {
// On a ReplSet or mongos, our impersonation payload should be fine with cluster user.
jsTest.log('Positive test, impersonation is okay when we\'re local.__system');
const local = mongod.getDB('local');
local.auth('__system', systemuserpwd);
assert.commandWorked(admin.runCommand(kImpersonatedUserHello));
assert.commandWorked(admin.runCommand(kImpersonatedClientHello));
local.logout();
authutil.asCluster(conn, keyFile, () => {
assert.commandWorked(admin.runCommand(kImpersonatedUserHello));
assert.commandWorked(admin.runCommand(kImpersonatedClientHello));
});
}
jsTest.log('End');
@ -72,18 +72,31 @@ function testMongod(mongod, systemuserpwd = undefined) {
{
const standalone = MongoRunner.runMongod({auth: ''});
testMongod(standalone);
runTest(standalone);
MongoRunner.stopMongod(standalone);
}
{
const kKeyfile = 'jstests/libs/key1';
const kKey = cat(kKeyfile).replace(/[\011-\015\040]/g, '');
const rst = new ReplSetTest({nodes: 2});
rst.startSet({keyFile: kKeyfile});
rst.initiate();
rst.awaitSecondaryNodes();
testMongod(rst.getPrimary(), kKey);
runTest(rst.getPrimary(), kKeyfile);
rst.stopSet();
}
{
const kKeyfile = 'jstests/libs/key1';
const st = new ShardingTest({
mongos: 1,
config: 1,
shard: 2,
keyFile: kKeyfile,
other: {mongosOptions: {auth: null}, configOptions: {auth: null}, rsOptions: {auth: null}}
});
runTest(st.s0, kKeyfile);
st.stop();
}

View File

@ -1651,22 +1651,6 @@ void ExecCommandDatabase::_initiateCommand() {
"Skipping command execution for help request"));
}
if (auto auditUserAttrs = rpc::AuditUserAttrs::get(opCtx);
auditUserAttrs && auditUserAttrs->getIsImpersonating()) {
uassert(
ErrorCodes::Unauthorized,
"Unauthorized use of impersonation metadata.",
authzSession->isAuthorizedForClusterActions({ActionType::impersonate}, boost::none));
}
if (auto auditClientAttrs = rpc::AuditClientAttrs::get(client);
auditClientAttrs && auditClientAttrs->getIsImpersonating()) {
uassert(
ErrorCodes::Unauthorized,
"Unauthorized use of impersonation metadata.",
authzSession->isAuthorizedForClusterActions({ActionType::impersonate}, boost::none));
}
_invocation->checkAuthorization(opCtx, _execContext.getRequest());
if (!opCtx->getClient()->isInDirectClient() &&

View File

@ -54,7 +54,7 @@
namespace mongo {
namespace rpc {
void setAuditClientMetadata(OperationContext* opCtx,
const boost::optional<AuditMetadata>& data,
const ImpersonatedClientMetadata& clientMetadata,
boost::optional<ImpersonatedClientSessionGuard>& clientSessionGuard) {
// TODO SERVER-83990: remove
if (!gFeatureFlagExposeClientIpInAuditLogs.isEnabledUseLastLTSFCVWhenUninitialized(
@ -66,20 +66,32 @@ void setAuditClientMetadata(OperationContext* opCtx,
return;
}
if (!data || !data->getClientMetadata()) {
return;
}
clientSessionGuard.emplace(opCtx->getClient(), data->getClientMetadata().value());
clientSessionGuard.emplace(opCtx->getClient(), clientMetadata);
}
void setAuditMetadata(OperationContext* opCtx,
const boost::optional<AuditMetadata>& data,
boost::optional<ImpersonatedClientSessionGuard>& clientSessionGuard) {
setAuditClientMetadata(opCtx, data, clientSessionGuard);
if (data) {
if (auto& user = data->getUser()) {
rpc::AuditUserAttrs::set(opCtx, *user, data->getRoles(), true /* isImpersonating */);
const auto& user = data->getUser();
const auto& roles = data->getRoles();
const auto& clientMetadata = data->getClientMetadata();
if (user || !roles.empty() || clientMetadata) {
// Only set $impersonatedUser, $impersonatedRoles, or $impersonatedClient if the client
// is authorized for cluster-level privileges.
auto* authzSession = AuthorizationSession::get(opCtx->getClient());
uassert(ErrorCodes::Unauthorized,
"Unauthorized use of impersonation metadata.",
authzSession->isAuthorizedForClusterActions({ActionType::impersonate},
boost::none));
if (clientMetadata) {
setAuditClientMetadata(opCtx, clientMetadata.value(), clientSessionGuard);
}
if (user || !roles.empty()) {
rpc::AuditUserAttrs::set(opCtx, *user, roles, true /* isImpersonating */);
}
}
}
}

View File

@ -157,6 +157,9 @@ TEST_F(AuditMetadataTest, ReadAuditMetadata) {
ASSERT_TRUE(auditClientAttrs);
ASSERT_TRUE(auditUserAttrs);
ASSERT_EQ(auditUserAttrs->getUser().getUser(), kUserName);
ASSERT_EQ(auditUserAttrs->getUser().getDatabaseName().toString_forTest(), kDBName);
ASSERT_EQ(auditClientAttrs->getRemote(), HostAndPort::parse(kRemoteAddr));
ASSERT_EQ(kExpectedProxies.size(), auditClientAttrs->getProxies().size());
for (size_t i = 0; i < auditClientAttrs->getProxies().size(); i++) {
@ -173,6 +176,90 @@ TEST_F(AuditMetadataTest, ReadAuditMetadata) {
ASSERT_TRUE(auditUserAttrs);
}
TEST_F(AuditMetadataTest, ReadAuditMetadataUserOnly) {
auto auditClientAttrs = rpc::AuditClientAttrs::get(client());
auto auditUserAttrs = rpc::AuditUserAttrs::get(opCtx());
ASSERT_FALSE(auditClientAttrs);
ASSERT_FALSE(auditUserAttrs);
// Construct an AuditMetadata object representing a parsed $audit object.
BSONObj dollarAudit = BSON("$impersonatedUser" << BSON("user" << kUserName << "db" << kDBName)
<< "$impersonatedRoles" << BSONArray());
AuditMetadata parsedDollarAudit =
AuditMetadata::parse(IDLParserContext{kImpersonationMetadataSectionName}, dollarAudit);
GenericArguments requestArgs;
requestArgs.setDollarAudit(parsedDollarAudit);
{
boost::optional<rpc::ImpersonatedClientSessionGuard> clientSessionGuard;
ASSERT_FALSE(clientSessionGuard);
rpc::readRequestMetadata(opCtx(), requestArgs, false, clientSessionGuard);
ASSERT_FALSE(clientSessionGuard.has_value());
// Now, AuditClientAttrs and AuditUserAttrs should be updated to store the user
// info supplied by requestArgs. Since there was nothing in $impersonatedClient,
// auditClientAttrs should still be empty.
auditClientAttrs = rpc::AuditClientAttrs::get(client());
auditUserAttrs = rpc::AuditUserAttrs::get(opCtx());
ASSERT_FALSE(auditClientAttrs);
ASSERT_TRUE(auditUserAttrs);
ASSERT_EQ(auditUserAttrs->getUser().getUser(), kUserName);
ASSERT_EQ(auditUserAttrs->getUser().getDatabaseName().toString_forTest(), kDBName);
}
// After clientSessionGuard goes out of scope, the AuditClientAttrs should be cleared. Since
// AuditUserAttrs is scoped to a single OperationContext and that is still alive, it will be
// nonempty.
auditClientAttrs = rpc::AuditClientAttrs::get(client());
auditUserAttrs = rpc::AuditUserAttrs::get(opCtx());
ASSERT_FALSE(auditClientAttrs);
ASSERT_TRUE(auditUserAttrs);
}
TEST_F(AuditMetadataTest, ReadAuditMetadataClientOnly) {
auto auditClientAttrs = rpc::AuditClientAttrs::get(client());
auto auditUserAttrs = rpc::AuditUserAttrs::get(opCtx());
ASSERT_FALSE(auditClientAttrs);
ASSERT_FALSE(auditUserAttrs);
// Construct an AuditMetadata object representing a parsed $audit object.
BSONObj dollarAudit =
BSON("$impersonatedRoles" << BSONArray() << "$impersonatedClient"
<< BSON("hosts"
<< BSON_ARRAY(kRemoteAddr << kLocalAddr << kProxyAddr)));
AuditMetadata parsedDollarAudit =
AuditMetadata::parse(IDLParserContext{kImpersonationMetadataSectionName}, dollarAudit);
GenericArguments requestArgs;
requestArgs.setDollarAudit(parsedDollarAudit);
{
boost::optional<rpc::ImpersonatedClientSessionGuard> clientSessionGuard;
ASSERT_FALSE(clientSessionGuard);
rpc::readRequestMetadata(opCtx(), requestArgs, false, clientSessionGuard);
ASSERT_TRUE(clientSessionGuard.has_value());
// Now, AuditClientAttrs should be updated to store the client
// info supplied by requestArgs.
auditClientAttrs = rpc::AuditClientAttrs::get(client());
auditUserAttrs = rpc::AuditUserAttrs::get(opCtx());
ASSERT_TRUE(auditClientAttrs);
ASSERT_FALSE(auditUserAttrs);
ASSERT_EQ(auditClientAttrs->getRemote(), HostAndPort::parse(kRemoteAddr));
ASSERT_EQ(kExpectedProxies.size(), auditClientAttrs->getProxies().size());
for (size_t i = 0; i < auditClientAttrs->getProxies().size(); i++) {
ASSERT_EQ(auditClientAttrs->getProxies()[i], HostAndPort::parse(kExpectedProxies[i]));
}
}
// After clientSessionGuard goes out of scope, the AuditClientAttrs should be cleared.
auditClientAttrs = rpc::AuditClientAttrs::get(client());
auditUserAttrs = rpc::AuditUserAttrs::get(opCtx());
ASSERT_FALSE(auditClientAttrs);
ASSERT_FALSE(auditUserAttrs);
}
TEST_F(AuditMetadataTest, WriteAuditMetadata) {
setUpTestData();