Skip to main content

aliyun_oss/operations/
bucket_acl.rs

1//! Bucket ACL (Access Control List) operations.
2
3use std::sync::Arc;
4
5use serde::Deserialize;
6
7use crate::client::{BucketOperations, OSSClientInner};
8use crate::error::{ErrorContext, OssError, OssErrorKind, Result};
9use crate::http::client::HttpRequest;
10use crate::types::acl::BucketAcl;
11use crate::types::bucket::BucketName;
12use crate::types::response::OwnerInfo;
13
14pub struct PutBucketAclBuilder {
15    client: Arc<OSSClientInner>,
16    bucket: BucketName,
17    acl: BucketAcl,
18}
19
20impl PutBucketAclBuilder {
21    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName, acl: BucketAcl) -> Self {
22        Self {
23            client,
24            bucket,
25            acl,
26        }
27    }
28
29    pub async fn send(self) -> Result<PutBucketAclOutput> {
30        let endpoint = self.client.endpoint.clone();
31        let uri = format!("https://{}.{}?acl", self.bucket.as_str(), endpoint);
32
33        let query_params: Vec<(String, String)> = vec![("acl".into(), String::new())];
34
35        let request = HttpRequest::builder()
36            .method(http::Method::PUT)
37            .uri(&uri)
38            .header(
39                http::HeaderName::from_static("x-oss-acl"),
40                http::HeaderValue::from_str(self.acl.as_str()).map_err(|e| OssError {
41                    kind: OssErrorKind::ValidationError,
42                    context: Box::new(ErrorContext {
43                        operation: Some("set x-oss-acl header".into()),
44                        bucket: Some(self.bucket.to_string()),
45                        ..Default::default()
46                    }),
47                    source: Some(Box::new(e)),
48                })?,
49            )
50            .build();
51
52        let response = self
53            .client
54            .send_signed(request, Some(&self.bucket), query_params)
55            .await
56            .map_err(|e| OssError {
57                kind: OssErrorKind::TransportError,
58                context: Box::new(ErrorContext {
59                    operation: Some("PutBucketAcl".into()),
60                    bucket: Some(self.bucket.to_string()),
61                    endpoint: Some(endpoint),
62                    ..Default::default()
63                }),
64                source: Some(Box::new(e)),
65            })?;
66
67        if response.status().is_success() {
68            Ok(PutBucketAclOutput {
69                request_id: response
70                    .headers
71                    .get("x-oss-request-id")
72                    .and_then(|v| v.to_str().ok())
73                    .unwrap_or("")
74                    .to_string(),
75            })
76        } else {
77            Err(OssError {
78                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
79                    status_code: response.status().as_u16(),
80                    code: String::new(),
81                    message: String::new(),
82                    request_id: String::new(),
83                    host_id: String::new(),
84                    resource: Some(self.bucket.to_string()),
85                    string_to_sign: None,
86                })),
87                context: Box::new(ErrorContext {
88                    operation: Some("PutBucketAcl".into()),
89                    bucket: Some(self.bucket.to_string()),
90                    ..Default::default()
91                }),
92                source: None,
93            })
94        }
95    }
96}
97
98#[derive(Debug, Clone)]
99pub struct PutBucketAclOutput {
100    pub request_id: String,
101}
102
103pub struct GetBucketAclBuilder {
104    client: Arc<OSSClientInner>,
105    bucket: BucketName,
106}
107
108impl GetBucketAclBuilder {
109    pub(crate) fn new(client: Arc<OSSClientInner>, bucket: BucketName) -> Self {
110        Self { client, bucket }
111    }
112
113    pub async fn send(self) -> Result<GetBucketAclOutput> {
114        let endpoint = self.client.endpoint.clone();
115        let uri = format!("https://{}.{}?acl", self.bucket.as_str(), endpoint);
116
117        let query_params: Vec<(String, String)> = vec![("acl".into(), String::new())];
118
119        let request = HttpRequest::builder()
120            .method(http::Method::GET)
121            .uri(&uri)
122            .build();
123
124        let response = self
125            .client
126            .send_signed(request, Some(&self.bucket), query_params)
127            .await
128            .map_err(|e| OssError {
129                kind: OssErrorKind::TransportError,
130                context: Box::new(ErrorContext {
131                    operation: Some("GetBucketAcl".into()),
132                    bucket: Some(self.bucket.to_string()),
133                    endpoint: Some(endpoint),
134                    ..Default::default()
135                }),
136                source: Some(Box::new(e)),
137            })?;
138
139        if response.is_success() {
140            let body_str = response.body_as_str().unwrap_or("");
141            let policy: AccessControlPolicy =
142                crate::util::xml::from_xml(body_str).map_err(|e| OssError {
143                    kind: OssErrorKind::DeserializationError,
144                    context: Box::new(ErrorContext {
145                        operation: Some("GetBucketAcl: parse XML".into()),
146                        bucket: Some(self.bucket.to_string()),
147                        ..Default::default()
148                    }),
149                    source: Some(Box::new(e)),
150                })?;
151
152            Ok(GetBucketAclOutput {
153                owner: policy.owner,
154                grant: policy.acl.grant,
155            })
156        } else {
157            Err(OssError {
158                kind: OssErrorKind::ServiceError(Box::new(crate::error::OssServiceError {
159                    status_code: response.status().as_u16(),
160                    code: String::new(),
161                    message: String::new(),
162                    request_id: String::new(),
163                    host_id: String::new(),
164                    resource: Some(self.bucket.to_string()),
165                    string_to_sign: None,
166                })),
167                context: Box::new(ErrorContext {
168                    operation: Some("GetBucketAcl".into()),
169                    bucket: Some(self.bucket.to_string()),
170                    ..Default::default()
171                }),
172                source: None,
173            })
174        }
175    }
176}
177
178#[derive(Debug, Clone, Deserialize)]
179#[serde(rename = "AccessControlPolicy")]
180struct AccessControlPolicy {
181    #[serde(rename = "Owner")]
182    owner: OwnerInfo,
183    #[serde(rename = "AccessControlList")]
184    acl: AclGrant,
185}
186
187#[derive(Debug, Clone, Deserialize)]
188struct AclGrant {
189    #[serde(rename = "Grant")]
190    grant: String,
191}
192
193#[derive(Debug, Clone)]
194pub struct GetBucketAclOutput {
195    pub owner: OwnerInfo,
196    pub grant: String,
197}
198
199impl BucketOperations {
200    pub fn put_acl(&self, acl: BucketAcl) -> PutBucketAclBuilder {
201        PutBucketAclBuilder::new(self.client_inner().clone(), self.bucket_name().clone(), acl)
202    }
203
204    pub fn get_acl(&self) -> GetBucketAclBuilder {
205        GetBucketAclBuilder::new(self.client_inner().clone(), self.bucket_name().clone())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use std::str::FromStr;
212    use std::sync::Mutex;
213
214    use crate::client::OSSClientInner;
215    use crate::config::credentials::Credentials;
216    use crate::http::client::{HttpClient, HttpRequest, HttpResponse};
217    use crate::types::region::Region;
218
219    use super::*;
220
221    struct RecordingHttpClient {
222        requests: Arc<Mutex<Vec<HttpRequest>>>,
223        status_code: http::StatusCode,
224        response_body: bytes::Bytes,
225    }
226
227    #[async_trait::async_trait]
228    impl HttpClient for RecordingHttpClient {
229        async fn send(&self, request: HttpRequest) -> crate::error::Result<HttpResponse> {
230            self.requests.lock().unwrap().push(request);
231            let mut headers = http::HeaderMap::new();
232            headers.insert(
233                "x-oss-request-id",
234                http::HeaderValue::from_static("rid-acl"),
235            );
236            Ok(HttpResponse {
237                status: self.status_code,
238                headers,
239                body: self.response_body.clone(),
240            })
241        }
242    }
243
244    fn create_test_inner_with_body(
245        status: http::StatusCode,
246        body: bytes::Bytes,
247    ) -> (Arc<OSSClientInner>, Arc<Mutex<Vec<HttpRequest>>>) {
248        let requests = Arc::new(Mutex::new(Vec::new()));
249        let http = Arc::new(RecordingHttpClient {
250            requests: requests.clone(),
251            status_code: status,
252            response_body: body,
253        });
254        let credentials = Arc::new(crate::config::credentials::StaticCredentialsProvider::new(
255            Credentials::builder()
256                .access_key_id("test-ak")
257                .access_key_secret("test-sk")
258                .build()
259                .unwrap(),
260        ));
261        let inner = Arc::new(OSSClientInner {
262            http,
263            credentials,
264            signer: Arc::from(crate::signer::create_signer(crate::signer::SignVersion::V4)),
265            region: Region::CnHangzhou,
266            endpoint: "oss-cn-hangzhou.aliyuncs.com".into(),
267        });
268        (inner, requests)
269    }
270
271    #[tokio::test]
272    async fn put_bucket_acl_sets_acl_header() {
273        let (inner, requests) =
274            create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::new());
275        let builder = PutBucketAclBuilder::new(
276            inner,
277            BucketName::new("test-bucket").unwrap(),
278            BucketAcl::PublicRead,
279        );
280
281        builder.send().await.unwrap();
282        let captured = requests.lock().unwrap();
283        assert_eq!(captured[0].method, http::Method::PUT);
284        assert!(captured[0].uri.contains("?acl"));
285    }
286
287    #[tokio::test]
288    async fn get_bucket_acl_parses_xml_response() {
289        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
290<AccessControlPolicy>
291  <Owner><ID>owner-id</ID></Owner>
292  <AccessControlList>
293    <Grant>private</Grant>
294  </AccessControlList>
295</AccessControlPolicy>"#;
296        let (inner, _) = create_test_inner_with_body(http::StatusCode::OK, bytes::Bytes::from(xml));
297        let builder = GetBucketAclBuilder::new(inner, BucketName::new("test-bucket").unwrap());
298
299        let output = builder.send().await.unwrap();
300        assert_eq!(output.owner.id, "owner-id");
301        assert_eq!(output.grant, "private");
302    }
303
304    #[tokio::test]
305    #[ignore = "requires valid OSS credentials"]
306    async fn e2e_bucket_acl() {
307        let ak = std::env::var("OSS_ACCESS_KEY_ID").expect("OSS_ACCESS_KEY_ID not set");
308        let sk = std::env::var("OSS_ACCESS_KEY_SECRET").expect("OSS_ACCESS_KEY_SECRET not set");
309        let region_str = std::env::var("OSS_REGION").unwrap_or_else(|_| "cn-wulanchabu".into());
310        let bucket_str = std::env::var("OSS_BUCKET").expect("OSS_BUCKET not set");
311
312        let region = Region::from_str(&region_str).unwrap_or_else(|_| Region::Custom {
313            endpoint: format!("oss-{}.aliyuncs.com", region_str),
314            region_id: region_str.clone(),
315        });
316
317        let client = crate::client::OSSClient::builder()
318            .region(region)
319            .credentials(ak, sk)
320            .build()
321            .unwrap();
322
323        let output = client
324            .bucket(&bucket_str)
325            .unwrap()
326            .get_acl()
327            .send()
328            .await
329            .unwrap();
330
331        assert!(!output.grant.is_empty());
332        eprintln!(
333            "GetBucketAcl: grant={}, owner={}",
334            output.grant, output.owner.id
335        );
336    }
337}