Skip to main content

aliyun_oss/operations/
bucket_encryption.rs

1//! Bucket server-side encryption operations.
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::bucket::BucketName;
11
12#[derive(Debug, Clone)]
13pub struct ServerSideEncryptionConfiguration {
14    pub sse_algorithm: String,
15    pub kms_master_key_id: Option<String>,
16}
17
18#[derive(Debug, Clone, Serialize)]
19#[serde(rename = "ServerSideEncryptionRule")]
20struct SSEConfig {
21    #[serde(rename = "ApplyServerSideEncryptionByDefault")]
22    apply: SSEApply,
23}
24
25#[derive(Debug, Clone, Serialize)]
26struct SSEApply {
27    #[serde(rename = "SSEAlgorithm")]
28    sse_algorithm: String,
29    #[serde(rename = "KMSMasterKeyID", skip_serializing_if = "Option::is_none")]
30    kms_master_key_id: Option<String>,
31}
32
33#[derive(Debug, Clone, Deserialize)]
34#[serde(rename = "ServerSideEncryptionRule")]
35struct SSEConfigResponse {
36    #[serde(rename = "ApplyServerSideEncryptionByDefault")]
37    apply: SSEApplyResponse,
38}
39
40#[derive(Debug, Clone, Deserialize)]
41struct SSEApplyResponse {
42    #[serde(rename = "SSEAlgorithm")]
43    sse_algorithm: String,
44    #[serde(rename = "KMSMasterKeyID", default)]
45    kms_master_key_id: Option<String>,
46}
47
48pub struct PutBucketEncryptionBuilder {
49    client: Arc<OSSClientInner>,
50    bucket: BucketName,
51    config: ServerSideEncryptionConfiguration,
52}
53
54impl PutBucketEncryptionBuilder {
55    pub(crate) fn new(
56        client: Arc<OSSClientInner>,
57        bucket: BucketName,
58        config: ServerSideEncryptionConfiguration,
59    ) -> Self {
60        Self {
61            client,
62            bucket,
63            config,
64        }
65    }
66
67    pub async fn send(self) -> Result<PutBucketEncryptionOutput> {
68        let endpoint = self.client.endpoint.clone();
69        let uri = format!("https://{}.{}?encryption", self.bucket.as_str(), endpoint);
70
71        let query_params: Vec<(String, String)> = vec![("encryption".into(), String::new())];
72
73        let config = SSEConfig {
74            apply: SSEApply {
75                sse_algorithm: self.config.sse_algorithm,
76                kms_master_key_id: self.config.kms_master_key_id,
77            },
78        };
79
80        let body_xml = crate::util::xml::to_xml(&config)?;
81
82        let request = HttpRequest::builder()
83            .method(http::Method::PUT)
84            .uri(&uri)
85            .body(bytes::Bytes::from(body_xml))
86            .build();
87
88        let response = self
89            .client
90            .send_signed(request, Some(&self.bucket), query_params)
91            .await
92            .map_err(|e| OssError {
93                kind: OssErrorKind::TransportError,
94                context: Box::new(ErrorContext {
95                    operation: Some("PutBucketEncryption".into()),
96                    bucket: Some(self.bucket.to_string()),
97                    endpoint: Some(endpoint),
98                    ..Default::default()
99                }),
100                source: Some(Box::new(e)),
101            })?;
102
103        if response.status().is_success() {
104            Ok(PutBucketEncryptionOutput {
105                request_id: response
106                    .headers
107                    .get("x-oss-request-id")
108                    .and_then(|v| v.to_str().ok())
109                    .unwrap_or("")
110                    .to_string(),
111            })
112        } else {
113            Err(OssError {
114                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
115                    status_code: response.status().as_u16(),
116                    code: String::new(),
117                    message: String::new(),
118                    request_id: String::new(),
119                    host_id: String::new(),
120                    resource: Some(self.bucket.to_string()),
121                    string_to_sign: None,
122                })),
123                context: Box::new(ErrorContext {
124                    operation: Some("PutBucketEncryption".into()),
125                    bucket: Some(self.bucket.to_string()),
126                    ..Default::default()
127                }),
128                source: None,
129            })
130        }
131    }
132}
133
134#[derive(Debug, Clone)]
135pub struct PutBucketEncryptionOutput {
136    pub request_id: String,
137}
138
139pub struct GetBucketEncryptionBuilder {
140    client: Arc<OSSClientInner>,
141    bucket: BucketName,
142}
143
144impl GetBucketEncryptionBuilder {
145    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
146        Self { client, bucket }
147    }
148
149    pub async fn send(self) -> Result<GetBucketEncryptionOutput> {
150        let endpoint = self.client.endpoint.clone();
151        let uri = format!("https://{}.{}?encryption", self.bucket.as_str(), endpoint);
152
153        let query_params: Vec<(String, String)> = vec![("encryption".into(), String::new())];
154
155        let request = HttpRequest::builder()
156            .method(http::Method::GET)
157            .uri(&uri)
158            .build();
159
160        let response = self
161            .client
162            .send_signed(request, Some(&self.bucket), query_params)
163            .await
164            .map_err(|e| OssError {
165                kind: OssErrorKind::TransportError,
166                context: Box::new(ErrorContext {
167                    operation: Some("GetBucketEncryption".into()),
168                    bucket: Some(self.bucket.to_string()),
169                    endpoint: Some(endpoint),
170                    ..Default::default()
171                }),
172                source: Some(Box::new(e)),
173            })?;
174
175        if response.is_success() {
176            let body_str = response.body_as_str().unwrap_or("");
177            let config: SSEConfigResponse =
178                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
179                    kind: OssErrorKind::DeserializationError,
180                    context: Box::new(ErrorContext {
181                        operation: Some("GetBucketEncryption: parse XML".into()),
182                        bucket: Some(self.bucket.to_string()),
183                        ..Default::default()
184                    }),
185                    source: Some(Box::new(e)),
186                })?;
187
188            Ok(GetBucketEncryptionOutput {
189                sse_algorithm: config.apply.sse_algorithm,
190                kms_master_key_id: config.apply.kms_master_key_id,
191            })
192        } else {
193            Err(OssError {
194                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
195                    status_code: response.status().as_u16(),
196                    code: String::new(),
197                    message: String::new(),
198                    request_id: String::new(),
199                    host_id: String::new(),
200                    resource: Some(self.bucket.to_string()),
201                    string_to_sign: None,
202                })),
203                context: Box::new(ErrorContext {
204                    operation: Some("GetBucketEncryption".into()),
205                    bucket: Some(self.bucket.to_string()),
206                    ..Default::default()
207                }),
208                source: None,
209            })
210        }
211    }
212}
213
214#[derive(Debug, Clone)]
215pub struct GetBucketEncryptionOutput {
216    pub sse_algorithm: String,
217    pub kms_master_key_id: Option<String>,
218}
219
220pub struct DeleteBucketEncryptionBuilder {
221    client: Arc<OSSClientInner>,
222    bucket: BucketName,
223}
224
225impl DeleteBucketEncryptionBuilder {
226    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
227        Self { client, bucket }
228    }
229
230    pub async fn send(self) -> Result<DeleteBucketEncryptionOutput> {
231        let endpoint = self.client.endpoint.clone();
232        let uri = format!("https://{}.{}?encryption", self.bucket.as_str(), endpoint);
233
234        let query_params: Vec<(String, String)> = vec![("encryption".into(), String::new())];
235
236        let request = HttpRequest::builder()
237            .method(http::Method::DELETE)
238            .uri(&uri)
239            .build();
240
241        let response = self
242            .client
243            .send_signed(request, Some(&self.bucket), query_params)
244            .await
245            .map_err(|e| OssError {
246                kind: OssErrorKind::TransportError,
247                context: Box::new(ErrorContext {
248                    operation: Some("DeleteBucketEncryption".into()),
249                    bucket: Some(self.bucket.to_string()),
250                    endpoint: Some(endpoint),
251                    ..Default::default()
252                }),
253                source: Some(Box::new(e)),
254            })?;
255
256        if response.status().is_success() {
257            Ok(DeleteBucketEncryptionOutput {
258                request_id: response
259                    .headers
260                    .get("x-oss-request-id")
261                    .and_then(|v| v.to_str().ok())
262                    .unwrap_or("")
263                    .to_string(),
264            })
265        } else {
266            Err(OssError {
267                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
268                    status_code: response.status().as_u16(),
269                    code: String::new(),
270                    message: String::new(),
271                    request_id: String::new(),
272                    host_id: String::new(),
273                    resource: Some(self.bucket.to_string()),
274                    string_to_sign: None,
275                })),
276                context: Box::new(ErrorContext {
277                    operation: Some("DeleteBucketEncryption".into()),
278                    bucket: Some(self.bucket.to_string()),
279                    ..Default::default()
280                }),
281                source: None,
282            })
283        }
284    }
285}
286
287#[derive(Debug, Clone)]
288pub struct DeleteBucketEncryptionOutput {
289    pub request_id: String,
290}
291
292impl BucketOperations {
293    pub fn put_encryption(
294        &self,
295        config: ServerSideEncryptionConfiguration,
296    ) -> PutBucketEncryptionBuilder {
297        PutBucketEncryptionBuilder::new(
298            self.client_inner().clone(),
299            self.bucket_name().clone(),
300            config,
301        )
302    }
303
304    pub fn get_encryption(&self) -> GetBucketEncryptionBuilder {
305        GetBucketEncryptionBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
306    }
307
308    pub fn delete_encryption(&self) -> DeleteBucketEncryptionBuilder {
309        DeleteBucketEncryptionBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use std::sync::Mutex;
316
317    use crate::client::OSSClientInner;
318    use crate::config::credentials::Credentials;
319    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
320    use crate::types::region::Region;
321
322    use super::*;
323
324    struct RecordingHttpClient {
325        requests: Arc<Mutex<Vec<HttpRequest>>>,
326        status_code: http::StatusCode,
327        response_body: bytes::Bytes,
328    }
329
330    #[async_trait::async_trait]
331    impl HttpClient for RecordingHttpClient {
332        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
333            self.requests.lock().unwrap().push(request);
334            let mut headers = http::HeaderMap::new();
335            headers.insert(
336                "x-oss-request-id",
337                http::HeaderValue::from_static("rid-encryption"),
338            );
339            Ok(HttpResponse {
340                status: self.status_code,
341                headers,
342                body: self.response_body.clone(),
343            })
344        }
345    }
346
347    fn create_test_inner_with_body(
348        status: http::StatusCode,
349        body: bytes::Bytes,
350    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
351        let requests = Arc::new(Mutex::new(Vec::new()));
352        let http = Arc::new(RecordingHttpClient {
353            requests: requests.clone(),
354            status_code: status,
355            response_body: body,
356        });
357        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
358            Credentials::builder()
359                .access_key_id("test-ak")
360                .access_key_secret("test-sk")
361                .build()
362                .unwrap(),
363        ));
364        let inner = Arc::new(OSSClientInner {
365            http,
366            credentials,
367            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
368            region: Region::CnHangzhou,
369            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
370        });
371        (inner, requests)
372    }
373
374    #[test]
375    fn encryption_xml_aes256() {
376        let config = SSEConfig {
377            apply: SSEApply {
378                sse_algorithm: "AES256".into(),
379                kms_master_key_id: None,
380            },
381        };
382        let xml = crate::util::xml::to_xml(&config).unwrap();
383        assert!(xml.contains("<SSEAlgorithm>AES256</SSEAlgorithm>"));
384        assert!(!xml.contains("KMSMasterKeyID"));
385    }
386
387    #[test]
388    fn encryption_xml_kms() {
389        let config = SSEConfig {
390            apply: SSEApply {
391                sse_algorithm: "KMS".into(),
392                kms_master_key_id: Some("key-id-123".into()),
393            },
394        };
395        let xml = crate::util::xml::to_xml(&config).unwrap();
396        assert!(xml.contains("<SSEAlgorithm>KMS</SSEAlgorithm>"));
397        assert!(xml.contains("<KMSMasterKeyID>key-id-123</KMSMasterKeyID>"));
398    }
399
400    #[tokio::test]
401    async fn get_bucket_encryption_parses_xml() {
402        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
403<ServerSideEncryptionRule>
404  <ApplyServerSideEncryptionByDefault>
405    <SSEAlgorithm>KMS</SSEAlgorithm>
406    <KMSMasterKeyID>key-id</KMSMasterKeyID>
407  </ApplyServerSideEncryptionByDefault>
408</ServerSideEncryptionRule>"#;
409        let (inner, _) = create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::from(xml));
410        let builder =
411            GetBucketEncryptionBuilder::new(inner, BucketName::new("test-bucket").unwrap());
412        let output = builder.send().await.unwrap();
413        assert_eq!(output.sse_algorithm, "KMS");
414        assert_eq!(output.kms_master_key_id.as_deref(), Some("key-id"));
415    }
416
417    #[tokio::test]
418    async fn delete_bucket_encryption_sends_delete_request() {
419        let (inner, requests) =
420            create_test_inner_with_body(http::StatusCode::NO_CONTENT, bytes::Bytes::new());
421        let builder =
422            DeleteBucketEncryptionBuilder::new(inner, BucketName::new("test-bucket").unwrap());
423        builder.send().await.unwrap();
424        let captured = requests.lock().unwrap();
425        assert_eq!(captured[0].method, http::Method::DELETE);
426    }
427}