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