reqsign_aliyun_oss/provide_credential/
assume_role_with_oidc.rs1use crate::{Credential, constants::*};
19use async_trait::async_trait;
20use reqsign_core::Result;
21use reqsign_core::time::Timestamp;
22use reqsign_core::{Context, ProvideCredential};
23use serde::Deserialize;
24
25#[derive(Debug, Default, Clone)]
33pub struct AssumeRoleWithOidcCredentialProvider {
34 sts_endpoint: Option<String>,
35}
36
37impl AssumeRoleWithOidcCredentialProvider {
38 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn with_sts_endpoint(mut self, endpoint: impl Into<String>) -> Self {
46 self.sts_endpoint = Some(endpoint.into());
47 self
48 }
49
50 fn get_sts_endpoint(&self, envs: &std::collections::HashMap<String, String>) -> String {
51 if let Some(endpoint) = &self.sts_endpoint {
52 return endpoint.clone();
53 }
54
55 match envs.get(ALIBABA_CLOUD_STS_ENDPOINT) {
56 Some(endpoint) => format!("https://{endpoint}"),
57 None => "https://sts.aliyuncs.com".to_string(),
58 }
59 }
60}
61
62#[async_trait]
63impl ProvideCredential for AssumeRoleWithOidcCredentialProvider {
64 type Credential = Credential;
65
66 async fn provide_credential(&self, ctx: &Context) -> Result<Option<Self::Credential>> {
67 let envs = ctx.env_vars();
68
69 let token_file = envs.get(ALIBABA_CLOUD_OIDC_TOKEN_FILE);
71 let role_arn = envs.get(ALIBABA_CLOUD_ROLE_ARN);
72 let provider_arn = envs.get(ALIBABA_CLOUD_OIDC_PROVIDER_ARN);
73
74 let (token_file, role_arn, provider_arn) = match (token_file, role_arn, provider_arn) {
75 (Some(tf), Some(ra), Some(pa)) => (tf, ra, pa),
76 _ => return Ok(None),
77 };
78
79 let token = ctx.file_read(token_file).await?;
80 let token = String::from_utf8(token)?;
81 let role_session_name = "reqsign"; let url = format!(
85 "{}/?Action=AssumeRoleWithOIDC&OIDCProviderArn={}&RoleArn={}&RoleSessionName={}&Format=JSON&Version=2015-04-01&Timestamp={}&OIDCToken={}",
86 self.get_sts_endpoint(&envs),
87 provider_arn,
88 role_arn,
89 role_session_name,
90 Timestamp::now().format_rfc3339_zulu(),
91 token
92 );
93
94 let req = http::Request::builder()
95 .method(http::Method::GET)
96 .uri(&url)
97 .header(
98 http::header::CONTENT_TYPE,
99 "application/x-www-form-urlencoded",
100 )
101 .body(Vec::new())?;
102
103 let resp = ctx.http_send(req.map(|body| body.into())).await?;
104
105 if resp.status() != http::StatusCode::OK {
106 let content = String::from_utf8_lossy(resp.body());
107 return Err(reqsign_core::Error::unexpected(format!(
108 "request to Aliyun STS Services failed: {content}"
109 )));
110 }
111
112 let resp: AssumeRoleWithOidcResponse =
113 serde_json::from_slice(resp.body()).map_err(|e| {
114 reqsign_core::Error::unexpected(format!("Failed to parse STS response: {e}"))
115 })?;
116 let resp_cred = resp.credentials;
117
118 let cred = Credential {
119 access_key_id: resp_cred.access_key_id,
120 access_key_secret: resp_cred.access_key_secret,
121 security_token: Some(resp_cred.security_token),
122 expires_in: Some(resp_cred.expiration.parse()?),
123 };
124
125 Ok(Some(cred))
126 }
127}
128
129#[derive(Default, Debug, Deserialize)]
130#[serde(default)]
131struct AssumeRoleWithOidcResponse {
132 #[serde(rename = "Credentials")]
133 credentials: AssumeRoleWithOidcCredentials,
134}
135
136#[derive(Default, Debug, Deserialize)]
137#[serde(default, rename_all = "PascalCase")]
138struct AssumeRoleWithOidcCredentials {
139 access_key_id: String,
140 access_key_secret: String,
141 security_token: String,
142 expiration: String,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use reqsign_core::StaticEnv;
149 use reqsign_file_read_tokio::TokioFileRead;
150 use reqsign_http_send_reqwest::ReqwestHttpSend;
151 use std::collections::HashMap;
152
153 #[test]
154 fn test_parse_assume_role_with_oidc_response() -> Result<()> {
155 let content = r#"{
156 "RequestId": "3D57EAD2-8723-1F26-B69C-F8707D8B565D",
157 "OIDCTokenInfo": {
158 "Subject": "KryrkIdjylZb7agUgCEf****",
159 "Issuer": "https://dev-xxxxxx.okta.com",
160 "ClientIds": "496271242565057****"
161 },
162 "AssumedRoleUser": {
163 "AssumedRoleId": "33157794895460****",
164 "Arn": "acs:ram::113511544585****:role/testoidc/TestOidcAssumedRoleSession"
165 },
166 "Credentials": {
167 "SecurityToken": "CAIShwJ1q6Ft5B2yfSjIr5bSEsj4g7BihPWGWHz****",
168 "Expiration": "2021-10-20T04:27:09Z",
169 "AccessKeySecret": "CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****",
170 "AccessKeyId": "STS.NUgYrLnoC37mZZCNnAbez****"
171 }
172}"#;
173
174 let resp: AssumeRoleWithOidcResponse =
175 serde_json::from_str(content).expect("json deserialize must success");
176
177 assert_eq!(
178 &resp.credentials.access_key_id,
179 "STS.NUgYrLnoC37mZZCNnAbez****"
180 );
181 assert_eq!(
182 &resp.credentials.access_key_secret,
183 "CVwjCkNzTMupZ8NbTCxCBRq3K16jtcWFTJAyBEv2****"
184 );
185 assert_eq!(
186 &resp.credentials.security_token,
187 "CAIShwJ1q6Ft5B2yfSjIr5bSEsj4g7BihPWGWHz****"
188 );
189 assert_eq!(&resp.credentials.expiration, "2021-10-20T04:27:09Z");
190
191 Ok(())
192 }
193
194 #[tokio::test]
195 async fn test_assume_role_with_oidc_loader_without_config() {
196 let ctx = Context::new()
197 .with_file_read(TokioFileRead)
198 .with_http_send(ReqwestHttpSend::default());
199 let ctx = ctx.with_env(StaticEnv {
200 home_dir: None,
201 envs: HashMap::new(),
202 });
203
204 let loader = AssumeRoleWithOidcCredentialProvider::new();
205 let credential = loader.provide_credential(&ctx).await.unwrap();
206
207 assert!(credential.is_none());
208 }
209}