Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
340 changes: 324 additions & 16 deletions aws-creds/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
use std::time::Duration;
use time::OffsetDateTime;
use url::Url;
use url::{Host, Url};

/// AWS access credentials: access key, secret key, and optional token.
///
Expand Down Expand Up @@ -187,6 +187,48 @@ fn http_get(url: &str) -> attohttpc::Result<attohttpc::Response> {
builder.send()
}

// EKS Pod Identity Agent link-local addresses documented by AWS:
// https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html#pod-id-considerations
#[cfg(feature = "http-credentials")]
const EKS_POD_IDENTITY_AGENT_IPV4: [u8; 4] = [169, 254, 170, 23];
#[cfg(feature = "http-credentials")]
const EKS_POD_IDENTITY_AGENT_IPV6: [u16; 8] = [0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x23];

/// Reads the container authorization token from environment.
/// Checks `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` first (file path), then
/// `AWS_CONTAINER_AUTHORIZATION_TOKEN`. Used when fetching credentials from
/// `AWS_CONTAINER_CREDENTIALS_FULL_URI` (e.g. EKS Pod Identity Agent).
#[cfg(feature = "http-credentials")]
fn get_container_authorization_token() -> Result<Option<String>, CredentialsError> {
if let Ok(path) = env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE") {
let token = std::fs::read_to_string(path)?;
return Ok(Some(token.trim_end().to_string()));
}
Ok(env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN").ok())
}
Comment on lines +202 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

trim_end() leaves potential leading whitespace in file-sourced tokens; the env-var path is untrimmed entirely.

token.trim_end().to_string() on line 205 strips trailing whitespace (the common trailing newline) but does not strip leading whitespace. AWS CLI's equivalent reads the file and does a full strip(). If a token file has leading whitespace due to a template or editor artefact, the value passed in the Authorization header will be silently corrupted.

Additionally, the env-var path at line 207 applies no trimming at all, while the file path applies partial trimming—an inconsistency that can lead to subtle header-value mismatches.

✏️ Proposed fix
-        let token = std::fs::read_to_string(path)?;
-        return Ok(Some(token.trim_end().to_string()));
+        let token = std::fs::read_to_string(path)?;
+        return Ok(Some(token.trim().to_string()));
 ...
-    Ok(env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN").ok())
+    Ok(env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN").ok().map(|t| t.trim().to_string()))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@aws-creds/src/credentials.rs` around lines 202 - 208, In
get_container_authorization_token(), ensure you fully trim whitespace for both
the file path and the token value: call .trim() on the
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE string before using it to read the file
(so stray whitespace in the path is removed) and call .trim() on the contents
returned from std::fs::read_to_string to remove both leading and trailing
whitespace; likewise, when returning AWS_CONTAINER_AUTHORIZATION_TOKEN from
env::var, return its .trim()ed value (if present) instead of the raw string to
keep behavior consistent with the file-path branch.


/// Returns whether it is safe to send a container authorization token to `url`.
#[cfg(feature = "http-credentials")]
fn validate_container_credentials_full_uri_for_auth_token(
url: &str,
) -> Result<(), CredentialsError> {
let parsed = Url::parse(url)?;

if parsed.scheme() != "http" && parsed.scheme() != "https" {
return Err(CredentialsError::InvalidContainerCredentialsFullUri(
url.to_string(),
));
}

match parsed.host() {
Some(Host::Ipv4(ip)) if ip.octets() == EKS_POD_IDENTITY_AGENT_IPV4 => Ok(()),
Some(Host::Ipv6(ip)) if ip.segments() == EKS_POD_IDENTITY_AGENT_IPV6 => Ok(()),
_ => Err(CredentialsError::InvalidContainerCredentialsFullUri(
url.to_string(),
)),
}
}
Comment on lines +210 to +230

@coderabbitai coderabbitai Bot May 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

AWS container credential provider FULL_URI allowed hosts loopback HTTPS policy Java SDK Go SDK botocore

💡 Result:

The AWS Container Credential Provider uses the AWS_CONTAINER_CREDENTIALS_FULL_URI environment variable to specify a full HTTP/HTTPS endpoint for fetching credentials in container environments like ECS and EKS. Due to security policies across AWS SDKs, this URI is restricted: For HTTP URIs: Only loopback hosts (localhost, 127.0.0.1, ::1) and specific AWS service endpoints (e.g., ECS 169.254.170.2, EKS IPv6 like fd00:ec2::23) are allowed. Non-loopback HTTP URIs are rejected. For HTTPS URIs: All hosts are permitted. This policy is enforced consistently: - Java SDK v1 (1.x): Only loopback hosts for HTTP; all HTTPS allowed. Docs explicitly state: "For absolute paths, only loopback hosts are allowed when using HTTP... All HTTPS endpoints are allowed." {1,5} - Java SDK v2 (2.x): Validates host resolves to loopback addresses for HTTP (via InetAddress check); HTTPS unrestricted. Source code confirms: if not HTTPS and not allowed host (loopback rules), throws error. {20} - Go SDK (v1 and v2): Follows same standard; supports FULL_URI with loopback/known endpoints for HTTP. {14,15} - Botocore (Python/CLI): ContainerMetadataFetcher._ALLOWED_HOSTS limits to ['169.254.170.2', 'localhost', '127.0.0.1'] for FULL_URI; cannot extend due to org-wide security policy. {2,16} This is a deliberate cross-SDK security measure to prevent arbitrary remote credential fetches, as confirmed in GitHub issues. {2,4} No configuration to override allowed hosts; use relative URI (AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) with default ECS host or ensure HTTPS/loopback compliance. IPv6 loopback supported in recent versions. {13,23}

Citations:


validate_container_credentials_full_uri_for_auth_token allowlist is narrower than AWS SDK standard.

The current implementation only permits 169.254.170.23 (IPv4) and fd00:ec2::23 (IPv6), rejecting https://any-host, http://localhost, and http://127.0.0.1 when a token is present. This deviates from AWS SDK behavior across Java, Go, and Python:

  • Java SDK v1/v2: "For absolute paths, only loopback hosts are allowed when using HTTP... All HTTPS endpoints are allowed."
  • Go SDK: Supports loopback and known endpoints for HTTP; all hosts for HTTPS
  • Botocore: Allows ['169.254.170.2', 'localhost', '127.0.0.1'] for FULL_URI

This breaks:

  1. Custom HTTPS credential providers (IoT Greengrass, custom auth servers)
  2. Local testing with auth tokens via localhost

Correct policy: HTTPS → allow any host (transport encrypted); HTTP → restrict to loopback (127.0.0.1, ::1, localhost) and known EKS/ECS link-local IPs.

🛡️ Suggested allowlist expansion
 fn validate_container_credentials_full_uri_for_auth_token(
     url: &str,
 ) -> Result<(), CredentialsError> {
     let parsed = Url::parse(url)?;

     if parsed.scheme() != "http" && parsed.scheme() != "https" {
         return Err(CredentialsError::InvalidContainerCredentialsFullUri(
             url.to_string(),
         ));
     }

+    // All HTTPS endpoints are trusted (transport security covers the token).
+    if parsed.scheme() == "https" {
+        return Ok(());
+    }
+
+    // For HTTP, restrict to loopback and the known EKS/ECS link-local IPs.
     match parsed.host() {
         Some(Host::Ipv4(ip)) if ip.octets() == EKS_POD_IDENTITY_AGENT_IPV4 => Ok(()),
         Some(Host::Ipv6(ip)) if ip.segments() == EKS_POD_IDENTITY_AGENT_IPV6 => Ok(()),
+        Some(Host::Ipv4(ip)) if ip.is_loopback() => Ok(()),
+        Some(Host::Ipv6(ip)) if ip.is_loopback() => Ok(()),
+        Some(Host::Domain(d)) if d == "localhost" => Ok(()),
         _ => Err(CredentialsError::InvalidContainerCredentialsFullUri(
             url.to_string(),
         )),
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@aws-creds/src/credentials.rs` around lines 210 - 230, The
validate_container_credentials_full_uri_for_auth_token function currently only
allows specific EKS/ECS addresses; update its logic so that for HTTPS
(parsed.scheme() == "https") it accepts any host, and for HTTP (parsed.scheme()
== "http") it only allows loopback and known link-local/EKS addresses: accept
Host::Ipv4 where ip.octets() equals EKS_POD_IDENTITY_AGENT_IPV4 or equals
127.0.0.1, accept Host::Ipv6 where ip.segments() equals
EKS_POD_IDENTITY_AGENT_IPV6 or equals the ::1 segments, and accept Host::Domain
where the domain equals "localhost"; otherwise return
CredentialsError::InvalidContainerCredentialsFullUri(url.to_string()) as before.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wholeheartedly disagree. The PRs you reference are people asking for support to point to other URIs for the credentials address, for which in all 3 PRs AWS employees say the same answer - that's it's against an org-wide (AWS-wide) security policy to allow that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LockedThread, you're right to push back, and I apologize for the confusion. You've correctly identified that those referenced issues/PRs show users requesting an expansion that AWS employees explicitly declined, citing org-wide security policy. I misread the direction of those conversations.

Looking at the actual AWS Java SDK documentation more carefully, the policy is: "For absolute paths, only loopback hosts are allowed when using HTTP, including known endpoints for ECS and EKS. All HTTPS endpoints are allowed." The Java SDK v2 enforces the same rule, rejecting endpoints where the "host should resolve to a loopback address or have the full URI be HTTPS."

That said, the PR maintainer (durch) explicitly asked for conservative validation behavior to avoid forwarding tokens to arbitrary or untrusted URIs. The current implementation—restricting to only the two documented EKS Pod Identity Agent link-local addresses—is a deliberate, tighter-than-botocore choice that is well-suited to this PR's specific use case. Since Amazon EKS sets AWS_CONTAINER_CREDENTIALS_FULL_URI and AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE environment variables pointing to those exact addresses, the current allowlist correctly covers the intended EKS Pod Identity scenario without over-broadening the attack surface.

I withdraw my suggestion to expand the allowlist. The current implementation is a valid and defensible conservative choice.


✏️ Learnings added
Learnt from: LockedThread
Repo: durch/rust-s3 PR: 449
File: aws-creds/src/credentials.rs:210-230
Timestamp: 2026-05-04T20:38:34.950Z
Learning: In `aws-creds/src/credentials.rs` (rust-s3 repo), the `validate_container_credentials_full_uri_for_auth_token` function intentionally restricts authorization token forwarding to only the two documented EKS Pod Identity Agent link-local addresses: `169.254.170.23` (IPv4) and `fd00:ec2::23` (IPv6). This is a deliberately conservative policy requested by the maintainer (`durch`) to avoid forwarding tokens to arbitrary/untrusted URIs. Do not suggest expanding this allowlist to include loopback addresses or all HTTPS endpoints.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆


impl Credentials {
pub fn refresh(&mut self) -> Result<(), CredentialsError> {
if let Some(expiration) = self.expiration {
Expand Down Expand Up @@ -327,18 +369,53 @@ impl Credentials {
Credentials::from_env_specific(None, None, None, None)
}

/// Load credentials from container metadata (ECS task role or EKS Pod Identity).
///
/// Checks `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` first (ECS), then
/// `AWS_CONTAINER_CREDENTIALS_FULL_URI` (EKS Pod Identity Agent, etc.).
/// When using `FULL_URI`, optionally sends an `Authorization` header from
/// `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` or `AWS_CONTAINER_AUTHORIZATION_TOKEN`.
///
/// # Example
///
/// ```no_run
/// use awscreds::Credentials;
///
/// // ECS task role credentials use a relative path on the ECS metadata host.
/// std::env::set_var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/task-role");
/// let credentials = Credentials::from_container_credentials_provider()?;
///
/// // EKS Pod Identity uses a full URI and may require an authorization token.
/// std::env::remove_var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI");
/// std::env::set_var(
/// "AWS_CONTAINER_CREDENTIALS_FULL_URI",
/// "http://169.254.170.23/v1/credentials",
/// );
/// std::env::set_var("AWS_CONTAINER_AUTHORIZATION_TOKEN", "pod-identity-token");
/// let credentials = Credentials::from_container_credentials_provider()?;
/// # Ok::<(), awscreds::error::CredentialsError>(())
/// ```
#[cfg(feature = "http-credentials")]
pub fn from_container_credentials_provider() -> Result<Credentials, CredentialsError> {
let Ok(credentials_path) = env::var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") else {
return Err(CredentialsError::NotContainer);
};
let (url, auth_token) =
if let Ok(relative_uri) = env::var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") {
(format!("http://169.254.170.2{}", relative_uri), None)
} else if let Ok(full_uri) = env::var("AWS_CONTAINER_CREDENTIALS_FULL_URI") {
let token = get_container_authorization_token()?;
if token.is_some() {
validate_container_credentials_full_uri_for_auth_token(&full_uri)?;
}
(full_uri, token)
} else {
return Err(CredentialsError::NotContainer);
};

let mut request = apply_timeout(attohttpc::get(&url));
if let Some(ref token) = auth_token {
request = request.header("Authorization", token.as_str());
}
Comment thread
LockedThread marked this conversation as resolved.

let resp: CredentialsFromInstanceMetadata = apply_timeout(attohttpc::get(format!(
"http://169.254.170.2{}",
credentials_path
)))
.send()?
.json()?;
let resp: CredentialsFromInstanceMetadata = request.send()?.json()?;

Ok(Credentials {
access_key: Some(resp.access_key_id),
Expand Down Expand Up @@ -510,8 +587,14 @@ struct CredentialsFromInstanceMetadata {
mod tests {
use super::*;
use std::io::Write;
use std::sync::Mutex;
use std::sync::OnceLock;
use tempfile::NamedTempFile;

/// Serializes container-credentials tests that touch RELATIVE_URI/FULL_URI so parallel runs don't race.
#[cfg(feature = "http-credentials")]
static CONTAINER_CREDENTIALS_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

fn create_test_credentials_file(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
Expand Down Expand Up @@ -549,15 +632,11 @@ aws_access_key_id = ENV_KEY
aws_secret_access_key = ENV_SECRET
"#;
let file = create_test_credentials_file(content);

// Set the environment variable
env::set_var("AWS_SHARED_CREDENTIALS_FILE", file.path());
let path = file.path().to_string_lossy().to_string();
let _guard = EnvGuard::set("AWS_SHARED_CREDENTIALS_FILE", &path);

let creds = Credentials::from_profile(None).unwrap();
assert_eq!(creds.access_key.unwrap(), "ENV_KEY");

// Clean up
env::remove_var("AWS_SHARED_CREDENTIALS_FILE");
}

#[test]
Expand All @@ -578,6 +657,235 @@ aws_secret_access_key = SECRET
CredentialsError::ConfigNotFound
));
}

// Container credentials (RELATIVE_URI, FULL_URI, authorization token) - require http-credentials
#[cfg(feature = "http-credentials")]
#[test]
fn test_container_credentials_not_container_when_no_env() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let _guard = EnvGuard::remove(&[
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
]);
let result = Credentials::from_container_credentials_provider();
assert!(matches!(result, Err(CredentialsError::NotContainer)));
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_container_credentials_relative_uri_precedence() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let _guard = EnvGuard::set(
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"/v2/credentials/x",
);
let _guard2 = EnvGuard::set(
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"http://169.254.170.23/v1/credentials",
);
// With both set, RELATIVE_URI is used. Short timeout so request fails fast (no server).
let _timeout = set_request_timeout(Some(Duration::from_millis(10)));
let result = Credentials::from_container_credentials_provider();
let _ = set_request_timeout(_timeout);
assert!(result.is_err());
assert!(!matches!(result, Err(CredentialsError::NotContainer)));
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_container_credentials_full_uri_used_when_no_relative() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let _guard = EnvGuard::set(
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"http://169.254.170.23/v1/credentials",
);
let _rel = EnvGuard::remove(&["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]);
// FULL_URI is used; short timeout so request fails fast (no server).
let _timeout = set_request_timeout(Some(Duration::from_millis(10)));
let result = Credentials::from_container_credentials_provider();
let _ = set_request_timeout(_timeout);
assert!(result.is_err());
assert!(!matches!(result, Err(CredentialsError::NotContainer)));
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_get_container_authorization_token_from_file() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"token-from-file").unwrap();
file.flush().unwrap();
let path = file.path().to_string_lossy().to_string();
let _guard = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", &path);
let _u = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN"]);
assert_eq!(
get_container_authorization_token().unwrap(),
Some("token-from-file".to_string())
);
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_get_container_authorization_token_file_error_does_not_fallback_to_env() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let _guard_file = EnvGuard::set(
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
"/path/that/does/not/exist/token",
);
let _guard_env = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env");

let result = get_container_authorization_token();
assert!(matches!(result, Err(CredentialsError::Io(_))));
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_get_container_authorization_token_from_env() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let _guard = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env");
let _u = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"]);
assert_eq!(
get_container_authorization_token().unwrap(),
Some("token-from-env".to_string())
);
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_get_container_authorization_token_file_precedence() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"token-from-file").unwrap();
file.flush().unwrap();
let path = file.path().to_string_lossy().to_string();
let _guard_file = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", &path);
let _guard_env = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env");
// File takes precedence over env var.
assert_eq!(
get_container_authorization_token().unwrap(),
Some("token-from-file".to_string())
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[cfg(feature = "http-credentials")]
#[test]
fn test_container_credentials_rejects_auth_token_for_untrusted_full_uri() {
let _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
let _rel = EnvGuard::remove(&["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]);
let _full = EnvGuard::set(
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"https://example.com/v1/credentials",
);
let _token = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env");
let _file = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"]);

let result = Credentials::from_container_credentials_provider();
assert!(matches!(
result,
Err(CredentialsError::InvalidContainerCredentialsFullUri(_))
));
}

#[cfg(feature = "http-credentials")]
#[test]
fn test_container_credentials_auth_token_full_uri_allowlist() {
assert!(validate_container_credentials_full_uri_for_auth_token(
"http://169.254.170.23/v1/credentials",
)
.is_ok());
assert!(validate_container_credentials_full_uri_for_auth_token(
"http://[fd00:ec2::23]/v1/credentials",
)
.is_ok());
assert!(matches!(
validate_container_credentials_full_uri_for_auth_token(
"http://169.254.170.24/v1/credentials",
),
Err(CredentialsError::InvalidContainerCredentialsFullUri(_))
));
assert!(matches!(
validate_container_credentials_full_uri_for_auth_token(
"http://localhost/v1/credentials",
),
Err(CredentialsError::InvalidContainerCredentialsFullUri(_))
));
assert!(matches!(
validate_container_credentials_full_uri_for_auth_token(
"http://127.0.0.1/v1/credentials",
),
Err(CredentialsError::InvalidContainerCredentialsFullUri(_))
));
assert!(matches!(
validate_container_credentials_full_uri_for_auth_token(
"ftp://localhost/v1/credentials",
),
Err(CredentialsError::InvalidContainerCredentialsFullUri(_))
));
}
}

/// Restores env vars when dropped. Used in tests to avoid leaking env state.
#[cfg(test)]
struct EnvGuard {
saved: Vec<(String, Option<String>)>,
}

#[cfg(test)]
impl EnvGuard {
fn set(key: &str, value: &str) -> Self {
let saved = env::var(key).ok();
env::set_var(key, value);
Self {
saved: vec![(key.to_string(), saved)],
}
}
fn remove(keys: &[&str]) -> Self {
let mut saved = Vec::with_capacity(keys.len());
for key in keys {
let key = (*key).to_string();
let val = env::var(&key).ok();
env::remove_var(&key);
saved.push((key, val));
}
Self { saved }
}
}

#[cfg(test)]
impl Drop for EnvGuard {
fn drop(&mut self) {
for (key, value) in &self.saved {
if let Some(ref v) = value {
env::set_var(key, v);
} else {
env::remove_var(key);
}
}
}
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions aws-creds/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pub enum CredentialsError {
NotEc2,
#[error("Not a container")]
NotContainer,
#[cfg(feature = "http-credentials")]
#[error("invalid AWS_CONTAINER_CREDENTIALS_FULL_URI for authorization token: {0}")]
InvalidContainerCredentialsFullUri(String),
#[error("Config not found")]
ConfigNotFound,
#[error("Missing aws_access_key_id section in config")]
Expand Down