1use 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(®ion_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}