Skip to content

Commit 00bd927

Browse files
nipunn1313Heath Hopkins
authored andcommitted
convex-backend PR 187: Refactor S3 credential handling in aws_utils (#40594)
Refactor S3 credential handling in aws_utils to allow S3 credentials from sources other than environment variables by using the AWS default credential chain. Preserved credential checking before first use to prevent logs from filling up. Also added `aws-credential-types` and `tokio` to dependencies in Cargo.toml and aws_utils/Cargo.toml This PR preserves the same S3 client environment variable credential checks while allowing credential sources from IAM roles which is useful for local debugging (`aws sso`) and self-hosting on AWS (EC2 IAM role attachment). The AWS S3 Rust SDK uses this default credential chain: `env vars -> shared config/credentials (incl. SSO) -> web identity -> container creds -> EC2 IMDSv2`. The new code checks the credential sources and bails with an error if none are found. Closes #185 ---- By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Heath Hopkins <heath.hopkins@emory.edu> GitOrigin-RevId: 2aa20649ba78df338cfae387a99f3e581f1d8e72
1 parent 2c01439 commit 00bd927

File tree

6 files changed

+179
-23
lines changed

6 files changed

+179
-23
lines changed

Cargo.lock

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,12 @@ async-trait = "0.1"
7676
async_zip = { version = "0.0.17", default-features = false, features = [ "deflate", "tokio", "zstd" ] }
7777
async_zip_0_0_9 = { package = "async_zip", version = "0.0.9", default-features = false, features = [ "zstd", "deflate" ] }
7878
atomic_refcell = "0.1.13"
79-
aws-config = { version = "1.6", default-features = false, features = [ "client-hyper", "default-https-client", "rustls", "rt-tokio" ] }
79+
aws-config = { version = "1.6", default-features = false, features = [ "client-hyper", "default-https-client", "rustls", "rt-tokio", "sso" ] }
8080
aws-lc-rs = { version = "1.13", default-features = false, features = [ "aws-lc-sys", "prebuilt-nasm" ] }
8181
aws-sdk-s3 = { version = "1.83", default-features = false, features = [ "default-https-client", "rt-tokio", "sigv4a" ] }
8282
aws-smithy-http = "0.62.0"
8383
aws-smithy-types-convert = { version = "0.60", features = [ "convert-streams" ] }
84+
aws-credential-types = "1"
8485
aws-types = "1"
8586
axum = { version = "0.8", features = [ "ws", "original-uri", "macros", "multipart" ] }
8687
axum-extra = { version = "0.10", features = [ "typed-header", "cookie" ] }

crates/aws_utils/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ license = "LicenseRef-FSL-1.1-Apache-2.0"
77
[dependencies]
88
anyhow = { workspace = true }
99
aws-config = { workspace = true }
10+
aws-credential-types = { workspace = true }
1011
aws-sdk-s3 = { workspace = true }
1112
aws-smithy-types-convert = { workspace = true }
1213
aws-types = { workspace = true }
1314
futures = { workspace = true }
15+
tokio = { workspace = true }
1416
tracing = { workspace = true }
1517

1618
[lints]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use std::env;
2+
3+
use anyhow::{
4+
Context,
5+
Result,
6+
};
7+
use aws_config::BehaviorVersion;
8+
use aws_sdk_s3 as s3;
9+
use aws_utils::preflight_credentials;
10+
11+
#[tokio::main]
12+
async fn main() -> Result<()> {
13+
// 1) Preflight: try to resolve credentials using the standard chain.
14+
let _creds = preflight_credentials().await?;
15+
16+
println!(
17+
"✅ Credentials resolved{}",
18+
match env::var("AWS_PROFILE") {
19+
Ok(p) => format!(" (AWS_PROFILE={p})"),
20+
Err(_) => String::new(),
21+
}
22+
);
23+
24+
// 2) Load full config explicitly setting profile if available
25+
let mut config_loader = aws_config::defaults(BehaviorVersion::latest());
26+
if let Ok(profile) = env::var("AWS_PROFILE") {
27+
config_loader = config_loader.profile_name(&profile);
28+
}
29+
let conf = config_loader.load().await;
30+
31+
// 3) Use S3 client safely now that we know creds exist.
32+
let client = s3::Client::new(&conf);
33+
34+
// Example: list buckets
35+
println!("Testing S3 access by listing buckets...");
36+
let resp = client.list_buckets().send().await.context(
37+
"S3 call failed (credentials may be invalid/expired or region/network misconfigured)",
38+
)?;
39+
40+
println!("✅ S3 access successful!");
41+
println!("Buckets:");
42+
let buckets = resp.buckets();
43+
if buckets.is_empty() {
44+
println!(" (no buckets found)");
45+
} else {
46+
for b in buckets {
47+
println!(" - {}", b.name().unwrap_or("<unnamed>"));
48+
}
49+
}
50+
51+
Ok(())
52+
}

crates/aws_utils/src/lib.rs

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
#![feature(coroutines)]
2-
#![feature(exit_status_error)]
1+
// #![feature(coroutines)]
2+
// #![feature(exit_status_error)]
33
use std::{
44
env,
55
sync::LazyLock,
66
};
77

88
use aws_config::{
9-
environment::credentials::EnvironmentVariableCredentialsProvider,
9+
default_provider::credentials::DefaultCredentialsChain,
1010
BehaviorVersion,
1111
ConfigLoader,
1212
};
13+
use aws_credential_types::provider::ProvideCredentials;
1314
use aws_sdk_s3::config::Builder as S3ConfigBuilder;
1415
use aws_types::region::Region;
1516

@@ -18,12 +19,6 @@ pub mod s3;
1819
static S3_ENDPOINT_URL: LazyLock<Option<String>> =
1920
LazyLock::new(|| env::var("S3_ENDPOINT_URL").ok());
2021

21-
static AWS_ACCESS_KEY_ID: LazyLock<Option<String>> =
22-
LazyLock::new(|| env::var("AWS_ACCESS_KEY_ID").ok());
23-
24-
static AWS_SECRET_ACCESS_KEY: LazyLock<Option<String>> =
25-
LazyLock::new(|| env::var("AWS_SECRET_ACCESS_KEY").ok());
26-
2722
static AWS_REGION: LazyLock<Option<String>> = LazyLock::new(|| env::var("AWS_REGION").ok());
2823

2924
static AWS_S3_FORCE_PATH_STYLE: LazyLock<bool> = LazyLock::new(|| {
@@ -50,25 +45,20 @@ static AWS_S3_DISABLE_CHECKSUMS: LazyLock<bool> = LazyLock::new(|| {
5045
/// Similar aws_config::from_env but returns an error if credentials or
5146
/// region is are not. It also doesn't spew out log lines every time
5247
/// credentials are accessed.
53-
pub fn must_config_from_env() -> anyhow::Result<ConfigLoader> {
48+
pub async fn must_config_from_env() -> anyhow::Result<ConfigLoader> {
5449
let Some(region) = AWS_REGION.clone() else {
5550
anyhow::bail!("AWS_REGION env variable must be set");
5651
};
5752
let region = Region::new(region);
58-
let Some(_) = AWS_ACCESS_KEY_ID.clone() else {
59-
anyhow::bail!("AWS_ACCESS_KEY_ID env variable must be set");
60-
};
61-
let Some(_) = AWS_SECRET_ACCESS_KEY.clone() else {
62-
anyhow::bail!("AWS_SECRET_ACCESS_KEY env variable must be set");
63-
};
64-
let credentials = EnvironmentVariableCredentialsProvider::new();
65-
Ok(aws_config::defaults(BehaviorVersion::v2025_01_17())
66-
.region(region)
67-
.credentials_provider(credentials))
53+
54+
// Check for credentials using the default provider chain
55+
let _creds = preflight_credentials().await?;
56+
57+
Ok(aws_config::defaults(BehaviorVersion::v2025_01_17()).region(region))
6858
}
6959

7060
pub async fn must_s3_config_from_env() -> anyhow::Result<S3ConfigBuilder> {
71-
let base_config = must_config_from_env()?.load().await;
61+
let base_config = must_config_from_env().await?.load().await;
7262
let mut s3_config_builder = S3ConfigBuilder::from(&base_config);
7363
if let Some(s3_endpoint_url) = S3_ENDPOINT_URL.clone() {
7464
s3_config_builder = s3_config_builder.endpoint_url(s3_endpoint_url);
@@ -77,6 +67,64 @@ pub async fn must_s3_config_from_env() -> anyhow::Result<S3ConfigBuilder> {
7767
Ok(s3_config_builder)
7868
}
7969

70+
/// Attempts to resolve credentials using the default chain:
71+
/// env vars -> shared config/credentials (incl. SSO) -> web identity ->
72+
/// container creds -> EC2 IMDSv2. Returns early with a helpful error if nothing
73+
/// is available.
74+
pub async fn preflight_credentials() -> anyhow::Result<aws_credential_types::Credentials> {
75+
let chain = DefaultCredentialsChain::builder().build().await;
76+
77+
match chain.provide_credentials().await {
78+
Ok(creds) => Ok(creds),
79+
Err(err) => {
80+
// Give actionable hints based on common setups.
81+
let profile = env::var("AWS_PROFILE").unwrap_or_else(|_| "default".to_string());
82+
let mut help = String::new();
83+
help.push_str("No AWS credentials were found by the default provider chain.\n\n");
84+
help.push_str("Tried in this order:\n");
85+
help.push_str(
86+
" 1) Environment: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY [/ \
87+
AWS_SESSION_TOKEN]\n",
88+
);
89+
help.push_str(
90+
" 2) Shared config/credentials files (~/.aws/config, ~/.aws/credentials) ",
91+
);
92+
help.push_str(&format!("(profile: {profile})\n"));
93+
help.push_str(" - If you use IAM Identity Center (SSO), run: aws sso login");
94+
if profile != "default" {
95+
help.push_str(&format!(" --profile {profile}"));
96+
}
97+
help.push('\n');
98+
help.push_str(
99+
" 3) Web identity (AssumeRoleWithWebIdentity; env/profiles with role_arn & \
100+
web_identity_token_file)\n",
101+
);
102+
help.push_str(
103+
" 4) Container credentials (ECS/EKS env: AWS_CONTAINER_CREDENTIALS_* or Pod \
104+
Identity)\n",
105+
);
106+
help.push_str(" 5) EC2 Instance Metadata (IMDSv2; instance role)\n\n");
107+
108+
help.push_str("Fixes:\n");
109+
help.push_str(
110+
" • For access keys: set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and \
111+
AWS_SESSION_TOKEN (optional)\n",
112+
);
113+
help.push_str(
114+
" • For profiles: set AWS_PROFILE or add a [profile] with credentials in \
115+
~/.aws/credentials\n",
116+
);
117+
help.push_str(" • For SSO: aws configure sso && aws sso login\n");
118+
help.push_str(
119+
" • For web identity: ensure web_identity_token_file and role_arn are set\n",
120+
);
121+
help.push_str(" • For containers/EC2: attach the proper task/IRSA/instance role\n");
122+
123+
anyhow::bail!("{}Underlying error: {}", help, err)
124+
},
125+
}
126+
}
127+
80128
/// Returns true if server-side encryption headers should be disabled
81129
pub fn is_sse_disabled() -> bool {
82130
*AWS_S3_DISABLE_SSE

crates/aws_utils/src/s3.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ impl S3Client {
3333
};
3434
let config = must_s3_config_from_env()
3535
.await
36-
.context("AWS env variables are required when using AWS Lambda")?
36+
.context(
37+
"Failed to create S3 configuration. Check AWS env variables or IAM permissions.",
38+
)?
3739
.retry_config(retry_config)
3840
.build();
3941

0 commit comments

Comments
 (0)