fix(replication): reject unsupported target options (#5622)

This commit is contained in:
cxymds
2026-08-02 19:28:30 +08:00
committed by GitHub
parent 921ddef2c7
commit 885096d1de
2 changed files with 128 additions and 8 deletions
@@ -530,6 +530,47 @@ mod tests {
assert!(redacted_json.contains(r#""session_token":null"#));
}
#[test]
fn historical_bucket_target_options_remain_readable() {
let target: BucketTarget = serde_json::from_value(serde_json::json!({
"endpoint": "legacy.example:9000",
"credentials": {
"accessKey": "legacy-access",
"secretKey": "legacy-secret",
"session_token": "legacy-session-token",
"expiration": "2024-12-31T23:59:59Z"
},
"targetbucket": "legacy-bucket",
"api": "s3v2",
"healthCheckDuration": 30,
"disableProxy": true,
"edge": true,
"edgeSyncBeforeExpiry": true,
"type": "replication"
}))
.expect("historical remote target should remain readable");
assert_eq!(target.api, "s3v2");
assert_eq!(target.health_check_duration, Duration::from_secs(30));
assert!(target.disable_proxy);
assert!(target.edge);
assert!(target.edge_sync_before_expiry);
assert_eq!(
target
.credentials
.as_ref()
.and_then(|credentials| credentials.session_token.as_deref()),
Some("legacy-session-token")
);
assert!(
target
.credentials
.as_ref()
.and_then(|credentials| credentials.expiration)
.is_some()
);
}
#[test]
fn test_bucket_target_type_json_deserialize() {
// Test BucketTargetType JSON deserialization
+87 -8
View File
@@ -50,6 +50,8 @@ use time::OffsetDateTime;
use tracing::{debug, error, info, warn};
use url::Host;
const SUPPORTED_REMOTE_TARGET_API: &str = "s3v4";
fn extract_query_params(uri: &Uri) -> HashMap<String, String> {
let mut params = HashMap::new();
@@ -78,7 +80,7 @@ fn map_bucket_target_error(err: BucketTargetError) -> S3Error {
}
}
#[derive(Debug, Deserialize)]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RemoteTargetCredentialsRequest {
#[serde(rename = "accessKey")]
@@ -100,7 +102,7 @@ impl From<RemoteTargetCredentialsRequest> for TargetCredentials {
}
}
#[derive(Debug, Deserialize)]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RemoteTargetRequest {
#[serde(rename = "sourcebucket", default)]
@@ -179,6 +181,32 @@ impl RemoteTargetRequest {
return Err(s3_error!(InvalidRequest, "credentials.secretKey is required"));
}
if self
.credentials
.session_token
.as_deref()
.is_some_and(|token| !token.trim().is_empty())
{
return Err(s3_error!(
InvalidRequest,
"remote target field credentials.session_token is not supported by this RustFS version"
));
}
if self.credentials.expiration.is_some() {
return Err(s3_error!(
InvalidRequest,
"remote target field credentials.expiration is not supported by this RustFS version"
));
}
if !self.api.is_empty() && self.api != SUPPORTED_REMOTE_TARGET_API {
return Err(s3_error!(
InvalidRequest,
"remote target field api value is not supported by this RustFS version"
));
}
for (unsupported, configured) in [
("disableProxy", self.disable_proxy),
("healthCheckDuration", self.health_check_duration != 0),
@@ -1021,7 +1049,7 @@ impl Operation for ReplicationMrfHandler {
#[cfg(test)]
mod tests {
use super::{
RemoteTargetRequest, build_mrf_response, extract_query_params, unique_replication_peers,
RemoteTargetRequest, SUPPORTED_REMOTE_TARGET_API, build_mrf_response, extract_query_params, unique_replication_peers,
validate_remote_target_tls_settings,
};
use crate::admin::storage_api::bucket::target::BucketTarget;
@@ -1276,8 +1304,10 @@ mod tests {
let mut request = valid_remote_target_request();
request["unexpected"] = serde_json::json!(true);
let err = serde_json::from_value::<RemoteTargetRequest>(request)
.expect_err("remote target request should reject unknown fields");
let err = match serde_json::from_value::<RemoteTargetRequest>(request) {
Ok(_) => panic!("remote target request should reject unknown fields"),
Err(err) => err,
};
assert!(err.to_string().contains("unknown field"));
}
@@ -1290,8 +1320,10 @@ mod tests {
.expect("request should be an object")
.remove("credentials");
let err =
serde_json::from_value::<RemoteTargetRequest>(request).expect_err("remote target request should require credentials");
let err = match serde_json::from_value::<RemoteTargetRequest>(request) {
Ok(_) => panic!("remote target request should require credentials"),
Err(err) => err,
};
assert!(err.to_string().contains("missing field"));
}
@@ -1314,13 +1346,20 @@ mod tests {
#[test]
fn remote_target_request_rejects_unimplemented_fields() {
for (field, value) in [
("credentials.session_token", serde_json::json!("session-token")),
("credentials.expiration", serde_json::json!("2026-01-01T00:00:00Z")),
("api", serde_json::json!("s3v2")),
("disableProxy", serde_json::json!(true)),
("healthCheckDuration", serde_json::json!(5)),
("edge", serde_json::json!(true)),
("edgeSyncBeforeExpiry", serde_json::json!(true)),
] {
let mut request = valid_remote_target_request();
request[field] = value;
if let Some((credential_field, credential_name)) = field.split_once('.') {
request[credential_field][credential_name] = value;
} else {
request[field] = value;
}
let request: RemoteTargetRequest =
serde_json::from_value(request).expect("unsupported field should still deserialize");
let err = request
@@ -1332,6 +1371,46 @@ mod tests {
}
}
#[test]
fn remote_target_request_accepts_static_credentials_and_supported_api() {
let mut request = valid_remote_target_request();
request["api"] = serde_json::json!("s3v4");
request["secure"] = serde_json::json!(true);
request["caCertPem"] = serde_json::json!("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n");
request["credentials"]["session_token"] = serde_json::json!("");
let target = serde_json::from_value::<RemoteTargetRequest>(request)
.expect("supported remote target request should deserialize")
.into_bucket_target()
.expect("static credentials, SigV4 and custom CA should remain supported");
assert_eq!(target.api, SUPPORTED_REMOTE_TARGET_API);
assert_eq!(
target
.credentials
.as_ref()
.and_then(|credentials| credentials.session_token.as_deref()),
Some("")
);
assert!(!target.ca_cert_pem.is_empty());
}
#[test]
fn remote_target_request_validation_does_not_echo_credential_values() {
let mut request = valid_remote_target_request();
request["credentials"]["session_token"] = serde_json::json!("session-token-must-not-leak");
let request: RemoteTargetRequest = serde_json::from_value(request).expect("request should deserialize");
let err = request
.into_bucket_target()
.expect_err("session tokens must be rejected before persistence");
let message = err.to_string();
assert!(message.contains("credentials.session_token"));
assert!(!message.contains("session-token-must-not-leak"));
assert!(!message.contains("secret"));
}
#[test]
fn remote_target_request_converts_to_bucket_target() {
let target = serde_json::from_value::<RemoteTargetRequest>(valid_remote_target_request())