Skip to main content

rustack_s3_core/ops/
object_config.rs

1//! Object configuration operation handlers.
2//!
3//! Implements `get_object_tagging`, `put_object_tagging`, `delete_object_tagging`,
4//! `get_object_acl`, `put_object_acl`, `get_object_retention`,
5//! `put_object_retention`, `get_object_legal_hold`, `put_object_legal_hold`,
6//! and `get_object_attributes`.
7
8use rustack_s3_model::{
9    error::S3Error,
10    input::{
11        DeleteObjectTaggingInput, GetObjectAclInput, GetObjectAttributesInput,
12        GetObjectLegalHoldInput, GetObjectRetentionInput, GetObjectTaggingInput, PutObjectAclInput,
13        PutObjectLegalHoldInput, PutObjectRetentionInput, PutObjectTaggingInput,
14    },
15    output::{
16        DeleteObjectTaggingOutput, GetObjectAclOutput, GetObjectAttributesOutput,
17        GetObjectLegalHoldOutput, GetObjectRetentionOutput, GetObjectTaggingOutput,
18        PutObjectAclOutput, PutObjectLegalHoldOutput, PutObjectRetentionOutput,
19        PutObjectTaggingOutput,
20    },
21    types::{
22        Checksum, ChecksumType, GetObjectAttributesParts, Grant, Grantee, ObjectLockLegalHold,
23        ObjectLockLegalHoldStatus, ObjectLockRetention, ObjectLockRetentionMode, Permission,
24        StorageClass, Tag, Type,
25    },
26};
27use tracing::debug;
28
29use super::bucket::to_model_owner;
30use crate::{error::S3ServiceError, provider::RustackS3, state::object::CannedAcl};
31
32// AWS S3 DTOs use signed integers (i32/i64) for inherently non-negative values.
33// These handler methods must remain async for consistency.
34#[allow(
35    clippy::cast_possible_wrap,
36    clippy::cast_possible_truncation,
37    clippy::cast_sign_loss,
38    clippy::unused_async
39)]
40impl RustackS3 {
41    // -----------------------------------------------------------------------
42    // Object Tagging
43    // -----------------------------------------------------------------------
44
45    /// Get tags for an object.
46    pub async fn handle_get_object_tagging(
47        &self,
48        input: GetObjectTaggingInput,
49    ) -> Result<GetObjectTaggingOutput, S3Error> {
50        let bucket_name = input.bucket;
51        let key = input.key;
52
53        let bucket = self
54            .state
55            .get_bucket(&bucket_name)
56            .map_err(S3ServiceError::into_s3_error)?;
57
58        let store = bucket.objects.read();
59        let obj = if let Some(version_id) = &input.version_id {
60            store.get_version(&key, version_id).ok_or_else(|| {
61                S3ServiceError::NoSuchVersion {
62                    key: key.clone(),
63                    version_id: version_id.clone(),
64                }
65                .into_s3_error()
66            })?
67        } else {
68            store
69                .get(&key)
70                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
71        };
72
73        let tag_set: Vec<Tag> = obj
74            .metadata
75            .tagging
76            .iter()
77            .map(|(k, v)| Tag {
78                key: k.clone(),
79                value: v.clone(),
80            })
81            .collect();
82
83        let version_id = if obj.version_id == "null" {
84            None
85        } else {
86            Some(obj.version_id.clone())
87        };
88
89        Ok(GetObjectTaggingOutput {
90            tag_set,
91            version_id,
92        })
93    }
94
95    /// Set tags for an object.
96    pub async fn handle_put_object_tagging(
97        &self,
98        input: PutObjectTaggingInput,
99    ) -> Result<PutObjectTaggingOutput, S3Error> {
100        let bucket_name = input.bucket;
101        let key = input.key;
102
103        let bucket = self
104            .state
105            .get_bucket(&bucket_name)
106            .map_err(S3ServiceError::into_s3_error)?;
107
108        let tagging = input.tagging;
109
110        let tags: Vec<(String, String)> = tagging
111            .tag_set
112            .into_iter()
113            .map(|t| (t.key, t.value))
114            .collect();
115
116        crate::validation::validate_tags(&tags).map_err(S3ServiceError::into_s3_error)?;
117
118        // We need mutable access to the object. Since ObjectStore wraps objects
119        // immutably, we re-insert a modified copy.
120        let mut store = bucket.objects.write();
121        let obj = if let Some(version_id) = &input.version_id {
122            store.get_version(&key, version_id).ok_or_else(|| {
123                S3ServiceError::NoSuchVersion {
124                    key: key.clone(),
125                    version_id: version_id.clone(),
126                }
127                .into_s3_error()
128            })?
129        } else {
130            store
131                .get(&key)
132                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
133        };
134
135        let mut updated = obj.clone();
136        updated.metadata.tagging = tags;
137        store.put(updated);
138
139        debug!(bucket = %bucket_name, key = %key, "put_object_tagging completed");
140
141        let version_id_out = input.version_id;
142        Ok(PutObjectTaggingOutput {
143            version_id: version_id_out,
144        })
145    }
146
147    /// Delete tags for an object.
148    pub async fn handle_delete_object_tagging(
149        &self,
150        input: DeleteObjectTaggingInput,
151    ) -> Result<DeleteObjectTaggingOutput, S3Error> {
152        let bucket_name = input.bucket;
153        let key = input.key;
154
155        let bucket = self
156            .state
157            .get_bucket(&bucket_name)
158            .map_err(S3ServiceError::into_s3_error)?;
159
160        let mut store = bucket.objects.write();
161        let obj = if let Some(version_id) = &input.version_id {
162            store.get_version(&key, version_id).ok_or_else(|| {
163                S3ServiceError::NoSuchVersion {
164                    key: key.clone(),
165                    version_id: version_id.clone(),
166                }
167                .into_s3_error()
168            })?
169        } else {
170            store
171                .get(&key)
172                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
173        };
174
175        let mut updated = obj.clone();
176        updated.metadata.tagging = Vec::new();
177        store.put(updated);
178
179        debug!(bucket = %bucket_name, key = %key, "delete_object_tagging completed");
180
181        let version_id_out = input.version_id;
182        Ok(DeleteObjectTaggingOutput {
183            version_id: version_id_out,
184        })
185    }
186
187    // -----------------------------------------------------------------------
188    // Object ACL
189    // -----------------------------------------------------------------------
190
191    /// Get the ACL for an object.
192    pub async fn handle_get_object_acl(
193        &self,
194        input: GetObjectAclInput,
195    ) -> Result<GetObjectAclOutput, S3Error> {
196        let bucket_name = input.bucket;
197        let key = input.key;
198
199        let bucket = self
200            .state
201            .get_bucket(&bucket_name)
202            .map_err(S3ServiceError::into_s3_error)?;
203
204        let store = bucket.objects.read();
205        let obj = if let Some(version_id) = &input.version_id {
206            store.get_version(&key, version_id).ok_or_else(|| {
207                S3ServiceError::NoSuchVersion {
208                    key: key.clone(),
209                    version_id: version_id.clone(),
210                }
211                .into_s3_error()
212            })?
213        } else {
214            store
215                .get(&key)
216                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
217        };
218
219        let owner = to_model_owner(&obj.owner);
220
221        let grant = Grant {
222            grantee: Some(Grantee {
223                display_name: Some(obj.owner.display_name.clone()),
224                email_address: None,
225                id: Some(obj.owner.id.clone()),
226                r#type: Type::CanonicalUser,
227                uri: None,
228            }),
229            permission: Some(Permission::FullControl),
230        };
231
232        Ok(GetObjectAclOutput {
233            grants: vec![grant],
234            owner: Some(owner),
235            request_charged: None,
236        })
237    }
238
239    /// Set the ACL for an object.
240    pub async fn handle_put_object_acl(
241        &self,
242        input: PutObjectAclInput,
243    ) -> Result<PutObjectAclOutput, S3Error> {
244        let bucket_name = input.bucket;
245        let key = input.key;
246
247        let bucket = self
248            .state
249            .get_bucket(&bucket_name)
250            .map_err(S3ServiceError::into_s3_error)?;
251
252        if let Some(acl_enum) = input.acl {
253            let acl: CannedAcl = acl_enum
254                .as_str()
255                .parse()
256                .map_err(|_| S3Error::invalid_argument("Invalid canned ACL"))?;
257
258            let mut store = bucket.objects.write();
259            let obj = if let Some(version_id) = &input.version_id {
260                store.get_version(&key, version_id).ok_or_else(|| {
261                    S3ServiceError::NoSuchVersion {
262                        key: key.clone(),
263                        version_id: version_id.clone(),
264                    }
265                    .into_s3_error()
266                })?
267            } else {
268                store
269                    .get(&key)
270                    .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
271            };
272
273            let mut updated = obj.clone();
274            updated.metadata.acl = acl;
275            store.put(updated);
276        }
277
278        debug!(bucket = %bucket_name, key = %key, "put_object_acl completed");
279
280        Ok(PutObjectAclOutput {
281            request_charged: None,
282        })
283    }
284
285    // -----------------------------------------------------------------------
286    // Object Retention
287    // -----------------------------------------------------------------------
288
289    /// Get the retention configuration for an object.
290    pub async fn handle_get_object_retention(
291        &self,
292        input: GetObjectRetentionInput,
293    ) -> Result<GetObjectRetentionOutput, S3Error> {
294        let bucket_name = input.bucket;
295        let key = input.key;
296
297        let bucket = self
298            .state
299            .get_bucket(&bucket_name)
300            .map_err(S3ServiceError::into_s3_error)?;
301
302        let store = bucket.objects.read();
303        let obj = if let Some(version_id) = &input.version_id {
304            store.get_version(&key, version_id).ok_or_else(|| {
305                S3ServiceError::NoSuchVersion {
306                    key: key.clone(),
307                    version_id: version_id.clone(),
308                }
309                .into_s3_error()
310            })?
311        } else {
312            store
313                .get(&key)
314                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
315        };
316
317        let retention = match (
318            &obj.metadata.object_lock_mode,
319            obj.metadata.object_lock_retain_until,
320        ) {
321            (Some(mode), Some(until)) => Some(ObjectLockRetention {
322                mode: Some(ObjectLockRetentionMode::from(mode.as_str())),
323                retain_until_date: Some(until),
324            }),
325            _ => None,
326        };
327
328        if retention.is_none() {
329            return Err(S3Error::invalid_argument(
330                "No retention configuration found",
331            ));
332        }
333
334        Ok(GetObjectRetentionOutput { retention })
335    }
336
337    /// Set the retention configuration for an object.
338    pub async fn handle_put_object_retention(
339        &self,
340        input: PutObjectRetentionInput,
341    ) -> Result<PutObjectRetentionOutput, S3Error> {
342        let bucket_name = input.bucket;
343        let key = input.key;
344
345        let bucket = self
346            .state
347            .get_bucket(&bucket_name)
348            .map_err(S3ServiceError::into_s3_error)?;
349
350        let retention = input.retention;
351
352        let mut store = bucket.objects.write();
353
354        // First read existing retention metadata (immutable borrow).
355        {
356            let obj = if let Some(version_id) = &input.version_id {
357                store.get_version(&key, version_id).ok_or_else(|| {
358                    S3ServiceError::NoSuchVersion {
359                        key: key.clone(),
360                        version_id: version_id.clone(),
361                    }
362                    .into_s3_error()
363                })?
364            } else {
365                store
366                    .get(&key)
367                    .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
368            };
369
370            // Enforce Object Lock retention rules.
371            let existing_until = obj.metadata.object_lock_retain_until;
372            let existing_mode = obj.metadata.object_lock_mode.as_deref();
373            let bypass = input.bypass_governance_retention.unwrap_or(false);
374
375            if let Some(current_until) = existing_until {
376                let now = chrono::Utc::now();
377                if current_until > now {
378                    let new_mode = retention.as_ref().and_then(|r| r.mode.as_ref());
379                    let new_until = retention.as_ref().and_then(|r| r.retain_until_date);
380
381                    // COMPLIANCE mode: cannot change mode, cannot shorten, cannot remove.
382                    if existing_mode == Some("COMPLIANCE") {
383                        let mode_changed = new_mode.is_none_or(|m| m.as_str() != "COMPLIANCE");
384                        let is_shortening = match new_until {
385                            Some(new) => new < current_until,
386                            None => true,
387                        };
388                        if mode_changed || is_shortening {
389                            return Err(S3ServiceError::AccessDenied.into_s3_error());
390                        }
391                    } else {
392                        // GOVERNANCE mode: can be bypassed with the bypass flag.
393                        let is_shortening = match new_until {
394                            Some(new) => new < current_until,
395                            None => true,
396                        };
397                        if is_shortening && !bypass {
398                            return Err(S3ServiceError::AccessDenied.into_s3_error());
399                        }
400                    }
401                }
402            }
403        }
404
405        // Now mutate the version metadata in-place (mutable borrow).
406        let obj = if let Some(version_id) = &input.version_id {
407            store.get_version_mut(&key, version_id).ok_or_else(|| {
408                S3ServiceError::NoSuchVersion {
409                    key: key.clone(),
410                    version_id: version_id.clone(),
411                }
412                .into_s3_error()
413            })?
414        } else {
415            store
416                .get_mut(&key)
417                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
418        };
419
420        if let Some(ret) = retention {
421            obj.metadata.object_lock_mode = ret.mode.as_ref().map(|m| m.as_str().to_owned());
422            obj.metadata.object_lock_retain_until = ret.retain_until_date;
423        } else {
424            obj.metadata.object_lock_mode = None;
425            obj.metadata.object_lock_retain_until = None;
426        }
427
428        debug!(bucket = %bucket_name, key = %key, "put_object_retention completed");
429
430        Ok(PutObjectRetentionOutput {
431            request_charged: None,
432        })
433    }
434
435    // -----------------------------------------------------------------------
436    // Object Legal Hold
437    // -----------------------------------------------------------------------
438
439    /// Get the legal hold status for an object.
440    pub async fn handle_get_object_legal_hold(
441        &self,
442        input: GetObjectLegalHoldInput,
443    ) -> Result<GetObjectLegalHoldOutput, S3Error> {
444        let bucket_name = input.bucket;
445        let key = input.key;
446
447        let bucket = self
448            .state
449            .get_bucket(&bucket_name)
450            .map_err(S3ServiceError::into_s3_error)?;
451
452        if !*bucket.object_lock_enabled.read() {
453            return Err(S3ServiceError::ObjectLockConfigurationNotFoundError.into_s3_error());
454        }
455
456        let store = bucket.objects.read();
457        let obj = if let Some(version_id) = &input.version_id {
458            store.get_version(&key, version_id).ok_or_else(|| {
459                S3ServiceError::NoSuchVersion {
460                    key: key.clone(),
461                    version_id: version_id.clone(),
462                }
463                .into_s3_error()
464            })?
465        } else {
466            store
467                .get(&key)
468                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
469        };
470
471        let is_on = obj.metadata.object_lock_legal_hold.unwrap_or(false);
472        let status = if is_on {
473            ObjectLockLegalHoldStatus::On
474        } else {
475            ObjectLockLegalHoldStatus::Off
476        };
477
478        Ok(GetObjectLegalHoldOutput {
479            legal_hold: Some(ObjectLockLegalHold {
480                status: Some(status),
481            }),
482        })
483    }
484
485    /// Set the legal hold status for an object.
486    pub async fn handle_put_object_legal_hold(
487        &self,
488        input: PutObjectLegalHoldInput,
489    ) -> Result<PutObjectLegalHoldOutput, S3Error> {
490        let bucket_name = input.bucket;
491        let key = input.key;
492
493        let bucket = self
494            .state
495            .get_bucket(&bucket_name)
496            .map_err(S3ServiceError::into_s3_error)?;
497
498        if !*bucket.object_lock_enabled.read() {
499            return Err(S3ServiceError::ObjectLockConfigurationNotFoundError.into_s3_error());
500        }
501
502        let legal_hold = input.legal_hold;
503
504        // Reject requests with missing or empty LegalHold body.
505        let status = legal_hold.as_ref().and_then(|lh| lh.status.as_ref());
506        if status.is_none() {
507            return Err(S3Error::malformed_xml("Missing LegalHold status"));
508        }
509
510        let mut store = bucket.objects.write();
511
512        // Verify the object exists first.
513        if let Some(version_id) = &input.version_id {
514            store.get_version(&key, version_id).ok_or_else(|| {
515                S3ServiceError::NoSuchVersion {
516                    key: key.clone(),
517                    version_id: version_id.clone(),
518                }
519                .into_s3_error()
520            })?;
521        } else {
522            store
523                .get(&key)
524                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?;
525        }
526
527        // Update the legal hold status in-place (no new version created).
528        let new_hold = legal_hold
529            .and_then(|lh| lh.status)
530            .map(|s| s.as_str() == "ON");
531
532        let obj = if let Some(version_id) = &input.version_id {
533            store.get_version_mut(&key, version_id)
534        } else {
535            store.get_mut(&key)
536        }
537        .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?;
538
539        obj.metadata.object_lock_legal_hold = new_hold;
540
541        debug!(bucket = %bucket_name, key = %key, "put_object_legal_hold completed");
542
543        Ok(PutObjectLegalHoldOutput {
544            request_charged: None,
545        })
546    }
547
548    // -----------------------------------------------------------------------
549    // Get Object Attributes
550    // -----------------------------------------------------------------------
551
552    /// Get attributes for an object.
553    pub async fn handle_get_object_attributes(
554        &self,
555        input: GetObjectAttributesInput,
556    ) -> Result<GetObjectAttributesOutput, S3Error> {
557        let bucket_name = input.bucket;
558        let key = input.key;
559
560        let bucket = self
561            .state
562            .get_bucket(&bucket_name)
563            .map_err(S3ServiceError::into_s3_error)?;
564
565        let store = bucket.objects.read();
566        let obj = if let Some(version_id) = &input.version_id {
567            store.get_version(&key, version_id).ok_or_else(|| {
568                S3ServiceError::NoSuchVersion {
569                    key: key.clone(),
570                    version_id: version_id.clone(),
571                }
572                .into_s3_error()
573            })?
574        } else {
575            store
576                .get(&key)
577                .ok_or_else(|| S3ServiceError::NoSuchKey { key: key.clone() }.into_s3_error())?
578        };
579
580        let version_id = if obj.version_id == "null" {
581            None
582        } else {
583            Some(obj.version_id.clone())
584        };
585
586        let checksum = obj.checksum.as_ref().map(|c| {
587            let mut cksum = Checksum::default();
588            match c.algorithm.as_str() {
589                "CRC32" => cksum.checksum_crc32 = Some(c.value.clone()),
590                "CRC32C" => cksum.checksum_crc32c = Some(c.value.clone()),
591                "CRC64NVME" => cksum.checksum_crc64nvme = Some(c.value.clone()),
592                "SHA1" => cksum.checksum_sha1 = Some(c.value.clone()),
593                "SHA256" => cksum.checksum_sha256 = Some(c.value.clone()),
594                _ => {}
595            }
596            cksum.checksum_type = Some(match c.checksum_type.as_str() {
597                "COMPOSITE" => ChecksumType::Composite,
598                _ => ChecksumType::FullObject,
599            });
600            cksum
601        });
602
603        Ok(GetObjectAttributesOutput {
604            checksum,
605            delete_marker: None,
606            e_tag: Some(obj.etag.clone()),
607            last_modified: Some(obj.last_modified),
608            object_parts: obj.parts_count.map(|n| GetObjectAttributesParts {
609                is_truncated: None,
610                max_parts: None,
611                next_part_number_marker: None,
612                part_number_marker: None,
613                parts: Vec::new(),
614                total_parts_count: Some(n as i32),
615            }),
616            object_size: Some(obj.size as i64),
617            request_charged: None,
618            storage_class: Some(StorageClass::from(obj.storage_class.as_str())),
619            version_id,
620        })
621    }
622}