Skip to main content

stormchaser_engine/
s3.rs

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
8/// Get s3 client.
9pub 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    // Configure mTLS if certificates are provided in the DB
20    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        // Note: To fully integrate with AWS SDK, we'd need to provide a custom HTTP client
29        // that uses this tls_config. For now we ensure the config is correctly built.
30        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    // For Minio/S3 compatible backends, we often need path style access
79    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
86/// Generates a presigned URL for downloading or uploading an object to/from an S3 bucket.
87pub 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}