1#![allow(dead_code)]
2
3use crate::error::CredentialsError;
4use ini::Ini;
5use log::debug;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::env;
9use std::ops::Deref;
10use std::path::Path;
11use std::sync::atomic::AtomicU32;
12use std::sync::atomic::Ordering;
13use std::time::Duration;
14use time::OffsetDateTime;
15use url::Url;
16
17#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
66pub struct Credentials {
67 pub access_key: Option<String>,
69 pub secret_key: Option<String>,
71 pub security_token: Option<String>,
73 pub session_token: Option<String>,
74 pub expiration: Option<Rfc3339OffsetDateTime>,
75}
76
77#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
78#[repr(transparent)]
79pub struct Rfc3339OffsetDateTime(#[serde(with = "time::serde::rfc3339")] pub time::OffsetDateTime);
80
81impl From<time::OffsetDateTime> for Rfc3339OffsetDateTime {
82 fn from(v: time::OffsetDateTime) -> Self {
83 Self(v)
84 }
85}
86
87impl From<Rfc3339OffsetDateTime> for time::OffsetDateTime {
88 fn from(v: Rfc3339OffsetDateTime) -> Self {
89 v.0
90 }
91}
92
93impl Deref for Rfc3339OffsetDateTime {
94 type Target = time::OffsetDateTime;
95
96 fn deref(&self) -> &Self::Target {
97 &self.0
98 }
99}
100
101#[derive(Deserialize, Debug)]
102#[serde(rename_all = "PascalCase")]
103pub struct AssumeRoleWithWebIdentityResponse {
104 pub assume_role_with_web_identity_result: AssumeRoleWithWebIdentityResult,
105 pub response_metadata: ResponseMetadata,
106}
107
108#[derive(Deserialize, Debug)]
109#[serde(rename_all = "PascalCase")]
110pub struct AssumeRoleWithWebIdentityResult {
111 pub subject_from_web_identity_token: String,
112 pub audience: String,
113 pub assumed_role_user: AssumedRoleUser,
114 pub credentials: StsResponseCredentials,
115 pub provider: String,
116}
117
118#[derive(Deserialize, Debug)]
119#[serde(rename_all = "PascalCase")]
120pub struct StsResponseCredentials {
121 pub session_token: String,
122 pub secret_access_key: String,
123 pub expiration: Rfc3339OffsetDateTime,
124 pub access_key_id: String,
125}
126
127#[derive(Deserialize, Debug)]
128#[serde(rename_all = "PascalCase")]
129pub struct AssumedRoleUser {
130 pub arn: String,
131 pub assumed_role_id: String,
132}
133
134#[derive(Deserialize, Debug)]
135#[serde(rename_all = "PascalCase")]
136pub struct ResponseMetadata {
137 pub request_id: String,
138}
139
140static REQUEST_TIMEOUT_MS: AtomicU32 = AtomicU32::new(30_000);
144
145#[cfg(feature = "http-credentials")]
154pub fn set_request_timeout(timeout: Option<Duration>) -> Option<Duration> {
155 use std::convert::TryInto;
156 let duration_ms = timeout
157 .as_ref()
158 .map(Duration::as_millis)
159 .unwrap_or(u128::MAX)
160 .max(1); let prev = REQUEST_TIMEOUT_MS.swap(duration_ms.try_into().unwrap_or(0), Ordering::Relaxed);
165
166 if prev == 0 {
167 None
168 } else {
169 Some(Duration::from_millis(prev as u64))
170 }
171}
172
173#[cfg(feature = "http-credentials")]
174fn apply_timeout(builder: attohttpc::RequestBuilder) -> attohttpc::RequestBuilder {
175 let timeout_ms = REQUEST_TIMEOUT_MS.load(Ordering::Relaxed);
176 if timeout_ms > 0 {
177 return builder.timeout(Duration::from_millis(timeout_ms as u64));
178 }
179 builder
180}
181
182#[cfg(feature = "http-credentials")]
184fn http_get(url: &str) -> attohttpc::Result<attohttpc::Response> {
185 let builder = apply_timeout(attohttpc::get(url));
186
187 builder.send()
188}
189
190impl Credentials {
191 pub fn refresh(&mut self) -> Result<(), CredentialsError> {
192 if let Some(expiration) = self.expiration {
193 if expiration.0 <= OffsetDateTime::now_utc() {
194 debug!("Refreshing credentials!");
195 let refreshed = Credentials::default()?;
196 *self = refreshed
197 }
198 }
199 Ok(())
200 }
201
202 #[cfg(feature = "http-credentials")]
203 pub fn from_sts_env(session_name: &str) -> Result<Credentials, CredentialsError> {
204 let role_arn = env::var("AWS_ROLE_ARN")?;
205 let web_identity_token_file = env::var("AWS_WEB_IDENTITY_TOKEN_FILE")?;
206 let web_identity_token = std::fs::read_to_string(web_identity_token_file)?;
207 Credentials::from_sts(&role_arn, session_name, &web_identity_token)
208 }
209
210 #[cfg(feature = "http-credentials")]
211 pub fn from_sts(
212 role_arn: &str,
213 session_name: &str,
214 web_identity_token: &str,
215 ) -> Result<Credentials, CredentialsError> {
216 let url = Url::parse_with_params(
217 "https://sts.amazonaws.com/",
218 &[
219 ("Action", "AssumeRoleWithWebIdentity"),
220 ("RoleSessionName", session_name),
221 ("RoleArn", role_arn),
222 ("WebIdentityToken", web_identity_token),
223 ("Version", "2011-06-15"),
224 ],
225 )?;
226 let response = http_get(url.as_str())?;
227 let serde_response =
228 quick_xml::de::from_str::<AssumeRoleWithWebIdentityResponse>(&response.text()?)?;
229 Ok(Credentials {
232 access_key: Some(
233 serde_response
234 .assume_role_with_web_identity_result
235 .credentials
236 .access_key_id,
237 ),
238 secret_key: Some(
239 serde_response
240 .assume_role_with_web_identity_result
241 .credentials
242 .secret_access_key,
243 ),
244 security_token: None,
245 session_token: Some(
246 serde_response
247 .assume_role_with_web_identity_result
248 .credentials
249 .session_token,
250 ),
251 expiration: Some(
252 serde_response
253 .assume_role_with_web_identity_result
254 .credentials
255 .expiration,
256 ),
257 })
258 }
259
260 #[allow(clippy::should_implement_trait)]
261 pub fn default() -> Result<Credentials, CredentialsError> {
262 Credentials::new(None, None, None, None, None)
263 }
264
265 pub fn anonymous() -> Result<Credentials, CredentialsError> {
266 Ok(Credentials {
267 access_key: None,
268 secret_key: None,
269 security_token: None,
270 session_token: None,
271 expiration: None,
272 })
273 }
274
275 pub fn new(
278 access_key: Option<&str>,
279 secret_key: Option<&str>,
280 security_token: Option<&str>,
281 session_token: Option<&str>,
282 profile: Option<&str>,
283 ) -> Result<Credentials, CredentialsError> {
284 if access_key.is_some() {
285 return Ok(Credentials {
286 access_key: access_key.map(|s| s.to_string()),
287 secret_key: secret_key.map(|s| s.to_string()),
288 security_token: security_token.map(|s| s.to_string()),
289 session_token: session_token.map(|s| s.to_string()),
290 expiration: None,
291 });
292 }
293
294 let credentials = Credentials::from_env().or_else(|_| Credentials::from_profile(profile));
295
296 #[cfg(feature = "http-credentials")]
297 let credentials = credentials
298 .or_else(|_| Credentials::from_sts_env("aws-creds"))
299 .or_else(|_| Credentials::from_container_credentials_provider())
300 .or_else(|_| Credentials::from_instance_metadata_v2(false))
301 .or_else(|_| Credentials::from_instance_metadata(false));
302
303 credentials.map_err(|_| CredentialsError::NoCredentials)
304 }
305
306 pub fn from_env_specific(
307 access_key_var: Option<&str>,
308 secret_key_var: Option<&str>,
309 security_token_var: Option<&str>,
310 session_token_var: Option<&str>,
311 ) -> Result<Credentials, CredentialsError> {
312 let access_key = from_env_with_default(access_key_var, "AWS_ACCESS_KEY_ID")?;
313 let secret_key = from_env_with_default(secret_key_var, "AWS_SECRET_ACCESS_KEY")?;
314
315 let security_token = from_env_with_default(security_token_var, "AWS_SECURITY_TOKEN").ok();
316 let session_token = from_env_with_default(session_token_var, "AWS_SESSION_TOKEN").ok();
317 Ok(Credentials {
318 access_key: Some(access_key),
319 secret_key: Some(secret_key),
320 security_token,
321 session_token,
322 expiration: None,
323 })
324 }
325
326 pub fn from_env() -> Result<Credentials, CredentialsError> {
327 Credentials::from_env_specific(None, None, None, None)
328 }
329
330 #[cfg(feature = "http-credentials")]
331 pub fn from_container_credentials_provider() -> Result<Credentials, CredentialsError> {
332 let Ok(credentials_path) = env::var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") else {
333 return Err(CredentialsError::NotContainer);
334 };
335
336 let resp: CredentialsFromInstanceMetadata = apply_timeout(attohttpc::get(format!(
337 "http://169.254.170.2{}",
338 credentials_path
339 )))
340 .send()?
341 .json()?;
342
343 Ok(Credentials {
344 access_key: Some(resp.access_key_id),
345 secret_key: Some(resp.secret_access_key),
346 security_token: Some(resp.token),
347 expiration: Some(resp.expiration),
348 session_token: None,
349 })
350 }
351
352 #[cfg(feature = "http-credentials")]
353 pub fn from_instance_metadata(not_ec2: bool) -> Result<Credentials, CredentialsError> {
354 if !not_ec2 && !is_ec2() {
355 return Err(CredentialsError::NotEc2);
356 }
357
358 let role = apply_timeout(attohttpc::get(
359 "http://169.254.169.254/latest/meta-data/iam/security-credentials",
360 ))
361 .send()?
362 .text()?;
363
364 let resp: CredentialsFromInstanceMetadata = apply_timeout(attohttpc::get(format!(
365 "http://169.254.169.254/latest/meta-data/iam/security-credentials/{}",
366 role
367 )))
368 .send()?
369 .json()?;
370
371 Ok(Credentials {
372 access_key: Some(resp.access_key_id),
373 secret_key: Some(resp.secret_access_key),
374 security_token: Some(resp.token),
375 expiration: Some(resp.expiration),
376 session_token: None,
377 })
378 }
379
380 #[cfg(feature = "http-credentials")]
381 pub fn from_instance_metadata_v2(not_ec2: bool) -> Result<Credentials, CredentialsError> {
382 if !not_ec2 && !is_ec2() {
383 return Err(CredentialsError::NotEc2);
384 }
385
386 let token = apply_timeout(attohttpc::put("http://169.254.169.254/latest/api/token"))
387 .header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
388 .send()?;
389 if !token.is_success() {
390 return Err(CredentialsError::UnexpectedStatusCode(
391 token.status().as_u16(),
392 ));
393 }
394 let token = token.text()?;
395
396 let role = apply_timeout(attohttpc::get(
397 "http://169.254.169.254/latest/meta-data/iam/security-credentials",
398 ))
399 .header("X-aws-ec2-metadata-token", &token)
400 .send()?
401 .text()?;
402
403 let resp: CredentialsFromInstanceMetadata = apply_timeout(attohttpc::get(format!(
404 "http://169.254.169.254/latest/meta-data/iam/security-credentials/{}",
405 role
406 )))
407 .header("X-aws-ec2-metadata-token", &token)
408 .send()?
409 .json()?;
410
411 Ok(Credentials {
412 access_key: Some(resp.access_key_id),
413 secret_key: Some(resp.secret_access_key),
414 security_token: Some(resp.token),
415 expiration: Some(resp.expiration),
416 session_token: None,
417 })
418 }
419
420 pub fn from_credentials_file<P: AsRef<Path>>(
441 file: P,
442 section: Option<&str>,
443 ) -> Result<Credentials, CredentialsError> {
444 let conf = Ini::load_from_file(file.as_ref())?;
445 let section = section.unwrap_or("default");
446 let data = conf
447 .section(Some(section))
448 .ok_or(CredentialsError::ConfigNotFound)?;
449 let access_key = data
450 .get("aws_access_key_id")
451 .map(|s| s.to_string())
452 .ok_or(CredentialsError::ConfigMissingAccessKeyId)?;
453 let secret_key = data
454 .get("aws_secret_access_key")
455 .map(|s| s.to_string())
456 .ok_or(CredentialsError::ConfigMissingSecretKey)?;
457 let credentials = Credentials {
458 access_key: Some(access_key),
459 secret_key: Some(secret_key),
460 security_token: data.get("aws_security_token").map(|s| s.to_string()),
461 session_token: data.get("aws_session_token").map(|s| s.to_string()),
462 expiration: None,
463 };
464 Ok(credentials)
465 }
466
467 pub fn from_profile(section: Option<&str>) -> Result<Credentials, CredentialsError> {
468 let profile = if let Ok(path) = env::var("AWS_SHARED_CREDENTIALS_FILE") {
470 path
471 } else {
472 let home_dir = home::home_dir().ok_or(CredentialsError::HomeDir)?;
473 format!("{}/.aws/credentials", home_dir.display())
474 };
475 Credentials::from_credentials_file(&profile, section)
476 }
477}
478
479fn from_env_with_default(var: Option<&str>, default: &str) -> Result<String, CredentialsError> {
480 let val = var.unwrap_or(default);
481 env::var(val)
482 .or_else(|_e| env::var(val))
483 .map_err(|_| CredentialsError::MissingEnvVar(val.to_string(), default.to_string()))
484}
485
486fn is_ec2() -> bool {
487 if let Ok(uuid) = std::fs::read_to_string("/sys/hypervisor/uuid") {
488 if uuid.starts_with("ec2") {
489 return true;
490 }
491 }
492 if let Ok(vendor) = std::fs::read_to_string("/sys/class/dmi/id/board_vendor") {
493 if vendor.starts_with("Amazon EC2") {
494 return true;
495 }
496 }
497 false
498}
499
500#[derive(Deserialize)]
501#[serde(rename_all = "PascalCase")]
502struct CredentialsFromInstanceMetadata {
503 access_key_id: String,
504 secret_access_key: String,
505 token: String,
506 expiration: Rfc3339OffsetDateTime, }
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use std::io::Write;
513 use tempfile::NamedTempFile;
514
515 fn create_test_credentials_file(content: &str) -> NamedTempFile {
516 let mut file = NamedTempFile::new().unwrap();
517 file.write_all(content.as_bytes()).unwrap();
518 file.flush().unwrap();
519 file
520 }
521
522 #[test]
523 fn test_from_credentials_file_custom_location() {
524 let content = r#"[default]
525aws_access_key_id = AKIAIOSFODNN7EXAMPLE
526aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
527
528[production]
529aws_access_key_id = PROD_KEY
530aws_secret_access_key = PROD_SECRET
531aws_session_token = PROD_SESSION_TOKEN
532"#;
533 let file = create_test_credentials_file(content);
534
535 let creds = Credentials::from_credentials_file(file.path(), None).unwrap();
537 assert_eq!(creds.access_key.unwrap(), "AKIAIOSFODNN7EXAMPLE");
538
539 let creds = Credentials::from_credentials_file(file.path(), Some("production")).unwrap();
541 assert_eq!(creds.access_key.unwrap(), "PROD_KEY");
542 assert_eq!(creds.session_token.unwrap(), "PROD_SESSION_TOKEN");
543 }
544
545 #[test]
546 fn test_from_profile_respects_env_var() {
547 let content = r#"[default]
548aws_access_key_id = ENV_KEY
549aws_secret_access_key = ENV_SECRET
550"#;
551 let file = create_test_credentials_file(content);
552
553 env::set_var("AWS_SHARED_CREDENTIALS_FILE", file.path());
555
556 let creds = Credentials::from_profile(None).unwrap();
557 assert_eq!(creds.access_key.unwrap(), "ENV_KEY");
558
559 env::remove_var("AWS_SHARED_CREDENTIALS_FILE");
561 }
562
563 #[test]
564 fn test_from_credentials_file_errors() {
565 let result = Credentials::from_credentials_file("/nonexistent/path", None);
567 assert!(result.is_err());
568
569 let content = r#"[default]
571aws_access_key_id = KEY
572aws_secret_access_key = SECRET
573"#;
574 let file = create_test_credentials_file(content);
575 let result = Credentials::from_credentials_file(file.path(), Some("nonexistent"));
576 assert!(matches!(
577 result.unwrap_err(),
578 CredentialsError::ConfigNotFound
579 ));
580 }
581}
582
583#[cfg(test)]
584#[test]
585fn test_instance_metadata_creds_deserialization() {
586 serde_json::from_str::<CredentialsFromInstanceMetadata>(
589 r#"
590 {
591 "Code" : "Success",
592 "LastUpdated" : "2012-04-26T16:39:16Z",
593 "Type" : "AWS-HMAC",
594 "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
595 "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
596 "Token" : "token",
597 "Expiration" : "2017-05-17T15:09:54Z"
598 }
599 "#,
600 )
601 .unwrap();
602}
603
604#[cfg(test)]
605#[ignore]
606#[test]
607fn test_credentials_refresh() {
608 let mut c = Credentials::default().expect("Could not generate credentials");
609 let e = Rfc3339OffsetDateTime(OffsetDateTime::now_utc());
610 c.expiration = Some(e);
611 std::thread::sleep(std::time::Duration::from_secs(3));
612 c.refresh().expect("Could not refresh");
613 assert!(c.expiration.is_none())
614}