use anyhow::Result;
use aws_config::Region;
use aws_sdk_s3::presigning::PresigningConfig;
use aws_sdk_s3::Client;
use std::time::Duration;
use stormchaser_model::storage::StorageBackend;
pub async fn get_s3_client(backend: &StorageBackend) -> Result<Client> {
let config = &backend.config;
let endpoint = config["endpoint"].as_str();
let region = config["region"].as_str().unwrap_or("us-east-1");
let access_key = config["access_key"].as_str();
let secret_key = config["secret_key"].as_str();
let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(Region::new(region.to_string()));
if backend.ca_cert.is_some() || (backend.client_cert.is_some() && backend.client_key.is_some())
{
let _tls_config = build_client_config(
backend.ca_cert.as_deref(),
backend.client_cert.as_deref(),
backend.client_key.as_deref(),
)?;
tracing::info!("Configured mTLS for S3 backend: {}", backend.name);
}
if let (Some(ak), Some(sk)) = (access_key, secret_key) {
loader = loader.credentials_provider(aws_sdk_s3::config::Credentials::new(
ak, sk, None, None, "manual",
));
}
let mut sdk_config_builder = loader.load().await.into_builder();
if let Some(ep) = endpoint {
sdk_config_builder = sdk_config_builder.endpoint_url(ep);
}
let sdk_config = sdk_config_builder.build();
#[cfg(feature = "aws-s3-sts")]
let sdk_config = if let Some(role_arn) = &backend.aws_assume_role_arn {
let sts_client = aws_sdk_sts::Client::new(&sdk_config);
let assume_role_res = sts_client
.assume_role()
.role_arn(role_arn)
.role_session_name("StormchaserS3Backend")
.send()
.await?;
if let Some(credentials) = assume_role_res.credentials() {
let provider = aws_sdk_s3::config::Credentials::new(
credentials.access_key_id(),
credentials.secret_access_key(),
Some(credentials.session_token().to_string()),
None,
"StsAssumedRole",
);
let shared_provider = aws_sdk_s3::config::SharedCredentialsProvider::new(provider);
sdk_config
.into_builder()
.credentials_provider(shared_provider)
.build()
} else {
return Err(anyhow::anyhow!("Missing credentials from assume_role"));
}
} else {
sdk_config
};
let mut s3_config_builder = aws_sdk_s3::config::Builder::from(&sdk_config);
if config["force_path_style"].as_bool().unwrap_or(false) {
s3_config_builder = s3_config_builder.force_path_style(true);
}
Ok(Client::from_conf(s3_config_builder.build()))
}
pub async fn generate_presigned_url(
client: &Client,
bucket: &str,
key: &str,
is_upload: bool,
expires_in: Duration,
) -> Result<String> {
let presigning_config = PresigningConfig::builder().expires_in(expires_in).build()?;
let url = if is_upload {
client
.put_object()
.bucket(bucket)
.key(key)
.presigned(presigning_config)
.await?
} else {
client
.get_object()
.bucket(bucket)
.key(key)
.presigned(presigning_config)
.await?
};
Ok(url.uri().to_string())
}
use stormchaser_tls::build_client_config;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use stormchaser_model::storage::BackendType;
use stormchaser_model::BackendId;
#[tokio::test]
async fn test_get_s3_client_config_parsing() {
let backend = StorageBackend {
id: BackendId::new_v4(),
name: "test-s3".to_string(),
description: None,
backend_type: BackendType::S3,
config: json!({
"bucket": "my-bucket",
"endpoint": "http://localhost:9000",
"region": "us-east-1",
"access_key": "test",
"secret_key": "test",
"force_path_style": true
}),
aws_assume_role_arn: None,
is_default_sfs: true,
ca_cert: None,
client_cert: None,
client_key: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let client = get_s3_client(&backend).await;
assert!(client.is_ok());
}
}