This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch luban/zone-issue
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git

commit 98e47fa788aae970cdff650b336ccd9ab395481e
Author: Xuanwo <[email protected]>
AuthorDate: Mon Jan 19 22:24:47 2026 +0800

    feat(oss): Add role session name suppport
---
 services/aliyun-oss/Cargo.toml                     |   2 +
 services/aliyun-oss/README.md                      |   4 +-
 services/aliyun-oss/src/constants.rs               |   1 +
 services/aliyun-oss/src/lib.rs                     |   4 +-
 .../provide_credential/assume_role_with_oidc.rs    | 227 ++++++++++++++++++++-
 5 files changed, 226 insertions(+), 12 deletions(-)

diff --git a/services/aliyun-oss/Cargo.toml b/services/aliyun-oss/Cargo.toml
index 4d57610..c75bd2a 100644
--- a/services/aliyun-oss/Cargo.toml
+++ b/services/aliyun-oss/Cargo.toml
@@ -29,6 +29,7 @@ rust-version.workspace = true
 [dependencies]
 anyhow = { workspace = true }
 async-trait = { workspace = true }
+form_urlencoded = { workspace = true }
 http = { workspace = true }
 log = { workspace = true }
 percent-encoding = { workspace = true }
@@ -37,6 +38,7 @@ serde = { workspace = true }
 serde_json = { workspace = true }
 
 [dev-dependencies]
+bytes = { workspace = true }
 dotenv = { workspace = true }
 env_logger = { workspace = true }
 reqsign-file-read-tokio = { workspace = true }
diff --git a/services/aliyun-oss/README.md b/services/aliyun-oss/README.md
index b12e02e..b56f9ce 100644
--- a/services/aliyun-oss/README.md
+++ b/services/aliyun-oss/README.md
@@ -88,6 +88,8 @@ let config = Config::default()
 let loader = AssumeRoleWithOidcLoader::new(config);
 ```
 
+The session name defaults to `reqsign`. To customize it, set 
`ALIBABA_CLOUD_ROLE_SESSION_NAME` or use 
`AssumeRoleWithOidcCredentialProvider::with_role_session_name`.
+
 ## OSS Operations
 
 ### Object Operations
@@ -217,4 +219,4 @@ let loader = ConfigLoader::new(config);
 
 ## License
 
-Licensed under [Apache License, Version 2.0](./LICENSE).
\ No newline at end of file
+Licensed under [Apache License, Version 2.0](./LICENSE).
diff --git a/services/aliyun-oss/src/constants.rs 
b/services/aliyun-oss/src/constants.rs
index cc13dda..c34501e 100644
--- a/services/aliyun-oss/src/constants.rs
+++ b/services/aliyun-oss/src/constants.rs
@@ -20,6 +20,7 @@ pub const ALIBABA_CLOUD_ACCESS_KEY_ID: &str = 
"ALIBABA_CLOUD_ACCESS_KEY_ID";
 pub const ALIBABA_CLOUD_ACCESS_KEY_SECRET: &str = 
"ALIBABA_CLOUD_ACCESS_KEY_SECRET";
 pub const ALIBABA_CLOUD_SECURITY_TOKEN: &str = "ALIBABA_CLOUD_SECURITY_TOKEN";
 pub const ALIBABA_CLOUD_ROLE_ARN: &str = "ALIBABA_CLOUD_ROLE_ARN";
+pub const ALIBABA_CLOUD_ROLE_SESSION_NAME: &str = 
"ALIBABA_CLOUD_ROLE_SESSION_NAME";
 pub const ALIBABA_CLOUD_OIDC_PROVIDER_ARN: &str = 
"ALIBABA_CLOUD_OIDC_PROVIDER_ARN";
 pub const ALIBABA_CLOUD_OIDC_TOKEN_FILE: &str = 
"ALIBABA_CLOUD_OIDC_TOKEN_FILE";
 pub const ALIBABA_CLOUD_STS_ENDPOINT: &str = "ALIBABA_CLOUD_STS_ENDPOINT";
diff --git a/services/aliyun-oss/src/lib.rs b/services/aliyun-oss/src/lib.rs
index 205a409..295687a 100644
--- a/services/aliyun-oss/src/lib.rs
+++ b/services/aliyun-oss/src/lib.rs
@@ -135,7 +135,9 @@
 //!
 //! // Use environment variables
 //! // Set ALIBABA_CLOUD_ROLE_ARN, ALIBABA_CLOUD_OIDC_PROVIDER_ARN, 
ALIBABA_CLOUD_OIDC_TOKEN_FILE
-//! let loader = AssumeRoleWithOidcCredentialProvider::new();
+//! // Optionally set ALIBABA_CLOUD_ROLE_SESSION_NAME
+//! let loader = AssumeRoleWithOidcCredentialProvider::new()
+//!     .with_role_session_name("my-session");
 //! ```
 //!
 //! ### Custom Endpoints
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 cedb3a3..9cfe5a6 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
@@ -17,6 +17,7 @@
 
 use crate::{Credential, constants::*};
 use async_trait::async_trait;
+use form_urlencoded::Serializer;
 use reqsign_core::Result;
 use reqsign_core::time::Timestamp;
 use reqsign_core::{Context, ProvideCredential};
@@ -26,12 +27,14 @@ use serde::Deserialize;
 ///
 /// This provider reads configuration from environment variables at runtime:
 /// - `ALIBABA_CLOUD_ROLE_ARN`: The ARN of the role to assume
+/// - `ALIBABA_CLOUD_ROLE_SESSION_NAME`: Optional role session name
 /// - `ALIBABA_CLOUD_OIDC_PROVIDER_ARN`: The ARN of the OIDC provider
 /// - `ALIBABA_CLOUD_OIDC_TOKEN_FILE`: Path to the OIDC token file
 /// - `ALIBABA_CLOUD_STS_ENDPOINT`: Optional custom STS endpoint
 #[derive(Debug, Default, Clone)]
 pub struct AssumeRoleWithOidcCredentialProvider {
     sts_endpoint: Option<String>,
+    role_session_name: Option<String>,
 }
 
 impl AssumeRoleWithOidcCredentialProvider {
@@ -47,6 +50,14 @@ impl AssumeRoleWithOidcCredentialProvider {
         self
     }
 
+    /// Set the role session name.
+    ///
+    /// This setting takes precedence over `ALIBABA_CLOUD_ROLE_SESSION_NAME`.
+    pub fn with_role_session_name(mut self, name: impl Into<String>) -> Self {
+        self.role_session_name = Some(name.into());
+        self
+    }
+
     fn get_sts_endpoint(&self, envs: &std::collections::HashMap<String, 
String>) -> String {
         if let Some(endpoint) = &self.sts_endpoint {
             return endpoint.clone();
@@ -57,6 +68,16 @@ impl AssumeRoleWithOidcCredentialProvider {
             None => "https://sts.aliyuncs.com".to_string(),
         }
     }
+
+    fn get_role_session_name(&self, envs: &std::collections::HashMap<String, 
String>) -> String {
+        if let Some(name) = &self.role_session_name {
+            return name.clone();
+        }
+
+        envs.get(ALIBABA_CLOUD_ROLE_SESSION_NAME)
+            .cloned()
+            .unwrap_or_else(|| "reqsign".to_string())
+    }
 }
 
 #[async_trait]
@@ -76,19 +97,24 @@ impl ProvideCredential for 
AssumeRoleWithOidcCredentialProvider {
             _ => return Ok(None),
         };
 
-        let token = ctx.file_read(token_file).await?;
-        let token = String::from_utf8(token)?;
-        let role_session_name = "reqsign"; // Default session name
+        let token = ctx.file_read_as_string(token_file).await?;
+        let token = token.trim();
+        let role_session_name = self.get_role_session_name(&envs);
 
         // Construct request to Aliyun STS Service.
+        let query = Serializer::new(String::new())
+            .append_pair("Action", "AssumeRoleWithOIDC")
+            .append_pair("OIDCProviderArn", provider_arn)
+            .append_pair("RoleArn", role_arn)
+            .append_pair("RoleSessionName", &role_session_name)
+            .append_pair("Format", "JSON")
+            .append_pair("Version", "2015-04-01")
+            .append_pair("Timestamp", &Timestamp::now().format_rfc3339_zulu())
+            .append_pair("OIDCToken", token)
+            .finish();
         let url = format!(
-            
"{}/?Action=AssumeRoleWithOIDC&OIDCProviderArn={}&RoleArn={}&RoleSessionName={}&Format=JSON&Version=2015-04-01&Timestamp={}&OIDCToken={}",
-            self.get_sts_endpoint(&envs),
-            provider_arn,
-            role_arn,
-            role_session_name,
-            Timestamp::now().format_rfc3339_zulu(),
-            token
+            "{}/?{query}",
+            self.get_sts_endpoint(&envs)
         );
 
         let req = http::Request::builder()
@@ -145,10 +171,14 @@ struct AssumeRoleWithOidcCredentials {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use async_trait::async_trait;
+    use bytes::Bytes;
     use reqsign_core::StaticEnv;
+    use reqsign_core::{Context, FileRead, HttpSend};
     use reqsign_file_read_tokio::TokioFileRead;
     use reqsign_http_send_reqwest::ReqwestHttpSend;
     use std::collections::HashMap;
+    use std::sync::{Arc, Mutex};
 
     #[test]
     fn test_parse_assume_role_with_oidc_response() -> Result<()> {
@@ -206,4 +236,181 @@ mod tests {
 
         assert!(credential.is_none());
     }
+
+    #[derive(Debug)]
+    struct TestFileRead {
+        expected_path: String,
+        content: Vec<u8>,
+    }
+
+    #[async_trait]
+    impl FileRead for TestFileRead {
+        async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
+            assert_eq!(path, self.expected_path);
+            Ok(self.content.clone())
+        }
+    }
+
+    #[derive(Clone, Debug)]
+    struct CaptureHttpSend {
+        uri: Arc<Mutex<Option<String>>>,
+        body: String,
+    }
+
+    impl CaptureHttpSend {
+        fn new(body: impl Into<String>) -> Self {
+            Self {
+                uri: Arc::new(Mutex::new(None)),
+                body: body.into(),
+            }
+        }
+
+        fn uri(&self) -> Option<String> {
+            self.uri.lock().unwrap().clone()
+        }
+    }
+
+    #[async_trait]
+    impl HttpSend for CaptureHttpSend {
+        async fn http_send(&self, req: http::Request<Bytes>) -> 
Result<http::Response<Bytes>> {
+            *self.uri.lock().unwrap() = Some(req.uri().to_string());
+            let resp = http::Response::builder()
+                .status(http::StatusCode::OK)
+                .body(Bytes::from(self.body.clone()))
+                .expect("response must build");
+            Ok(resp)
+        }
+    }
+
+    #[tokio::test]
+    async fn test_assume_role_with_oidc_supports_role_session_name() -> 
Result<()> {
+        let _ = env_logger::builder().is_test(true).try_init();
+
+        let token_path = "/mock/token";
+        let raw_token = "header.payload.signature\n";
+
+        let file_read = TestFileRead {
+            expected_path: token_path.to_string(),
+            content: raw_token.as_bytes().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_ROLE_SESSION_NAME.to_string(),
+                        "my-session".to_string(),
+                    ),
+                ]),
+            });
+
+        let provider = AssumeRoleWithOidcCredentialProvider::new();
+        let cred = provider
+            .provide_credential(&ctx)
+            .await?
+            .expect("credential must be loaded");
+
+        assert_eq!(cred.access_key_id, "access_key_id");
+        assert_eq!(cred.access_key_secret, "secret_access_key");
+        assert_eq!(cred.security_token.as_deref(), Some("security_token"));
+
+        let recorded_uri = http_send
+            .uri()
+            .expect("http_send must capture outgoing uri");
+        let uri: http::Uri = recorded_uri.parse().expect("uri must parse");
+        let query = uri.query().expect("query must exist");
+        let params: HashMap<String, String> =
+            form_urlencoded::parse(query.as_bytes()).into_owned().collect();
+
+        assert_eq!(
+            params.get("RoleSessionName").map(String::as_str),
+            Some("my-session")
+        );
+        assert_eq!(
+            params.get("OIDCToken").map(String::as_str),
+            Some("header.payload.signature")
+        );
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    async fn test_assume_role_with_oidc_role_session_name_overrides_env() -> 
Result<()> {
+        let _ = env_logger::builder().is_test(true).try_init();
+
+        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_ROLE_SESSION_NAME.to_string(),
+                        "env-session".to_string(),
+                    ),
+                ]),
+            });
+
+        let provider = AssumeRoleWithOidcCredentialProvider::new()
+            .with_role_session_name("override-session");
+        let _ = provider
+            .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");
+        let query = uri.query().expect("query must exist");
+        let params: HashMap<String, String> =
+            form_urlencoded::parse(query.as_bytes()).into_owned().collect();
+
+        assert_eq!(
+            params.get("RoleSessionName").map(String::as_str),
+            Some("override-session")
+        );
+
+        Ok(())
+    }
 }

Reply via email to