This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git
The following commit(s) were added to refs/heads/main by this push:
new 0976884 fix(aliyun-oss): normalize custom sts endpoints (#726)
0976884 is described below
commit 0976884a738b22110e29fe7bed88caedd5392d50
Author: Xuanwo <[email protected]>
AuthorDate: Thu Mar 19 15:38:08 2026 +0800
fix(aliyun-oss): normalize custom sts endpoints (#726)
This fixes a correctness bug in Aliyun OSS STS endpoint handling: when
`ALIBABA_CLOUD_STS_ENDPOINT` was set to a full URL, the AssumeRole path
prefixed `https://` again and produced a malformed request URI.
The change normalizes custom STS endpoints so both bare hosts and full
URLs work consistently across the AK-based AssumeRole provider and the
OIDC-based STS provider. It also adds regression coverage for the
env-configured full-URL case.
Context: found while reviewing the latest 20 commits on `main`,
specifically the new AssumeRole provider added in #724.
---
.../src/provide_credential/assume_role.rs | 78 +++++++++++++++++++-
.../provide_credential/assume_role_with_oidc.rs | 83 +++++++++++++++++++++-
2 files changed, 155 insertions(+), 6 deletions(-)
diff --git a/services/aliyun-oss/src/provide_credential/assume_role.rs
b/services/aliyun-oss/src/provide_credential/assume_role.rs
index 62391c2..6c51ebe 100644
--- a/services/aliyun-oss/src/provide_credential/assume_role.rs
+++ b/services/aliyun-oss/src/provide_credential/assume_role.rs
@@ -36,6 +36,7 @@ static ALIYUN_RPC_QUERY_ENCODE_SET: AsciiSet =
NON_ALPHANUMERIC
.remove(b'.')
.remove(b'_')
.remove(b'~');
+const DEFAULT_STS_ENDPOINT: &str = "https://sts.aliyuncs.com";
static SIGNATURE_NONCE_COUNTER: AtomicU64 = AtomicU64::new(0);
/// AssumeRoleCredentialProvider loads credentials via Alibaba Cloud STS
AssumeRole.
@@ -175,12 +176,12 @@ impl AssumeRoleCredentialProvider {
fn get_sts_endpoint(&self, envs: &HashMap<String, String>) -> String {
if let Some(endpoint) = &self.sts_endpoint {
- return endpoint.clone();
+ return normalize_sts_endpoint(endpoint);
}
match envs.get(ALIBABA_CLOUD_STS_ENDPOINT) {
- Some(endpoint) => format!("https://{endpoint}"),
- None => "https://sts.aliyuncs.com".to_string(),
+ Some(endpoint) => normalize_sts_endpoint(endpoint),
+ None => DEFAULT_STS_ENDPOINT.to_string(),
}
}
@@ -308,6 +309,15 @@ fn default_base_provider_chain() ->
ProvideCredentialChain<Credential> {
.push(ConfigFileCredentialProvider::new())
}
+fn normalize_sts_endpoint(endpoint: &str) -> String {
+ let endpoint = endpoint.trim().trim_end_matches('/');
+ if endpoint.starts_with("https://") || endpoint.starts_with("http://") {
+ endpoint.to_string()
+ } else {
+ format!("https://{endpoint}")
+ }
+}
+
fn canonicalized_query_string(params: &BTreeMap<String, String>) -> String {
params
.iter()
@@ -453,6 +463,22 @@ mod tests {
Ok(())
}
+ #[test]
+ fn test_normalize_sts_endpoint_accepts_bare_host_and_full_url() {
+ assert_eq!(
+ "https://sts.aliyuncs.com",
+ normalize_sts_endpoint("sts.aliyuncs.com")
+ );
+ assert_eq!(
+ "https://sts.example.com",
+ normalize_sts_endpoint("https://sts.example.com/")
+ );
+ assert_eq!(
+ "http://sts.example.com",
+ normalize_sts_endpoint("http://sts.example.com/")
+ );
+ }
+
#[tokio::test]
async fn test_assume_role_loader_without_config() {
let ctx = Context::new().with_env(StaticEnv {
@@ -637,4 +663,50 @@ mod tests {
Ok(())
}
+
+ #[tokio::test]
+ async fn test_assume_role_accepts_full_url_sts_endpoint_from_env() ->
Result<()> {
+ let http_send = CaptureHttpSend::new(vec![
+
br#"{"Credentials":{"SecurityToken":"sts-token","Expiration":"2124-05-25T11:45:17Z","AccessKeySecret":"sts-secret","AccessKeyId":"sts-ak"}}"#
+ .to_vec(),
+ ]);
+ let ctx = Context::new()
+ .with_http_send(http_send.clone())
+ .with_env(StaticEnv {
+ home_dir: None,
+ envs: HashMap::from([
+ (
+ ALIBABA_CLOUD_ROLE_ARN.to_string(),
+ "acs:ram::123456789012:role/test-role".to_string(),
+ ),
+ (
+ ALIBABA_CLOUD_STS_ENDPOINT.to_string(),
+ "https://sts.example.com".to_string(),
+ ),
+ ]),
+ });
+
+ let provider = AssumeRoleCredentialProvider::new()
+
.with_base_provider(TestBaseCredentialProvider::new(Some(Credential {
+ access_key_id: "base-ak".to_string(),
+ access_key_secret: "base-sk".to_string(),
+ security_token: None,
+ expires_in: None,
+ })))
+ .with_role_session_name("test-session")
+ .with_time("2024-03-05T06:07:08Z".parse().unwrap())
+ .with_signature_nonce("test-nonce");
+
+ let _ = provider
+ .provide_credential(&ctx)
+ .await?
+ .expect("credential must be loaded");
+
+ let recorded_uri = http_send.uri().expect("request uri must be
captured");
+ let uri: http::Uri = recorded_uri.parse().expect("uri must parse");
+ assert_eq!(Some("sts.example.com"), uri.authority().map(|v|
v.as_str()));
+ assert_eq!("/", uri.path());
+
+ Ok(())
+ }
}
diff --git
a/services/aliyun-oss/src/provide_credential/assume_role_with_oidc.rs
b/services/aliyun-oss/src/provide_credential/assume_role_with_oidc.rs
index 29a6b43..9c82378 100644
--- a/services/aliyun-oss/src/provide_credential/assume_role_with_oidc.rs
+++ b/services/aliyun-oss/src/provide_credential/assume_role_with_oidc.rs
@@ -22,6 +22,8 @@ use reqsign_core::time::Timestamp;
use reqsign_core::{Context, ProvideCredential};
use serde::Deserialize;
+const DEFAULT_STS_ENDPOINT: &str = "https://sts.aliyuncs.com";
+
/// AssumeRoleWithOidcCredentialProvider loads credential via assume role with
OIDC.
///
/// This provider reads configuration from environment variables at runtime:
@@ -59,12 +61,12 @@ impl AssumeRoleWithOidcCredentialProvider {
fn get_sts_endpoint(&self, envs: &std::collections::HashMap<String,
String>) -> String {
if let Some(endpoint) = &self.sts_endpoint {
- return endpoint.clone();
+ return normalize_sts_endpoint(endpoint);
}
match envs.get(ALIBABA_CLOUD_STS_ENDPOINT) {
- Some(endpoint) => format!("https://{endpoint}"),
- None => "https://sts.aliyuncs.com".to_string(),
+ Some(endpoint) => normalize_sts_endpoint(endpoint),
+ None => DEFAULT_STS_ENDPOINT.to_string(),
}
}
@@ -146,6 +148,15 @@ impl ProvideCredential for
AssumeRoleWithOidcCredentialProvider {
}
}
+fn normalize_sts_endpoint(endpoint: &str) -> String {
+ let endpoint = endpoint.trim().trim_end_matches('/');
+ if endpoint.starts_with("https://") || endpoint.starts_with("http://") {
+ endpoint.to_string()
+ } else {
+ format!("https://{endpoint}")
+ }
+}
+
#[derive(Default, Debug, Deserialize)]
#[serde(default)]
struct AssumeRoleWithOidcResponse {
@@ -214,6 +225,22 @@ mod tests {
Ok(())
}
+ #[test]
+ fn test_normalize_sts_endpoint_accepts_bare_host_and_full_url() {
+ assert_eq!(
+ "https://sts.aliyuncs.com",
+ normalize_sts_endpoint("sts.aliyuncs.com")
+ );
+ assert_eq!(
+ "https://sts.example.com",
+ normalize_sts_endpoint("https://sts.example.com/")
+ );
+ assert_eq!(
+ "http://sts.example.com",
+ normalize_sts_endpoint("http://sts.example.com/")
+ );
+ }
+
#[tokio::test]
async fn test_assume_role_with_oidc_loader_without_config() {
let ctx = Context::new()
@@ -404,4 +431,54 @@ mod tests {
Ok(())
}
+
+ #[tokio::test]
+ async fn
test_assume_role_with_oidc_accepts_full_url_sts_endpoint_from_env() ->
Result<()> {
+ let token_path = "/mock/token";
+ let file_read = TestFileRead {
+ expected_path: token_path.to_string(),
+ content: b"token".to_vec(),
+ };
+ let http_body =
r#"{"Credentials":{"SecurityToken":"security_token","Expiration":"2124-05-25T11:45:17Z","AccessKeySecret":"secret_access_key","AccessKeyId":"access_key_id"}}"#;
+ let http_send = CaptureHttpSend::new(http_body);
+
+ let ctx = Context::new()
+ .with_file_read(file_read)
+ .with_http_send(http_send.clone())
+ .with_env(StaticEnv {
+ home_dir: None,
+ envs: HashMap::from_iter([
+ (
+ ALIBABA_CLOUD_OIDC_TOKEN_FILE.to_string(),
+ token_path.to_string(),
+ ),
+ (
+ ALIBABA_CLOUD_ROLE_ARN.to_string(),
+ "acs:ram::123456789012:role/test-role".to_string(),
+ ),
+ (
+ ALIBABA_CLOUD_OIDC_PROVIDER_ARN.to_string(),
+
"acs:ram::123456789012:oidc-provider/test-provider".to_string(),
+ ),
+ (
+ ALIBABA_CLOUD_STS_ENDPOINT.to_string(),
+ "https://sts.example.com".to_string(),
+ ),
+ ]),
+ });
+
+ let _ = AssumeRoleWithOidcCredentialProvider::new()
+ .provide_credential(&ctx)
+ .await?
+ .expect("credential must be loaded");
+
+ let recorded_uri = http_send
+ .uri()
+ .expect("http_send must capture outgoing uri");
+ let uri: http::Uri = recorded_uri.parse().expect("uri must parse");
+ assert_eq!(Some("sts.example.com"), uri.authority().map(|v|
v.as_str()));
+ assert_eq!("/", uri.path());
+
+ Ok(())
+ }
}