diff --git a/rustfs/src/admin/handlers/replication.rs b/rustfs/src/admin/handlers/replication.rs index c98648ed3..9f6367a5d 100644 --- a/rustfs/src/admin/handlers/replication.rs +++ b/rustfs/src/admin/handlers/replication.rs @@ -14,6 +14,7 @@ use crate::admin::auth::validate_admin_request; use crate::admin::router::{AdminOperation, Operation, S3Router}; +use crate::admin::utils::read_compatible_admin_body; use crate::auth::{check_key_valid, get_session_token}; use crate::error::ApiError; use crate::server::{ADMIN_PREFIX, RemoteAddr}; @@ -21,6 +22,7 @@ use http::{HeaderMap, HeaderValue, Uri}; use hyper::{Method, StatusCode}; use matchit::Params; use rustfs_config::MAX_ADMIN_REQUEST_BODY_SIZE; +use rustfs_credentials::Credentials; use rustfs_ecstore::bucket::bucket_target_sys::BucketTargetSys; use rustfs_ecstore::bucket::metadata::BUCKET_TARGETS_FILE; use rustfs_ecstore::bucket::metadata_sys; @@ -79,7 +81,7 @@ pub fn register_replication_route(r: &mut S3Router) -> std::io:: Ok(()) } -async fn validate_replication_admin_request(req: &S3Request, action: AdminAction) -> S3Result<()> { +async fn validate_replication_admin_request(req: &S3Request, action: AdminAction) -> S3Result { let Some(input_cred) = req.credentials.as_ref() else { return Err(s3_error!(InvalidRequest, "get cred failed")); }; @@ -88,7 +90,9 @@ async fn validate_replication_admin_request(req: &S3Request, action: Admin check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?; let remote_addr = req.extensions.get::>().and_then(|opt| opt.map(|a| a.0)); - validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await + validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await?; + + Ok(cred) } #[allow(dead_code)] @@ -156,7 +160,7 @@ pub struct SetRemoteTargetHandler {} #[async_trait::async_trait] impl Operation for SetRemoteTargetHandler { async fn call(&self, req: S3Request, _params: Params<'_, '_>) -> S3Result> { - validate_replication_admin_request(&req, AdminAction::SetBucketTargetAction).await?; + let cred = validate_replication_admin_request(&req, AdminAction::SetBucketTargetAction).await?; let queries = extract_query_params(&req.uri); @@ -181,14 +185,14 @@ impl Operation for SetRemoteTargetHandler { .await .map_err(ApiError::from)?; - let mut input = req.input; - let body = match input.store_all_limited(MAX_ADMIN_REQUEST_BODY_SIZE).await { - Ok(b) => b, - Err(e) => { - warn!("get body failed, e: {:?}", e); - return Err(s3_error!(InvalidRequest, "remote target configuration body too large or failed to read")); - } - }; + let body = + match read_compatible_admin_body(req.input, MAX_ADMIN_REQUEST_BODY_SIZE, req.uri.path(), &cred.secret_key).await { + Ok(body) => body, + Err(e) => { + warn!("get body failed, e: {:?}", e); + return Err(e); + } + }; let mut remote_target: BucketTarget = serde_json::from_slice(&body).map_err(|e| { error!("Failed to parse BucketTarget from body: {}", e); @@ -221,6 +225,8 @@ impl Operation for SetRemoteTargetHandler { let arn_str = serde_json::to_string(&arn).unwrap_or_default(); warn!("return exists, arn: {}", arn_str); + // MinIO-compatible clients encrypt the request payload for this endpoint, + // but they parse the success response directly as plain JSON string ARN. return Ok(S3Response::new((StatusCode::OK, Body::from(arn_str)))); } } @@ -276,6 +282,8 @@ impl Operation for SetRemoteTargetHandler { let arn_str = serde_json::to_string(&arn).unwrap_or_default(); + // MinIO-compatible clients encrypt the request payload for this endpoint, + // but they parse the success response directly as plain JSON string ARN. Ok(S3Response::new((StatusCode::OK, Body::from(arn_str)))) } } diff --git a/rustfs/src/admin/route_registration_test.rs b/rustfs/src/admin/route_registration_test.rs index f2be9d6a3..fe904eede 100644 --- a/rustfs/src/admin/route_registration_test.rs +++ b/rustfs/src/admin/route_registration_test.rs @@ -175,3 +175,39 @@ fn test_phase5_admin_info_contract() { "admin server info path must be served through DefaultAdminUsecase::execute_query_server_info" ); } + +fn extract_block_between_markers<'a>(src: &'a str, start_marker: &str, end_marker: &str) -> &'a str { + let start = src + .find(start_marker) + .unwrap_or_else(|| panic!("Expected marker `{}` in source", start_marker)); + let after_start = &src[start..]; + let end = after_start + .find(end_marker) + .unwrap_or_else(|| panic!("Expected end marker `{}` in source", end_marker)); + &after_start[..end] +} + +#[test] +fn test_replication_set_remote_target_compat_contract() { + let replication_src = include_str!("handlers/replication.rs"); + let handler_block = extract_block_between_markers( + replication_src, + "impl Operation for SetRemoteTargetHandler", + "pub struct ListRemoteTargetHandler", + ); + + assert!( + handler_block.contains("read_compatible_admin_body("), + "set-remote-target must decode MinIO-compatible encrypted admin payloads" + ); + + assert!( + handler_block.contains("Body::from(arn_str)"), + "set-remote-target must keep ARN success responses as plain JSON string body" + ); + + assert!( + !handler_block.contains("encode_compatible_admin_payload("), + "set-remote-target should not re-encrypt ARN success responses" + ); +}