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