aws_config/
web_identity_token.rs1use crate::provider_config::ProviderConfig;
65use crate::sts;
66use aws_credential_types::credential_feature::AwsCredentialFeature;
67use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
68use aws_sdk_sts::{types::PolicyDescriptorType, Client as StsClient};
69use aws_smithy_async::time::SharedTimeSource;
70use aws_smithy_types::error::display::DisplayErrorContext;
71use aws_types::os_shim_internal::{Env, Fs};
72
73use std::borrow::Cow;
74use std::path::{Path, PathBuf};
75
76const ENV_VAR_TOKEN_FILE: &str = "AWS_WEB_IDENTITY_TOKEN_FILE";
77const ENV_VAR_ROLE_ARN: &str = "AWS_ROLE_ARN";
78const ENV_VAR_SESSION_NAME: &str = "AWS_ROLE_SESSION_NAME";
79
80#[derive(Debug)]
84pub struct WebIdentityTokenCredentialsProvider {
85 source: Source,
86 time_source: SharedTimeSource,
87 fs: Fs,
88 sts_client: StsClient,
89 policy: Option<String>,
90 policy_arns: Option<Vec<PolicyDescriptorType>>,
91}
92
93impl WebIdentityTokenCredentialsProvider {
94 pub fn builder() -> Builder {
96 Builder::default()
97 }
98}
99
100#[derive(Debug)]
101enum Source {
102 Env(Env),
103 Static(StaticConfiguration),
104}
105
106#[derive(Debug, Clone)]
108pub struct StaticConfiguration {
109 pub web_identity_token_file: PathBuf,
111
112 pub role_arn: String,
114
115 pub session_name: String,
117}
118
119impl ProvideCredentials for WebIdentityTokenCredentialsProvider {
120 fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
121 where
122 Self: 'a,
123 {
124 future::ProvideCredentials::new(self.credentials())
125 }
126}
127
128impl WebIdentityTokenCredentialsProvider {
129 fn source(&self) -> Result<Cow<'_, StaticConfiguration>, CredentialsError> {
130 match &self.source {
131 Source::Env(env) => {
132 let token_file = env.get(ENV_VAR_TOKEN_FILE).map_err(|_| {
133 CredentialsError::not_loaded(format!("${ENV_VAR_TOKEN_FILE} was not set"))
134 })?;
135 let role_arn = env.get(ENV_VAR_ROLE_ARN).map_err(|_| {
136 CredentialsError::not_loaded("AWS_ROLE_ARN environment variable must be set")
137 })?;
138 let session_name = env.get(ENV_VAR_SESSION_NAME).unwrap_or_else(|_| {
139 sts::util::default_session_name("web-identity-token", self.time_source.now())
140 });
141 Ok(Cow::Owned(StaticConfiguration {
142 web_identity_token_file: token_file.into(),
143 role_arn,
144 session_name,
145 }))
146 }
147 Source::Static(conf) => Ok(Cow::Borrowed(conf)),
148 }
149 }
150 async fn credentials(&self) -> provider::Result {
151 let conf = self.source()?;
152 load_credentials(
153 &self.fs,
154 &self.sts_client,
155 self.policy.clone(),
156 self.policy_arns.clone(),
157 &conf.web_identity_token_file,
158 &conf.role_arn,
159 &conf.session_name,
160 )
161 .await
162 .map(|mut creds| {
163 creds
164 .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
165 .push(AwsCredentialFeature::CredentialsProfileStsWebIdToken);
166 creds
167 })
168 }
169}
170
171#[derive(Debug, Default)]
173pub struct Builder {
174 source: Option<Source>,
175 config: Option<ProviderConfig>,
176 policy: Option<String>,
177 policy_arns: Option<Vec<PolicyDescriptorType>>,
178}
179
180impl Builder {
181 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
194 self.config = Some(provider_config.clone());
195 self
196 }
197
198 pub fn static_configuration(mut self, config: StaticConfiguration) -> Self {
204 self.source = Some(Source::Static(config));
205 self
206 }
207
208 pub fn policy(mut self, policy: impl Into<String>) -> Self {
214 self.policy = Some(policy.into());
215 self
216 }
217
218 pub fn policy_arns(mut self, policy_arns: Vec<String>) -> Self {
224 self.policy_arns = Some(
225 policy_arns
226 .into_iter()
227 .map(|arn| PolicyDescriptorType::builder().arn(arn).build())
228 .collect::<Vec<_>>(),
229 );
230 self
231 }
232
233 pub fn build(self) -> WebIdentityTokenCredentialsProvider {
239 let conf = self.config.unwrap_or_default();
240 let source = self.source.unwrap_or_else(|| Source::Env(conf.env()));
241 WebIdentityTokenCredentialsProvider {
242 source,
243 fs: conf.fs(),
244 sts_client: StsClient::new(&conf.client_config()),
245 time_source: conf.time_source(),
246 policy: self.policy,
247 policy_arns: self.policy_arns,
248 }
249 }
250}
251
252async fn load_credentials(
253 fs: &Fs,
254 sts_client: &StsClient,
255 policy: Option<String>,
256 policy_arns: Option<Vec<PolicyDescriptorType>>,
257 token_file: impl AsRef<Path>,
258 role_arn: &str,
259 session_name: &str,
260) -> provider::Result {
261 let token = fs
262 .read_to_end(token_file)
263 .await
264 .map_err(CredentialsError::provider_error)?;
265 let token = String::from_utf8(token).map_err(|_utf_8_error| {
266 CredentialsError::unhandled("WebIdentityToken was not valid UTF-8")
267 })?;
268
269 let resp = sts_client.assume_role_with_web_identity()
270 .role_arn(role_arn)
271 .role_session_name(session_name)
272 .set_policy(policy)
273 .set_policy_arns(policy_arns)
274 .web_identity_token(token)
275 .send()
276 .await
277 .map_err(|sdk_error| {
278 tracing::warn!(error = %DisplayErrorContext(&sdk_error), "STS returned an error assuming web identity role");
279 CredentialsError::provider_error(sdk_error)
280 })?;
281 sts::util::into_credentials(resp.credentials, resp.assumed_role_user, "WebIdentityToken")
282}
283
284#[cfg(test)]
285mod test {
286 use crate::provider_config::ProviderConfig;
287 use crate::test_case::no_traffic_client;
288 use crate::web_identity_token::{
289 Builder, ENV_VAR_ROLE_ARN, ENV_VAR_SESSION_NAME, ENV_VAR_TOKEN_FILE,
290 };
291 use aws_credential_types::provider::error::CredentialsError;
292 use aws_smithy_async::rt::sleep::TokioSleep;
293 use aws_smithy_types::error::display::DisplayErrorContext;
294 use aws_types::os_shim_internal::{Env, Fs};
295 use aws_types::region::Region;
296 use std::collections::HashMap;
297
298 #[tokio::test]
299 async fn unloaded_provider() {
300 let conf = ProviderConfig::empty()
302 .with_sleep_impl(TokioSleep::new())
303 .with_env(Env::from_slice(&[]))
304 .with_http_client(no_traffic_client())
305 .with_region(Some(Region::from_static("us-east-1")));
306
307 let provider = Builder::default().configure(&conf).build();
308 let err = provider
309 .credentials()
310 .await
311 .expect_err("should fail, provider not loaded");
312 match err {
313 CredentialsError::CredentialsNotLoaded { .. } => { }
314 _ => panic!("incorrect error variant"),
315 }
316 }
317
318 #[tokio::test]
319 async fn missing_env_var() {
320 let env = Env::from_slice(&[(ENV_VAR_TOKEN_FILE, "/token.jwt")]);
321 let region = Some(Region::new("us-east-1"));
322 let provider = Builder::default()
323 .configure(
324 &ProviderConfig::empty()
325 .with_sleep_impl(TokioSleep::new())
326 .with_region(region)
327 .with_env(env)
328 .with_http_client(no_traffic_client()),
329 )
330 .build();
331 let err = provider
332 .credentials()
333 .await
334 .expect_err("should fail, provider not loaded");
335 assert!(
336 format!("{}", DisplayErrorContext(&err)).contains("AWS_ROLE_ARN"),
337 "`{}` did not contain expected string",
338 err
339 );
340 assert!(
341 matches!(err, CredentialsError::CredentialsNotLoaded(_)),
342 "incorrect error variant: {err:?}"
343 );
344 }
345
346 #[tokio::test]
347 async fn fs_missing_file() {
348 let env = Env::from_slice(&[
349 (ENV_VAR_TOKEN_FILE, "/token.jwt"),
350 (ENV_VAR_ROLE_ARN, "arn:aws:iam::123456789123:role/test-role"),
351 (ENV_VAR_SESSION_NAME, "test-session"),
352 ]);
353 let fs = Fs::from_raw_map(HashMap::new());
354 let provider = Builder::default()
355 .configure(
356 &ProviderConfig::empty()
357 .with_sleep_impl(TokioSleep::new())
358 .with_http_client(no_traffic_client())
359 .with_region(Some(Region::new("us-east-1")))
360 .with_env(env)
361 .with_fs(fs),
362 )
363 .build();
364 let err = provider.credentials().await.expect_err("no JWT token");
365 match err {
366 CredentialsError::ProviderError { .. } => { }
367 _ => panic!("incorrect error variant"),
368 }
369 }
370}