rusty_cdk_core/s3/
builder.rs

1use crate::custom_resource::{BucketNotificationBuilder, BUCKET_NOTIFICATION_HANDLER_CODE};
2use crate::iam::{
3    CustomPermission, Effect, Permission, PolicyDocument, PolicyDocumentBuilder, Principal, PrincipalBuilder, ServicePrincipal,
4    StatementBuilder,
5};
6use crate::intrinsic::join;
7use crate::lambda::{Architecture, Runtime};
8use crate::lambda::{Code, FunctionBuilder, FunctionRef, PermissionBuilder};
9use crate::s3::dto;
10use crate::s3::{
11    Bucket, BucketEncryption, BucketPolicy, BucketPolicyRef, BucketProperties, BucketRef, CorsConfiguration, CorsRule,
12    LifecycleConfiguration, LifecycleRule, LifecycleRuleTransition, NonCurrentVersionTransition, PublicAccessBlockConfiguration,
13    RedirectAllRequestsTo, S3BucketPolicyProperties, ServerSideEncryptionByDefault, ServerSideEncryptionRule, WebsiteConfiguration,
14};
15use crate::shared::http::{HttpMethod, Protocol};
16use crate::shared::Id;
17use crate::sns::{TopicPolicyBuilder, TopicRef};
18use crate::stack::{Resource, StackBuilder};
19use crate::type_state;
20use crate::wrappers::{BucketName, IamAction, LambdaPermissionAction, LifecycleTransitionInDays, Memory, S3LifecycleObjectSizes, Timeout};
21use serde_json::{Map, Value};
22use std::marker::PhantomData;
23use std::time::Duration;
24use crate::sqs::{QueuePolicyBuilder, QueueRef};
25
26/// Builder for S3 bucket policies.
27///
28/// Creates a policy document that controls access to an S3 bucket.
29///
30/// # Example
31///
32/// ```rust,no_run
33/// use serde_json::Value;
34/// use rusty_cdk_core::stack::StackBuilder;
35/// use rusty_cdk_core::s3::BucketPolicyBuilder;
36/// use rusty_cdk_core::iam::{PolicyDocumentBuilder, StatementBuilder, Effect, PrincipalBuilder};
37/// use rusty_cdk_core::wrappers::*;
38/// use rusty_cdk_core::s3::BucketBuilder;
39/// use rusty_cdk_macros::iam_action;
40///
41/// let mut stack_builder = StackBuilder::new();
42/// let bucket = unimplemented!("create a bucket");
43///
44/// let resources = vec![Value::String("*".to_string())];
45/// let statement = StatementBuilder::new(
46///         vec![iam_action!("s3:GetObject")],
47///         Effect::Allow
48///     )
49///     .principal(PrincipalBuilder::new().normal("*").build())
50///     .resources(resources)
51///     .build();
52///
53/// let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
54/// let policy = BucketPolicyBuilder::new("bucket-policy", &bucket, policy_doc)
55///     .build(&mut stack_builder);
56/// ```
57pub struct BucketPolicyBuilder {
58    id: Id,
59    bucket_name: Value,
60    policy_document: PolicyDocument,
61}
62
63impl BucketPolicyBuilder {
64    /// Creates a new S3 bucket policy builder.
65    ///
66    /// # Arguments
67    /// * `id` - Unique identifier for the bucket policy
68    /// * `bucket` - Reference to the S3 bucket
69    /// * `policy_document` - IAM policy document controlling access
70    pub fn new(id: &str, bucket: &BucketRef, policy_document: PolicyDocument) -> Self {
71        Self {
72            id: Id(id.to_string()),
73            bucket_name: bucket.get_ref(),
74            policy_document,
75        }
76    }
77
78    pub(crate) fn new_with_bucket_ref(id: &str, bucket_name: Value, policy_document: PolicyDocument) -> Self {
79        Self {
80            id: Id(id.to_string()),
81            bucket_name,
82            policy_document,
83        }
84    }
85
86    pub(crate) fn raw_build(self) -> (String, BucketPolicy) {
87        let resource_id = Resource::generate_id("S3BucketPolicy");
88        let policy = BucketPolicy {
89            id: self.id,
90            resource_id: resource_id.to_string(),
91            r#type: "AWS::S3::BucketPolicy".to_string(),
92            properties: S3BucketPolicyProperties {
93                bucket_name: self.bucket_name,
94                policy_document: self.policy_document,
95            },
96        };
97        (resource_id, policy)
98    }
99
100    pub fn build(self, stack_builder: &mut StackBuilder) -> BucketPolicyRef {
101        let (resource_id, policy) = self.raw_build();
102        stack_builder.add_resource(policy);
103        BucketPolicyRef::new(resource_id)
104    }
105}
106
107pub enum VersioningConfiguration {
108    Enabled,
109    Suspended,
110}
111
112impl From<VersioningConfiguration> for String {
113    fn from(value: VersioningConfiguration) -> Self {
114        match value {
115            VersioningConfiguration::Enabled => "Enabled".to_string(),
116            VersioningConfiguration::Suspended => "Suspended".to_string(),
117        }
118    }
119}
120
121pub enum Encryption {
122    S3Managed,
123    KmsManaged,
124    DsseManaged,
125    // KMS, => add, this requires creating a kms key and passing it to the bucket
126    // DSSE, => add, similar
127}
128
129impl From<Encryption> for String {
130    fn from(value: Encryption) -> Self {
131        match value {
132            Encryption::S3Managed => "AES256".to_string(),
133            Encryption::KmsManaged => "aws:kms".to_string(),
134            Encryption::DsseManaged => "aws:kms:dsse".to_string(),
135        }
136    }
137}
138
139pub enum NotificationDestination<'a> {
140    Lambda(&'a FunctionRef, NotificationEventType),
141    Sns(&'a TopicRef, NotificationEventType),
142    Sqs(&'a QueueRef, NotificationEventType),
143}
144
145pub enum NotificationEventType {
146    ObjectCreated,
147    ObjectCreatedPut,
148    ObjectCreatedPost,
149    ObjectCreatedCopy,
150    ObjectCreatedCompleteMultipartUpload,
151    ObjectRemoved,
152    ObjectRemovedDelete,
153    ObjectRemovedDeleteMarkerCreated,
154    ObjectRestorePost,
155    ObjectRestoreCompleted,
156    ObjectRestoreDelete,
157    ReducedRedundancyLostObject,
158    ReplicationOperationFailedReplication,
159    ReplicationOperationMissedThreshold,
160    ReplicationOperationReplicatedAfterThreshold,
161    ReplicationOperationNotTracked,
162    LifecycleExpiration,
163    LifecycleExpirationDelete,
164    LifecycleExpirationDeleteMarkerCreated,
165    LifecycleTransition,
166    IntelligentTiering,
167    ObjectTagging,
168    ObjectTaggingPut,
169    ObjectTaggingDelete,
170    ObjectAclPut,
171    ObjectRestore,
172    REPLICATION,
173}
174
175impl From<NotificationEventType> for String {
176    fn from(value: NotificationEventType) -> Self {
177        match value {
178            NotificationEventType::ObjectCreated => "s3:ObjectCreated:*".to_string(),
179            NotificationEventType::ObjectCreatedPut => "s3:ObjectCreated:Put".to_string(),
180            NotificationEventType::ObjectCreatedPost =>  "s3:ObjectCreated:Post".to_string(),
181            NotificationEventType::ObjectCreatedCopy => "s3:ObjectCreated:Copy".to_string(),
182            NotificationEventType::ObjectCreatedCompleteMultipartUpload => "s3:ObjectCreated:CompleteMultipartUpload".to_string(),
183            NotificationEventType::ObjectRemoved => "s3:ObjectRemoved:*".to_string(),
184            NotificationEventType::ObjectRemovedDelete => "s3:ObjectRemoved:Delete".to_string(),
185            NotificationEventType::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated".to_string(),
186            NotificationEventType::ObjectRestorePost => "s3:ObjectRestore:Post".to_string(),
187            NotificationEventType::ObjectRestoreCompleted => "s3:ObjectRestore:Completed".to_string(),
188            NotificationEventType::ObjectRestoreDelete => "s3:ObjectRestore:Delete".to_string(),
189            NotificationEventType::ReducedRedundancyLostObject => "s3:ReducedRedundancyLostObject".to_string(),
190            NotificationEventType::ReplicationOperationFailedReplication => "s3:Replication:OperationFailedReplication".to_string(),
191            NotificationEventType::ReplicationOperationMissedThreshold => "s3:Replication:OperationMissedThreshold".to_string(),
192            NotificationEventType::ReplicationOperationReplicatedAfterThreshold => "s3:Replication:OperationReplicatedAfterThreshold".to_string(),
193            NotificationEventType::ReplicationOperationNotTracked => "s3:Replication:OperationNotTracked".to_string(),
194            NotificationEventType::LifecycleExpiration => "s3:LifecycleExpiration:*".to_string(),
195            NotificationEventType::LifecycleExpirationDelete => "s3:LifecycleExpiration:Delete".to_string(),
196            NotificationEventType::LifecycleExpirationDeleteMarkerCreated => "s3:LifecycleExpiration:DeleteMarkerCreated".to_string(),
197            NotificationEventType::LifecycleTransition => "s3:LifecycleTransition".to_string(),
198            NotificationEventType::IntelligentTiering => "s3:IntelligentTiering".to_string(),
199            NotificationEventType::ObjectTagging => "s3:ObjectTagging:*".to_string(),
200            NotificationEventType::ObjectTaggingPut => "s3:ObjectTagging:Put".to_string(),
201            NotificationEventType::ObjectTaggingDelete => "s3:ObjectTagging:Delete".to_string(),
202            NotificationEventType::ObjectAclPut => "s3:ObjectAcl:Put".to_string(),
203            NotificationEventType::ObjectRestore => "s3:ObjectRestore:*".to_string(),
204            NotificationEventType::REPLICATION => "s3:Replication:*".to_string(),
205        }
206    }
207}
208
209type_state!(BucketBuilderState, StartState, WebsiteState,);
210
211/// Builder for S3 buckets.
212///
213/// Provides configuration for S3 buckets including versioning, lifecycle rules, encryption, CORS, and static website hosting.
214///
215/// # Example
216///
217/// ```rust,compile_fail
218/// use rusty_cdk_core::stack::StackBuilder;
219/// use rusty_cdk_core::s3::{BucketBuilder, VersioningConfig, Encryption, VersioningConfiguration};
220/// use rusty_cdk_core::wrappers::*;
221/// use rusty_cdk_macros::bucket_name;
222///
223/// let mut stack_builder = StackBuilder::new();
224///
225/// // Create a simple bucket
226/// let bucket = BucketBuilder::new("my-bucket")
227///     .name(bucket_name!("my-unique-bucket"))
228///     .versioning_configuration(VersioningConfiguration::Enabled)
229///     .encryption(Encryption::S3Managed)
230///     .build(&mut stack_builder);
231///
232/// // Create a website bucket
233/// let (website_bucket, policy) = BucketBuilder::new("website-bucket")
234///     .website("index.html")
235///     .error_document("error.html")
236///     .build(&mut stack_builder);
237/// ```
238pub struct BucketBuilder<T: BucketBuilderState> {
239    phantom_data: PhantomData<T>,
240    id: Id,
241    name: Option<String>,
242    access: Option<PublicAccessBlockConfiguration>,
243    versioning_configuration: Option<VersioningConfiguration>,
244    lifecycle_configuration: Option<LifecycleConfiguration>,
245    index_document: Option<String>,
246    error_document: Option<String>,
247    redirect_all_requests_to: Option<(String, Option<Protocol>)>,
248    cors_config: Option<CorsConfiguration>,
249    bucket_encryption: Option<Encryption>,
250    bucket_notification_lambda_destinations: Vec<(Value, String)>,
251    bucket_notification_sns_destinations: Vec<(Value, String)>,
252    bucket_notification_sqs_destinations: Vec<(Value, Value, String)>,
253}
254
255impl BucketBuilder<StartState> {
256    /// Creates a new S3 bucket builder.
257    ///
258    /// # Arguments
259    /// * `id` - Unique identifier for the bucket
260    pub fn new(id: &str) -> Self {
261        Self {
262            id: Id(id.to_string()),
263            phantom_data: Default::default(),
264            name: None,
265            access: None,
266            versioning_configuration: None,
267            lifecycle_configuration: None,
268            index_document: None,
269            error_document: None,
270            redirect_all_requests_to: None,
271            cors_config: None,
272            bucket_encryption: None,
273            bucket_notification_lambda_destinations: vec![],
274            bucket_notification_sns_destinations: vec![],
275            bucket_notification_sqs_destinations: vec![],
276        }
277    }
278
279    pub fn build(self, stack_builder: &mut StackBuilder) -> BucketRef {
280        let (bucket, _) = self.build_internal(false, stack_builder);
281        bucket
282    }
283}
284
285impl<T: BucketBuilderState> BucketBuilder<T> {
286    pub fn name(self, name: BucketName) -> Self {
287        Self {
288            name: Some(name.0),
289            ..self
290        }
291    }
292
293    pub fn versioning_configuration(self, config: VersioningConfiguration) -> Self {
294        Self {
295            versioning_configuration: Some(config),
296            ..self
297        }
298    }
299
300    pub fn lifecycle_configuration(self, config: LifecycleConfiguration) -> Self {
301        Self {
302            lifecycle_configuration: Some(config),
303            ..self
304        }
305    }
306
307    pub fn public_access_block_configuration(self, access: PublicAccessBlockConfiguration) -> Self {
308        Self {
309            access: Some(access),
310            ..self
311        }
312    }
313
314    pub fn encryption(self, encryption: Encryption) -> Self {
315        Self {
316            bucket_encryption: Some(encryption),
317            ..self
318        }
319    }
320
321    pub fn add_notification(mut self, destination: NotificationDestination) -> Self {
322        match destination {
323            NotificationDestination::Lambda(l, e) => self.bucket_notification_lambda_destinations.push((l.get_arn(), e.into())),
324            NotificationDestination::Sns(s, e) => self.bucket_notification_sns_destinations.push((s.get_ref(), e.into())),
325            NotificationDestination::Sqs(q, e) => self.bucket_notification_sqs_destinations.push((q.get_ref(), q.get_arn(), e.into())),
326        }
327        self
328    }
329
330    /// Configures the bucket for static website hosting.
331    ///
332    /// Automatically disables public access blocks and creates a bucket policy
333    /// allowing public GetObject access.
334    pub fn website<I: Into<String>>(self, index_document: I) -> BucketBuilder<WebsiteState> {
335        BucketBuilder {
336            phantom_data: Default::default(),
337            id: self.id,
338            name: self.name,
339            access: self.access,
340            versioning_configuration: self.versioning_configuration,
341            lifecycle_configuration: self.lifecycle_configuration,
342            index_document: Some(index_document.into()),
343            error_document: self.error_document,
344            redirect_all_requests_to: self.redirect_all_requests_to,
345            cors_config: self.cors_config,
346            bucket_encryption: self.bucket_encryption,
347            bucket_notification_lambda_destinations: self.bucket_notification_lambda_destinations,
348            bucket_notification_sns_destinations: self.bucket_notification_sns_destinations,
349            bucket_notification_sqs_destinations: self.bucket_notification_sqs_destinations,
350        }
351    }
352
353    fn build_internal(self, website: bool, stack_builder: &mut StackBuilder) -> (BucketRef, Option<BucketPolicyRef>) {
354        let resource_id = Resource::generate_id("S3Bucket");
355
356        let versioning_configuration = self.versioning_configuration.map(|c| dto::VersioningConfig { status: c.into() });
357
358        let website_configuration = if website {
359            let redirect_all_requests_to = self.redirect_all_requests_to.map(|r| RedirectAllRequestsTo {
360                host_name: r.0,
361                protocol: r.1.map(Into::into),
362            });
363
364            Some(WebsiteConfiguration {
365                index_document: self.index_document,
366                error_document: self.error_document,
367                redirect_all_requests_to,
368            })
369        } else {
370            None
371        };
372
373        let access = if self.access.is_none() && website {
374            // turning this off is required for an S3 website
375            Some(PublicAccessBlockConfiguration {
376                block_public_acls: Some(false),
377                block_public_policy: Some(false),
378                ignore_public_acls: Some(false),
379                restrict_public_buckets: Some(false),
380            })
381        } else {
382            self.access
383        };
384
385        let encryption = self.bucket_encryption.map(|v| {
386            let rule = ServerSideEncryptionRule {
387                server_side_encryption_by_default: ServerSideEncryptionByDefault {
388                    sse_algorithm: v.into(),
389                    kms_master_key_id: None,
390                },
391                bucket_key_enabled: None,
392            };
393
394            BucketEncryption {
395                server_side_encryption_configuration: vec![rule],
396            }
397        });
398
399        let properties = BucketProperties {
400            bucket_name: self.name,
401            cors_configuration: self.cors_config,
402            lifecycle_configuration: self.lifecycle_configuration,
403            public_access_block_configuration: access,
404            versioning_configuration,
405            website_configuration,
406            bucket_encryption: encryption,
407            notification_configuration: None,
408        };
409
410        stack_builder.add_resource(Bucket {
411            id: self.id.clone(),
412            resource_id: resource_id.clone(),
413            r#type: "AWS::S3::Bucket".to_string(),
414            properties,
415        });
416
417        let bucket = BucketRef::new(resource_id);
418
419        let policy = if website {
420            // website needs a policy to allow GETs
421            let bucket_resource = vec![join("", vec![bucket.get_arn(), Value::String("/*".to_string())])];
422            let statement = StatementBuilder::new(vec![IamAction("s3:GetObject".to_string())], Effect::Allow)
423                .resources(bucket_resource)
424                .principal(PrincipalBuilder::new().normal("*").build())
425                .build();
426            let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
427            let bucket_policy_id = format!("{}-website-s3-policy", self.id);
428            let s3_policy = BucketPolicyBuilder::new(bucket_policy_id.as_str(), &bucket, policy_doc).build(stack_builder);
429            Some(s3_policy)
430        } else {
431            None
432        };
433
434        for (i, (arn, event)) in self.bucket_notification_lambda_destinations.into_iter().enumerate() {
435            let permission = PermissionBuilder::new(
436                &format!("{}-lambda-destination-perm-{}", self.id, i),
437                LambdaPermissionAction("lambda:InvokeFunction".to_string()),
438                arn.clone(),
439                "s3.amazonaws.com",
440            )
441            .source_arn(bucket.get_arn())
442            .current_account()
443            .build(stack_builder);
444            let handler = Self::notification_handler(&self.id, "lambda", i, stack_builder);
445            BucketNotificationBuilder::new(
446                &format!("{}-lambda-bucket-notification-{}", self.id, i),
447                handler.get_arn(),
448                bucket.get_ref(),
449                event,
450                permission.get_id(),
451            )
452            .lambda(arn)
453            .build(stack_builder);
454        }
455
456        for (i, (reference, event)) in self.bucket_notification_sns_destinations.into_iter().enumerate() {
457            let handler = Self::notification_handler(&self.id, "sns", i, stack_builder);
458
459            let mut source_arn = Map::new();
460            source_arn.insert("aws:SourceArn".to_string(), bucket.get_arn());
461            let mut condition = Map::new();
462            condition.insert("ArnLike".to_string(), Value::Object(source_arn));
463            let statement = StatementBuilder::new(vec![IamAction("sns:Publish".to_string())], Effect::Allow)
464                .principal(Principal::Service(ServicePrincipal {
465                    service: "s3.amazonaws.com".to_string(),
466                }))
467                .condition(Value::Object(condition))
468                .resources(vec![reference.clone()])
469                .build();
470            let doc = PolicyDocumentBuilder::new(vec![statement]).build();
471            let topic_ref = TopicPolicyBuilder::new(&format!("{}-sns-destination-policy-{}", self.id, i), doc, vec![reference.clone()])
472                .build(stack_builder);
473
474            BucketNotificationBuilder::new(
475                &format!("{}-sns-bucket-notification-{}", self.id, i),
476                handler.get_arn(),
477                bucket.get_ref(),
478                event,
479                topic_ref.get_id(),
480            )
481            .sns(reference)
482            .build(stack_builder);
483        }
484
485        for (i, (reference, arn, event)) in self.bucket_notification_sqs_destinations.into_iter().enumerate() {
486            let handler = Self::notification_handler(&self.id, "sqs", i, stack_builder);
487            
488            let mut source_arn = Map::new();
489            source_arn.insert("aws:SourceArn".to_string(), bucket.get_arn());
490            let mut condition = Map::new();
491            condition.insert("ArnLike".to_string(), Value::Object(source_arn));
492            let statement = StatementBuilder::new(vec![IamAction("sqs:GetQueueAttributes".to_string()), IamAction("sqs:GetQueueUrl".to_string()), IamAction("sqs:SendMessage".to_string())], Effect::Allow)
493                .principal(Principal::Service(ServicePrincipal {
494                    service: "s3.amazonaws.com".to_string(),
495                }))
496                .condition(Value::Object(condition))
497                .resources(vec![arn.clone()])
498                .build();
499            let doc = PolicyDocumentBuilder::new(vec![statement]).build();
500            let queue_policy_ref = QueuePolicyBuilder::new(&format!("{}-sqs-destination-policy-{}", self.id, i), doc, vec![reference.clone()])
501                .build(stack_builder);
502
503            BucketNotificationBuilder::new(
504                &format!("{}-sqs-bucket-notification-{}", self.id, i),
505                handler.get_arn(),
506                bucket.get_ref(),
507                event,
508                queue_policy_ref.get_id(),
509            )
510            .sqs(arn)
511            .build(stack_builder);
512        }
513
514        (bucket, policy)
515    }
516
517    fn notification_handler(id: &Id, target: &str, num: usize, stack_builder: &mut StackBuilder) -> FunctionRef {
518        let (handler, ..) = FunctionBuilder::new(
519            &format!("{}-{}-handler-{}", id, target, num),
520            Architecture::X86_64,
521            Memory(128),
522            Timeout(300),
523        )
524        .code(Code::Inline(BUCKET_NOTIFICATION_HANDLER_CODE.to_string()))
525        .handler("index.handler")
526        .runtime(Runtime::Python313)
527        .add_permission(Permission::Custom(CustomPermission::new(
528            "NotificationPermission",
529            StatementBuilder::new(vec![IamAction("s3:PutBucketNotification".to_string())], Effect::Allow)
530                .all_resources()
531                .build(),
532        )))
533        .build(stack_builder);
534        handler
535    }
536}
537
538impl BucketBuilder<WebsiteState> {
539    pub fn error_document<I: Into<String>>(self, error: I) -> Self {
540        Self {
541            error_document: Some(error.into()),
542            ..self
543        }
544    }
545
546    pub fn redirect_all<I: Into<String>>(self, hostname: I, protocol: Option<Protocol>) -> Self {
547        Self {
548            redirect_all_requests_to: Some((hostname.into(), protocol)),
549            ..self
550        }
551    }
552
553    pub fn cors_config(self, config: CorsConfiguration) -> Self {
554        Self {
555            cors_config: Some(config),
556            ..self
557        }
558    }
559
560    /// Builds the website bucket and adds it to the stack.
561    ///
562    /// Returns both the bucket and the automatically created bucket policy
563    /// that allows public read access.
564    pub fn build(self, stack_builder: &mut StackBuilder) -> (BucketRef, BucketPolicyRef) {
565        let (bucket, policy) = self.build_internal(true, stack_builder);
566        (bucket, policy.expect("for website, bucket policy should always be present"))
567    }
568}
569
570/// Builder for S3 CORS configuration.
571pub struct CorsConfigurationBuilder {
572    rules: Vec<CorsRule>,
573}
574
575impl CorsConfigurationBuilder {
576    pub fn new(rules: Vec<CorsRule>) -> CorsConfigurationBuilder {
577        CorsConfigurationBuilder { rules }
578    }
579
580    pub fn build(self) -> CorsConfiguration {
581        CorsConfiguration { cors_rules: self.rules }
582    }
583}
584
585/// Builder for individual CORS rules.
586pub struct CorsRuleBuilder {
587    allow_origins: Vec<String>,
588    allow_methods: Vec<HttpMethod>,
589    allow_headers: Option<Vec<String>>,
590    expose_headers: Option<Vec<String>>,
591    max_age: Option<u64>,
592}
593
594impl CorsRuleBuilder {
595    pub fn new<T: Into<String>>(allow_origins: Vec<T>, allow_methods: Vec<HttpMethod>) -> Self {
596        Self {
597            allow_origins: allow_origins.into_iter().map(Into::into).collect(),
598            allow_methods,
599            allow_headers: None,
600            expose_headers: None,
601            max_age: None,
602        }
603    }
604
605    pub fn allow_headers(self, headers: Vec<String>) -> Self {
606        Self {
607            allow_headers: Some(headers),
608            ..self
609        }
610    }
611
612    pub fn expose_headers(self, headers: Vec<String>) -> Self {
613        Self {
614            expose_headers: Some(headers),
615            ..self
616        }
617    }
618
619    pub fn max_age(self, age: Duration) -> Self {
620        Self {
621            max_age: Some(age.as_secs()),
622            ..self
623        }
624    }
625
626    #[must_use]
627    pub fn build(self) -> CorsRule {
628        CorsRule {
629            allowed_headers: self.allow_headers,
630            allowed_methods: self.allow_methods.into_iter().map(Into::into).collect(),
631            allowed_origins: self.allow_origins,
632            exposed_headers: self.expose_headers,
633            max_age: self.max_age,
634        }
635    }
636}
637
638pub enum TransitionDefaultMinimumObjectSize {
639    VariesByStorageClass,
640    AllStorageClasses128k,
641}
642
643impl From<TransitionDefaultMinimumObjectSize> for String {
644    fn from(value: TransitionDefaultMinimumObjectSize) -> Self {
645        match value {
646            TransitionDefaultMinimumObjectSize::VariesByStorageClass => "varies_by_storage_class".to_string(),
647            TransitionDefaultMinimumObjectSize::AllStorageClasses128k => "all_storage_classes_128K".to_string(),
648        }
649    }
650}
651
652pub enum LifecycleStorageClass {
653    IntelligentTiering,
654    OneZoneIA,
655    StandardIA,
656    GlacierDeepArchive,
657    Glacier,
658    GlacierInstantRetrieval,
659}
660
661impl From<LifecycleStorageClass> for String {
662    fn from(value: LifecycleStorageClass) -> Self {
663        match value {
664            LifecycleStorageClass::GlacierDeepArchive => "DEEP_ARCHIVE".to_string(),
665            LifecycleStorageClass::Glacier => "GLACIER".to_string(),
666            LifecycleStorageClass::GlacierInstantRetrieval => "GLACIER_IR".to_string(),
667            LifecycleStorageClass::IntelligentTiering => "INTELLIGENT_TIERING".to_string(),
668            LifecycleStorageClass::OneZoneIA => "ONEZONE_IA".to_string(),
669            LifecycleStorageClass::StandardIA => "STANDARD_IA".to_string(),
670        }
671    }
672}
673
674/// Builder for S3 lifecycle rule transitions.
675///
676/// Configures automatic transitions of objects to different storage classes.
677pub struct LifecycleRuleTransitionBuilder {
678    storage_class: LifecycleStorageClass,
679    transition_in_days: Option<u16>,
680}
681
682impl LifecycleRuleTransitionBuilder {
683    pub fn new(storage_class: LifecycleStorageClass) -> Self {
684        Self {
685            storage_class,
686            transition_in_days: None,
687        }
688    }
689
690    pub fn transition_in_days(self, days: LifecycleTransitionInDays) -> Self {
691        Self {
692            transition_in_days: Some(days.0),
693            ..self
694        }
695    }
696
697    #[must_use]
698    pub fn build(self) -> LifecycleRuleTransition {
699        LifecycleRuleTransition {
700            storage_class: self.storage_class.into(),
701            transition_in_days: self.transition_in_days.unwrap_or(0),
702        }
703    }
704}
705
706/// Builder for non-current version transitions in versioned buckets.
707///
708/// Configures automatic transitions for previous versions of objects.
709pub struct NonCurrentVersionTransitionBuilder {
710    storage_class: LifecycleStorageClass,
711    transition_in_days: u32,
712    newer_non_current_versions: Option<u32>,
713}
714
715impl NonCurrentVersionTransitionBuilder {
716    pub fn new(storage_class: LifecycleStorageClass, transition_in_days: u32) -> Self {
717        Self {
718            storage_class,
719            transition_in_days,
720            newer_non_current_versions: None,
721        }
722    }
723
724    pub fn newer_non_current_versions(self, versions: u32) -> Self {
725        Self {
726            newer_non_current_versions: Some(versions),
727            ..self
728        }
729    }
730
731    #[must_use]
732    pub fn build(self) -> NonCurrentVersionTransition {
733        NonCurrentVersionTransition {
734            storage_class: self.storage_class.into(),
735            transition_in_days: self.transition_in_days,
736            newer_non_current_versions: self.newer_non_current_versions,
737        }
738    }
739}
740
741pub enum LifecycleRuleStatus {
742    Enabled,
743    Disabled,
744}
745
746impl From<LifecycleRuleStatus> for String {
747    fn from(value: LifecycleRuleStatus) -> Self {
748        match value {
749            LifecycleRuleStatus::Enabled => "Enabled".to_string(),
750            LifecycleRuleStatus::Disabled => "Disabled".to_string(),
751        }
752    }
753}
754
755/// Builder for S3 lifecycle rules.
756///
757/// Defines rules for automatic object expiration and transitions between storage classes.
758pub struct LifecycleRuleBuilder {
759    id: Option<String>,
760    status: LifecycleRuleStatus,
761    expiration_in_days: Option<u16>, // expiration must be > than expiration in transition (ow boy...)
762    prefix: Option<String>,
763    object_size_greater_than: Option<u32>,
764    object_size_less_than: Option<u32>,
765    abort_incomplete_multipart_upload: Option<u16>,
766    non_current_version_expiration: Option<u16>,
767    transitions: Option<Vec<LifecycleRuleTransition>>,
768    non_current_version_transitions: Option<Vec<NonCurrentVersionTransition>>,
769}
770
771impl LifecycleRuleBuilder {
772    pub fn new(status: LifecycleRuleStatus) -> Self {
773        Self {
774            status,
775            id: None,
776            expiration_in_days: None,
777            prefix: None,
778            object_size_greater_than: None,
779            object_size_less_than: None,
780            abort_incomplete_multipart_upload: None,
781            non_current_version_expiration: None,
782            transitions: None,
783            non_current_version_transitions: None,
784        }
785    }
786
787    pub fn id<T: Into<String>>(self, id: T) -> Self {
788        Self {
789            id: Some(id.into()),
790            ..self
791        }
792    }
793
794    pub fn expiration_in_days(self, days: u16) -> Self {
795        Self {
796            expiration_in_days: Some(days),
797            ..self
798        }
799    }
800
801    pub fn prefix<T: Into<String>>(self, prefix: T) -> Self {
802        Self {
803            prefix: Some(prefix.into()),
804            ..self
805        }
806    }
807
808    pub fn object_size(self, sizes: S3LifecycleObjectSizes) -> Self {
809        Self {
810            object_size_less_than: sizes.0,
811            object_size_greater_than: sizes.1,
812            ..self
813        }
814    }
815
816    pub fn abort_incomplete_multipart_upload(self, days: u16) -> Self {
817        Self {
818            abort_incomplete_multipart_upload: Some(days),
819            ..self
820        }
821    }
822
823    pub fn non_current_version_expiration(self, days: u16) -> Self {
824        Self {
825            non_current_version_expiration: Some(days),
826            ..self
827        }
828    }
829
830    pub fn add_transition(mut self, transition: LifecycleRuleTransition) -> Self {
831        if let Some(mut transitions) = self.transitions {
832            transitions.push(transition);
833            self.transitions = Some(transitions);
834        } else {
835            self.transitions = Some(vec![transition]);
836        }
837
838        Self { ..self }
839    }
840
841    pub fn add_non_current_version_transitions(mut self, transition: NonCurrentVersionTransition) -> Self {
842        if let Some(mut transitions) = self.non_current_version_transitions {
843            transitions.push(transition);
844            self.non_current_version_transitions = Some(transitions);
845        } else {
846            self.non_current_version_transitions = Some(vec![transition]);
847        }
848
849        Self { ..self }
850    }
851
852    pub fn build(self) -> LifecycleRule {
853        LifecycleRule {
854            id: self.id,
855            status: self.status.into(),
856            expiration_in_days: self.expiration_in_days,
857            prefix: self.prefix,
858            object_size_greater_than: self.object_size_greater_than,
859            object_size_less_than: self.object_size_less_than,
860            transitions: self.transitions,
861            abort_incomplete_multipart_upload: self.abort_incomplete_multipart_upload,
862            non_current_version_expiration: self.non_current_version_expiration,
863            non_current_version_transitions: self.non_current_version_transitions,
864        }
865    }
866}
867
868/// Builder for S3 lifecycle configuration.
869///
870/// Combines multiple lifecycle rules into a configuration for a bucket.
871pub struct LifecycleConfigurationBuilder {
872    rules: Vec<LifecycleRule>,
873    transition_minimum_size: Option<TransitionDefaultMinimumObjectSize>,
874}
875
876impl Default for LifecycleConfigurationBuilder {
877    fn default() -> Self {
878        Self::new()
879    }
880}
881
882impl LifecycleConfigurationBuilder {
883    pub fn new() -> Self {
884        Self {
885            rules: vec![],
886            transition_minimum_size: None,
887        }
888    }
889
890    pub fn transition_minimum_size(self, size: TransitionDefaultMinimumObjectSize) -> Self {
891        Self {
892            transition_minimum_size: Some(size),
893            ..self
894        }
895    }
896
897    pub fn add_rule(mut self, rule: LifecycleRule) -> Self {
898        self.rules.push(rule);
899        self
900    }
901
902    #[must_use]
903    pub fn build(self) -> LifecycleConfiguration {
904        LifecycleConfiguration {
905            rules: self.rules,
906            transition_minimum_size: self.transition_minimum_size.map(|v| v.into()),
907        }
908    }
909}
910
911/// Builder for S3 public access block configuration.
912///
913/// Controls public access to the bucket at the bucket level.
914pub struct PublicAccessBlockConfigurationBuilder {
915    block_public_acls: Option<bool>,
916    block_public_policy: Option<bool>,
917    ignore_public_acls: Option<bool>,
918    restrict_public_buckets: Option<bool>,
919}
920
921impl Default for PublicAccessBlockConfigurationBuilder {
922    fn default() -> Self {
923        Self::new()
924    }
925}
926
927impl PublicAccessBlockConfigurationBuilder {
928    pub fn new() -> Self {
929        Self {
930            block_public_acls: None,
931            block_public_policy: None,
932            ignore_public_acls: None,
933            restrict_public_buckets: None,
934        }
935    }
936
937    pub fn block_public_acls(self, config: bool) -> Self {
938        Self {
939            block_public_acls: Some(config),
940            ..self
941        }
942    }
943
944    pub fn block_public_policy(self, config: bool) -> Self {
945        Self {
946            block_public_policy: Some(config),
947            ..self
948        }
949    }
950
951    pub fn ignore_public_acls(self, config: bool) -> Self {
952        Self {
953            ignore_public_acls: Some(config),
954            ..self
955        }
956    }
957
958    pub fn restrict_public_buckets(self, config: bool) -> Self {
959        Self {
960            restrict_public_buckets: Some(config),
961            ..self
962        }
963    }
964
965    #[must_use]
966    pub fn build(self) -> PublicAccessBlockConfiguration {
967        PublicAccessBlockConfiguration {
968            block_public_acls: self.block_public_acls,
969            block_public_policy: self.block_public_policy,
970            ignore_public_acls: self.ignore_public_acls,
971            restrict_public_buckets: self.restrict_public_buckets,
972        }
973    }
974}