rusty_cdk_core/s3/
builder.rs

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