fix(mcp): validate sibling MongoDB commands
This commit is contained in:
1 file changed
+121
-12
+121
-12
@@ -2180,14 +2180,19 @@ fn validate_mongo_command_with_groups(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if matches!(command, MongoCommand::RunCommand { .. }) {
|
let (target_database, command_to_validate) = match &command {
|
||||||
|
MongoCommand::InDatabase { database, command } => (database.as_str(), command.as_ref()),
|
||||||
|
command => (database, command),
|
||||||
|
};
|
||||||
|
ensure_database_in_scope(database_scope, target_database)?;
|
||||||
|
if matches!(command_to_validate, MongoCommand::RunCommand { .. }) {
|
||||||
return Err(tool_error(
|
return Err(tool_error(
|
||||||
"SQL_BLOCKED",
|
"SQL_BLOCKED",
|
||||||
"MongoDB runCommand is not available through MCP; review and execute it manually in DBX.",
|
"MongoDB runCommand is not available through MCP; review and execute it manually in DBX.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let MongoCommand::Aggregate { pipeline, .. } = &command {
|
if let MongoCommand::Aggregate { pipeline, .. } = command_to_validate {
|
||||||
dbx_core::mcp_policy::ensure_mongo_database_execution_scope(policy, &connection.id, database, pipeline)
|
dbx_core::mcp_policy::ensure_mongo_database_execution_scope(policy, &connection.id, target_database, pipeline)
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
let (code, message) = error
|
let (code, message) = error
|
||||||
.strip_prefix("QUERY_ERROR: ")
|
.strip_prefix("QUERY_ERROR: ")
|
||||||
@@ -2196,23 +2201,24 @@ fn validate_mongo_command_with_groups(
|
|||||||
});
|
});
|
||||||
tool_error(code, message)
|
tool_error(code, message)
|
||||||
})?;
|
})?;
|
||||||
for target_database in mongo_aggregate_target_databases(pipeline, database)? {
|
for output_database in mongo_aggregate_target_databases(pipeline, target_database)? {
|
||||||
ensure_database_in_scope(database_scope, &target_database)?;
|
ensure_database_in_scope(database_scope, &output_database)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let effective_policy = effective_policy_for_database_with_groups(policy, group_ids, connection, database);
|
let effective_policy = effective_policy_for_database_with_groups(policy, group_ids, connection, target_database);
|
||||||
let permissions = mcp_permissions(connection, &effective_policy);
|
let permissions = mcp_permissions(connection, &effective_policy);
|
||||||
// getSiblingDB() targets another database for this command only; judge that one.
|
let production_database = match command_to_validate {
|
||||||
let target_database = command.target_database().unwrap_or(database);
|
|
||||||
let production_database = match &command {
|
|
||||||
MongoCommand::Aggregate { pipeline, .. } => {
|
MongoCommand::Aggregate { pipeline, .. } => {
|
||||||
mongo_pipeline_targets_production_database(connection, target_database, pipeline)
|
mongo_pipeline_targets_production_database(connection, target_database, pipeline)
|
||||||
}
|
}
|
||||||
_ => is_production_database(connection, target_database),
|
_ => is_production_database(connection, target_database),
|
||||||
};
|
};
|
||||||
if let Err(error) =
|
if let Err(error) = mongo::validate_safety(
|
||||||
mongo::validate_safety(&command, permissions.allow_writes, permissions.allow_dangerous, production_database)
|
command_to_validate,
|
||||||
{
|
permissions.allow_writes,
|
||||||
|
permissions.allow_dangerous,
|
||||||
|
production_database,
|
||||||
|
) {
|
||||||
return Err(match error {
|
return Err(match error {
|
||||||
MongoSafetyError::WritesDisabled => tool_error(
|
MongoSafetyError::WritesDisabled => tool_error(
|
||||||
if effective_policy.read_only { "MCP_READ_ONLY" } else { "CONNECTION_READ_ONLY" },
|
if effective_policy.read_only { "MCP_READ_ONLY" } else { "CONNECTION_READ_ONLY" },
|
||||||
@@ -3340,6 +3346,109 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mongo_sibling_database_run_command_is_never_exposed_through_mcp() {
|
||||||
|
let mongo = connection("mongo", "mongo", "mongodb", "operations");
|
||||||
|
let policy = McpGlobalPolicy {
|
||||||
|
read_only: false,
|
||||||
|
allow_dangerous_sql: true,
|
||||||
|
allowed_connection_ids: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let error = validate_mongo_command(
|
||||||
|
&mongo,
|
||||||
|
&policy,
|
||||||
|
&DatabaseScope::All,
|
||||||
|
"operations",
|
||||||
|
r#"db.getSiblingDB("staging").runCommand({ping: 1})"#,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(result_text(&error).contains("SQL_BLOCKED"));
|
||||||
|
assert!(result_text(&error).contains("runCommand"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mongo_sibling_database_honors_target_scope_and_read_only_policy() {
|
||||||
|
let mongo = connection("mongo", "mongo", "mongodb", "operations");
|
||||||
|
let policy = McpGlobalPolicy {
|
||||||
|
connection_policies: vec![dbx_core::storage::McpConnectionPolicy {
|
||||||
|
connection_id: "mongo".to_string(),
|
||||||
|
read_only: false,
|
||||||
|
allow_dangerous_sql: true,
|
||||||
|
execution_mode_configured: true,
|
||||||
|
execution_mode_policy_version: Some(dbx_core::mcp_policy::MCP_EXECUTION_POLICY_VERSION),
|
||||||
|
database_scope: McpDatabaseScope::Selected,
|
||||||
|
allowed_databases: vec!["operations".to_string(), "reporting".to_string()],
|
||||||
|
database_policies: vec![
|
||||||
|
dbx_core::storage::McpDatabasePolicy {
|
||||||
|
database_name: "operations".to_string(),
|
||||||
|
read_only: false,
|
||||||
|
allow_dangerous_sql: true,
|
||||||
|
},
|
||||||
|
dbx_core::storage::McpDatabasePolicy {
|
||||||
|
database_name: "reporting".to_string(),
|
||||||
|
read_only: true,
|
||||||
|
allow_dangerous_sql: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_only_error = validate_mongo_command(
|
||||||
|
&mongo,
|
||||||
|
&policy,
|
||||||
|
&DatabaseScope::Selected(vec!["operations".to_string(), "reporting".to_string()]),
|
||||||
|
"operations",
|
||||||
|
r#"db.getSiblingDB("reporting").users.deleteMany({_id: 1})"#,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(result_text(&read_only_error).contains("MCP_READ_ONLY"));
|
||||||
|
|
||||||
|
let scope_error = validate_mongo_command(
|
||||||
|
&mongo,
|
||||||
|
&policy,
|
||||||
|
&DatabaseScope::Selected(vec!["operations".to_string()]),
|
||||||
|
"operations",
|
||||||
|
r#"db.getSiblingDB("reporting").users.find({})"#,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(result_text(&scope_error).contains("DATABASE_OUT_OF_SCOPE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mongo_sibling_database_aggregate_honors_output_scope_and_production_protection() {
|
||||||
|
let mut mongo = connection("mongo", "mongo", "mongodb", "operations");
|
||||||
|
mongo.production_databases = vec!["production".to_string()];
|
||||||
|
let policy = McpGlobalPolicy {
|
||||||
|
read_only: false,
|
||||||
|
allow_dangerous_sql: true,
|
||||||
|
allowed_connection_ids: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let production_error = validate_mongo_command(
|
||||||
|
&mongo,
|
||||||
|
&policy,
|
||||||
|
&DatabaseScope::All,
|
||||||
|
"operations",
|
||||||
|
r#"db.getSiblingDB("staging").items.aggregate([{"$out":{"db":"production","coll":"archive"}}])"#,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(result_text(&production_error).contains("PRODUCTION_WRITE_BLOCKED"));
|
||||||
|
|
||||||
|
let scope_error = validate_mongo_command(
|
||||||
|
&mongo,
|
||||||
|
&policy,
|
||||||
|
&DatabaseScope::Selected(vec!["operations".to_string(), "staging".to_string()]),
|
||||||
|
"operations",
|
||||||
|
r#"db.getSiblingDB("staging").items.aggregate([{"$merge":{"into":{"db":"archive","coll":"items"}}}])"#,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(result_text(&scope_error).contains("DATABASE_OUT_OF_SCOPE"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_results_preserve_stable_backend_policy_errors() {
|
fn agent_results_preserve_stable_backend_policy_errors() {
|
||||||
let result = agent_result(dbx_core::agent_events::ToolResult {
|
let result = agent_result(dbx_core::agent_events::ToolResult {
|
||||||
|
|||||||
Reference in new issue
Block a user