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(())
+    }
 }

Reply via email to