fix(admin): decode compat payload in set-remote-target (#2216)

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: 马登山 <Cxymds@qq.com>
Co-authored-by: 安正超 <anzhengchao@gmail.com>
Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
LeonWang0735
2026-03-23 18:22:07 +08:00
committed by GitHub
parent 05dc131a49
commit 990acbcd4b
2 changed files with 55 additions and 11 deletions
+19 -11
View File
@@ -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<AdminOperation>) -> std::io::
Ok(())
}
async fn validate_replication_admin_request(req: &S3Request<Body>, action: AdminAction) -> S3Result<()> {
async fn validate_replication_admin_request(req: &S3Request<Body>, action: AdminAction) -> S3Result<Credentials> {
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<Body>, 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::<Option<RemoteAddr>>().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<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
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))))
}
}
@@ -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"
);
}