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 3918127 feat(oss): Add role session name suppport (#678)
3918127 is described below
commit 3918127630604e20bc14c2f8b03a10588a2c37df
Author: Xuanwo <[email protected]>
AuthorDate: Mon Jan 19 23:00:38 2026 +0800
feat(oss): Add role session name suppport (#678)
This PR will add role session name suppport for oss.
---
**Parts of this PR were drafted with assistance from Codex (with
`gpt-5.2`) and fully reviewed and edited by me. I take full
responsibility for all changes.**
---
.github/workflows/ci.yml | 14 +-
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 | 230 +++++++++++++++++++--
6 files changed, 240 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 07bced0..2f728e9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -96,7 +96,19 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli --version 0.2.106 --locked
+ run: |
+ WASM_BINDGEN_VERSION=$(
+ cargo metadata --format-version 1 --filter-platform
wasm32-unknown-unknown \
+ | jq -r '.packages[] | select(.name=="wasm-bindgen") | .version'
\
+ | sort -V \
+ | tail -n 1
+ )
+ if [ -z "${WASM_BINDGEN_VERSION}" ]; then
+ echo "failed to resolve wasm-bindgen version"
+ exit 1
+ fi
+ echo "wasm-bindgen version: ${WASM_BINDGEN_VERSION}"
+ cargo install wasm-bindgen-cli --version "${WASM_BINDGEN_VERSION}"
--locked
- name: Run wasm tests
env:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER: wasm-bindgen-test-runner
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..d340ed3 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,20 +97,22 @@ 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 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
- );
+ 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!("{}/?{query}", self.get_sts_endpoint(&envs));
let req = http::Request::builder()
.method(http::Method::GET)
@@ -145,10 +168,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 +233,183 @@ 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(())
+ }
}