mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-02 11:29:17 +00:00
fix(replication): reject unsupported target options (#5622)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user