mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 00:26:53 +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"#));
|
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]
|
#[test]
|
||||||
fn test_bucket_target_type_json_deserialize() {
|
fn test_bucket_target_type_json_deserialize() {
|
||||||
// Test BucketTargetType JSON deserialization
|
// Test BucketTargetType JSON deserialization
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ use time::OffsetDateTime;
|
|||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use url::Host;
|
use url::Host;
|
||||||
|
|
||||||
|
const SUPPORTED_REMOTE_TARGET_API: &str = "s3v4";
|
||||||
|
|
||||||
fn extract_query_params(uri: &Uri) -> HashMap<String, String> {
|
fn extract_query_params(uri: &Uri) -> HashMap<String, String> {
|
||||||
let mut params = HashMap::new();
|
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)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RemoteTargetCredentialsRequest {
|
struct RemoteTargetCredentialsRequest {
|
||||||
#[serde(rename = "accessKey")]
|
#[serde(rename = "accessKey")]
|
||||||
@@ -100,7 +102,7 @@ impl From<RemoteTargetCredentialsRequest> for TargetCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RemoteTargetRequest {
|
struct RemoteTargetRequest {
|
||||||
#[serde(rename = "sourcebucket", default)]
|
#[serde(rename = "sourcebucket", default)]
|
||||||
@@ -179,6 +181,32 @@ impl RemoteTargetRequest {
|
|||||||
return Err(s3_error!(InvalidRequest, "credentials.secretKey is required"));
|
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 [
|
for (unsupported, configured) in [
|
||||||
("disableProxy", self.disable_proxy),
|
("disableProxy", self.disable_proxy),
|
||||||
("healthCheckDuration", self.health_check_duration != 0),
|
("healthCheckDuration", self.health_check_duration != 0),
|
||||||
@@ -1021,7 +1049,7 @@ impl Operation for ReplicationMrfHandler {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
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,
|
validate_remote_target_tls_settings,
|
||||||
};
|
};
|
||||||
use crate::admin::storage_api::bucket::target::BucketTarget;
|
use crate::admin::storage_api::bucket::target::BucketTarget;
|
||||||
@@ -1276,8 +1304,10 @@ mod tests {
|
|||||||
let mut request = valid_remote_target_request();
|
let mut request = valid_remote_target_request();
|
||||||
request["unexpected"] = serde_json::json!(true);
|
request["unexpected"] = serde_json::json!(true);
|
||||||
|
|
||||||
let err = serde_json::from_value::<RemoteTargetRequest>(request)
|
let err = match serde_json::from_value::<RemoteTargetRequest>(request) {
|
||||||
.expect_err("remote target request should reject unknown fields");
|
Ok(_) => panic!("remote target request should reject unknown fields"),
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
|
||||||
assert!(err.to_string().contains("unknown field"));
|
assert!(err.to_string().contains("unknown field"));
|
||||||
}
|
}
|
||||||
@@ -1290,8 +1320,10 @@ mod tests {
|
|||||||
.expect("request should be an object")
|
.expect("request should be an object")
|
||||||
.remove("credentials");
|
.remove("credentials");
|
||||||
|
|
||||||
let err =
|
let err = match serde_json::from_value::<RemoteTargetRequest>(request) {
|
||||||
serde_json::from_value::<RemoteTargetRequest>(request).expect_err("remote target request should require credentials");
|
Ok(_) => panic!("remote target request should require credentials"),
|
||||||
|
Err(err) => err,
|
||||||
|
};
|
||||||
|
|
||||||
assert!(err.to_string().contains("missing field"));
|
assert!(err.to_string().contains("missing field"));
|
||||||
}
|
}
|
||||||
@@ -1314,13 +1346,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn remote_target_request_rejects_unimplemented_fields() {
|
fn remote_target_request_rejects_unimplemented_fields() {
|
||||||
for (field, value) in [
|
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)),
|
("disableProxy", serde_json::json!(true)),
|
||||||
("healthCheckDuration", serde_json::json!(5)),
|
("healthCheckDuration", serde_json::json!(5)),
|
||||||
("edge", serde_json::json!(true)),
|
("edge", serde_json::json!(true)),
|
||||||
("edgeSyncBeforeExpiry", serde_json::json!(true)),
|
("edgeSyncBeforeExpiry", serde_json::json!(true)),
|
||||||
] {
|
] {
|
||||||
let mut request = valid_remote_target_request();
|
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 =
|
let request: RemoteTargetRequest =
|
||||||
serde_json::from_value(request).expect("unsupported field should still deserialize");
|
serde_json::from_value(request).expect("unsupported field should still deserialize");
|
||||||
let err = request
|
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]
|
#[test]
|
||||||
fn remote_target_request_converts_to_bucket_target() {
|
fn remote_target_request_converts_to_bucket_target() {
|
||||||
let target = serde_json::from_value::<RemoteTargetRequest>(valid_remote_target_request())
|
let target = serde_json::from_value::<RemoteTargetRequest>(valid_remote_target_request())
|
||||||
|
|||||||
Reference in New Issue
Block a user