1use anyhow::Result;
2use aws_config::Region;
3use aws_sdk_s3::presigning::PresigningConfig;
4use aws_sdk_s3::Client;
5use std::time::Duration;
6use stormchaser_model::storage::StorageBackend;
7
8pub async fn get_s3_client(backend: &StorageBackend) -> Result<Client> {
10 let config = &backend.config;
11 let endpoint = config["endpoint"].as_str();
12 let region = config["region"].as_str().unwrap_or("us-east-1");
13 let access_key = config["access_key"].as_str();
14 let secret_key = config["secret_key"].as_str();
15
16 let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest())
17 .region(Region::new(region.to_string()));
18
19 if backend.ca_cert.is_some() || (backend.client_cert.is_some() && backend.client_key.is_some())
21 {
22 let _tls_config = build_client_config(
23 backend.ca_cert.as_deref(),
24 backend.client_cert.as_deref(),
25 backend.client_key.as_deref(),
26 )?;
27
28 tracing::info!("Configured mTLS for S3 backend: {}", backend.name);
31 }
32
33 if let (Some(ak), Some(sk)) = (access_key, secret_key) {
34 loader = loader.credentials_provider(aws_sdk_s3::config::Credentials::new(
35 ak, sk, None, None, "manual",
36 ));
37 }
38
39 let mut sdk_config_builder = loader.load().await.into_builder();
40 if let Some(ep) = endpoint {
41 sdk_config_builder = sdk_config_builder.endpoint_url(ep);
42 }
43
44 let sdk_config = sdk_config_builder.build();
45
46 #[cfg(feature = "aws-s3-sts")]
47 let sdk_config = if let Some(role_arn) = &backend.aws_assume_role_arn {
48 let sts_client = aws_sdk_sts::Client::new(&sdk_config);
49 let assume_role_res = sts_client
50 .assume_role()
51 .role_arn(role_arn)
52 .role_session_name("StormchaserS3Backend")
53 .send()
54 .await?;
55
56 if let Some(credentials) = assume_role_res.credentials() {
57 let provider = aws_sdk_s3::config::Credentials::new(
58 credentials.access_key_id(),
59 credentials.secret_access_key(),
60 Some(credentials.session_token().to_string()),
61 None,
62 "StsAssumedRole",
63 );
64 let shared_provider = aws_sdk_s3::config::SharedCredentialsProvider::new(provider);
65 sdk_config
66 .into_builder()
67 .credentials_provider(shared_provider)
68 .build()
69 } else {
70 return Err(anyhow::anyhow!("Missing credentials from assume_role"));
71 }
72 } else {
73 sdk_config
74 };
75
76 let mut s3_config_builder = aws_sdk_s3::config::Builder::from(&sdk_config);
77
78 if config["force_path_style"].as_bool().unwrap_or(false) {
80 s3_config_builder = s3_config_builder.force_path_style(true);
81 }
82
83 Ok(Client::from_conf(s3_config_builder.build()))
84}
85
86pub async fn generate_presigned_url(
88 client: &Client,
89 bucket: &str,
90 key: &str,
91 is_upload: bool,
92 expires_in: Duration,
93) -> Result<String> {
94 let presigning_config = PresigningConfig::builder().expires_in(expires_in).build()?;
95
96 let url = if is_upload {
97 client
98 .put_object()
99 .bucket(bucket)
100 .key(key)
101 .presigned(presigning_config)
102 .await?
103 } else {
104 client
105 .get_object()
106 .bucket(bucket)
107 .key(key)
108 .presigned(presigning_config)
109 .await?
110 };
111
112 Ok(url.uri().to_string())
113}
114
115use stormchaser_tls::build_client_config;
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use serde_json::json;
121 use stormchaser_model::storage::BackendType;
122 use stormchaser_model::BackendId;
123
124 #[tokio::test]
125 async fn test_get_s3_client_config_parsing() {
126 let backend = StorageBackend {
127 id: BackendId::new_v4(),
128 name: "test-s3".to_string(),
129 description: None,
130 backend_type: BackendType::S3,
131 config: json!({
132 "bucket": "my-bucket",
133 "endpoint": "http://localhost:9000",
134 "region": "us-east-1",
135 "access_key": "test",
136 "secret_key": "test",
137 "force_path_style": true
138 }),
139 aws_assume_role_arn: None,
140 is_default_sfs: true,
141 ca_cert: None,
142 client_cert: None,
143 client_key: None,
144 created_at: chrono::Utc::now(),
145 updated_at: chrono::Utc::now(),
146 };
147
148 let client = get_s3_client(&backend).await;
149 assert!(client.is_ok());
150 }
151}