fix(auth): isolate embedded IAM contexts (#5704)

This commit is contained in:
Zhengchao An
2026-08-04 23:12:35 +08:00
committed by GitHub
parent c26419e357
commit 1695873e55
6 changed files with 161 additions and 22 deletions
+101 -11
View File
@@ -71,22 +71,27 @@ fn hmac(key: &[u8], value: &str) -> Vec<u8> {
fn signed_admin_request(
client: &reqwest::Client,
endpoint: &str,
method: reqwest::Method,
request_path: &str,
access_key: &str,
secret_key: &str,
body: &[u8],
) -> reqwest::RequestBuilder {
let host = endpoint
.strip_prefix("http://")
.or_else(|| endpoint.strip_prefix("https://"))
.expect("embedded endpoint scheme");
let payload_hash = sha256_hex(b"");
let payload_hash = sha256_hex(body);
let now = Utc::now();
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
let date = now.format("%Y%m%d").to_string();
let canonical_headers = format!("host:{host}\nx-amz-content-sha256:{payload_hash}\nx-amz-date:{amz_date}\n");
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
let (path, query) = request_path.split_once('?').unwrap_or((request_path, ""));
let canonical_request = format!("GET\n{path}\n{query}\n{canonical_headers}\n{signed_headers}\n{payload_hash}");
let canonical_request = format!(
"{}\n{path}\n{query}\n{canonical_headers}\n{signed_headers}\n{payload_hash}",
method.as_str()
);
let scope = format!("{date}/us-east-1/s3/aws4_request");
let string_to_sign = format!("AWS4-HMAC-SHA256\n{amz_date}\n{scope}\n{}", sha256_hex(canonical_request.as_bytes()));
let date_key = hmac(format!("AWS4{secret_key}").as_bytes(), &date);
@@ -99,11 +104,12 @@ fn signed_admin_request(
);
client
.get(format!("{endpoint}{request_path}"))
.request(method, format!("{endpoint}{request_path}"))
.header("host", host)
.header("x-amz-content-sha256", payload_hash)
.header("x-amz-date", amz_date)
.header("authorization", authorization)
.body(body.to_vec())
}
// backlog#1052 acceptance: a second embedded server in the same process no
@@ -328,6 +334,81 @@ async fn two_embedded_servers_isolate_auth_and_data_planes_body() {
server_b.shutdown().await;
}
#[cfg(feature = "e2e-test-hooks")]
#[tokio::test]
async fn embedded_servers_isolate_iam_users_and_policies() {
let port_a = find_available_port().expect("find free port for server A");
let server_a = RustFSServerBuilder::new()
.address(format!("127.0.0.1:{port_a}"))
.access_key("iam-root-a")
.secret_key("iam-root-secret-a")
.build()
.await
.expect("start embedded server A");
let port_b = find_available_port().expect("find free port for server B");
let server_b = RustFSServerBuilder::new()
.address(format!("127.0.0.1:{port_b}"))
.access_key("iam-root-b")
.secret_key("iam-root-secret-b")
.build()
.await
.expect("start embedded server B");
let http = reqwest::Client::builder()
.no_proxy()
.build()
.expect("build local admin client");
let user_body = serde_json::json!({"secretKey": "iam-user-secret-a", "status": "enabled"}).to_string();
for (path, body) in [
("/rustfs/admin/v3/add-user?accessKey=iam-user-a", user_body.as_bytes()),
(
"/rustfs/admin/v3/set-user-or-group-policy?policyName=readwrite&userOrGroup=iam-user-a&isGroup=false",
b"".as_slice(),
),
] {
let response = signed_admin_request(
&http,
&server_a.endpoint(),
reqwest::Method::PUT,
path,
server_a.access_key(),
server_a.secret_key(),
body,
)
.send()
.await
.expect("send server A IAM request");
assert!(response.status().is_success(), "server A IAM setup failed: {}", response.status());
}
let client_b = s3_client(&server_b.endpoint(), server_b.access_key(), server_b.secret_key());
client_b
.create_bucket()
.bucket("private-to-b")
.send()
.await
.expect("create server B bucket");
client_b
.put_object()
.bucket("private-to-b")
.key("secret.txt")
.body(ByteStream::from_static(b"server B data"))
.send()
.await
.expect("write server B object");
let cross_instance = s3_client(&server_b.endpoint(), "iam-user-a", "iam-user-secret-a")
.get_object()
.bucket("private-to-b")
.key("secret.txt")
.send()
.await;
assert!(cross_instance.is_err(), "server A IAM user must not authorize against server B");
server_b.shutdown().await;
server_a.shutdown().await;
}
#[cfg(feature = "e2e-test-hooks")]
#[tokio::test]
async fn second_embedded_server_fails_closed_until_its_context_slot_is_installed() {
@@ -392,10 +473,11 @@ async fn second_embedded_server_fails_closed_until_its_context_slot_is_installed
.build()
.expect("build local admin client without proxy");
let inspect_path = "/rustfs/admin/v3/inspect-data?file=marker.txt&volume=startup-window";
let before_install = signed_admin_request(&http, &endpoint_b, inspect_path, b_access_key, b_secret_key)
.send()
.await
.expect("server B HTTP listener must accept the paused request");
let before_install =
signed_admin_request(&http, &endpoint_b, reqwest::Method::GET, inspect_path, b_access_key, b_secret_key, b"")
.send()
.await
.expect("server B HTTP listener must accept the paused request");
let before_install_status = before_install.status();
let before_install_body = before_install.text().await.expect("read paused response body");
assert_eq!(before_install_status, StatusCode::SERVICE_UNAVAILABLE, "{before_install_body}");
@@ -426,10 +508,18 @@ async fn second_embedded_server_fails_closed_until_its_context_slot_is_installed
.await
.expect("server B writes its marker");
let after_install = signed_admin_request(&http, &server_b.endpoint(), inspect_path, b_access_key, b_secret_key)
.send()
.await
.expect("server B admin request after context installation");
let after_install = signed_admin_request(
&http,
&server_b.endpoint(),
reqwest::Method::GET,
inspect_path,
b_access_key,
b_secret_key,
b"",
)
.send()
.await
.expect("server B admin request after context installation");
assert_eq!(after_install.status(), StatusCode::OK);
assert_eq!(after_install.bytes().await.expect("read server B marker"), b"from B".as_slice());