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, AccelerateConfiguration, IntelligentTieringConfiguration, InventoryTableConfiguration, JournalTableConfiguration, MetadataConfiguration, MetadataDestination, RecordExpiration, TagFilter, Tiering};
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, BucketTiering, IamAction, LambdaPermissionAction, LifecycleTransitionInDays, Memory, RecordExpirationDays, 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
104#[derive(Debug, Clone)]
105pub enum VersioningConfiguration {
106    Enabled,
107    Suspended,
108}
109
110impl From<VersioningConfiguration> for String {
111    fn from(value: VersioningConfiguration) -> Self {
112        match value {
113            VersioningConfiguration::Enabled => "Enabled".to_string(),
114            VersioningConfiguration::Suspended => "Suspended".to_string(),
115        }
116    }
117}
118
119#[derive(Debug, Clone)]
120pub enum Encryption {
121    S3Managed,
122    KmsManaged,
123    DsseManaged,
124    // KMS, => add, this requires creating a kms key and passing it to the bucket
125    // DSSE, => add, similar
126}
127
128impl From<Encryption> for String {
129    fn from(value: Encryption) -> Self {
130        match value {
131            Encryption::S3Managed => "AES256".to_string(),
132            Encryption::KmsManaged => "aws:kms".to_string(),
133            Encryption::DsseManaged => "aws:kms:dsse".to_string(),
134        }
135    }
136}
137
138pub enum NotificationDestination<'a> {
139    Lambda(&'a FunctionRef, NotificationEventType),
140    Sns(&'a TopicRef, NotificationEventType),
141    Sqs(&'a QueueRef, NotificationEventType),
142}
143
144#[derive(Debug, Clone)]
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
175#[derive(Debug, Clone)]
176pub enum AbacStatus {
177    Enabled,
178    Disabled,
179}
180
181impl From<AbacStatus> for String {
182    fn from(value: AbacStatus) -> Self {
183        match value {
184            AbacStatus::Enabled => "Enabled".to_string(),
185            AbacStatus::Disabled => "Disabled".to_string(),
186        }
187    }
188}
189#[derive(Debug, Clone)]
190pub enum AccelerationStatus {
191    Enabled,
192    Suspended,
193}
194
195impl From<AccelerationStatus> for String {
196    fn from(value: AccelerationStatus) -> Self {
197        match value {
198            AccelerationStatus::Enabled => "Enabled".to_string(),
199            AccelerationStatus::Suspended => "Suspended".to_string(),
200        }
201    }
202}
203
204impl From<NotificationEventType> for String {
205    fn from(value: NotificationEventType) -> Self {
206        match value {
207            NotificationEventType::ObjectCreated => "s3:ObjectCreated:*".to_string(),
208            NotificationEventType::ObjectCreatedPut => "s3:ObjectCreated:Put".to_string(),
209            NotificationEventType::ObjectCreatedPost => "s3:ObjectCreated:Post".to_string(),
210            NotificationEventType::ObjectCreatedCopy => "s3:ObjectCreated:Copy".to_string(),
211            NotificationEventType::ObjectCreatedCompleteMultipartUpload => "s3:ObjectCreated:CompleteMultipartUpload".to_string(),
212            NotificationEventType::ObjectRemoved => "s3:ObjectRemoved:*".to_string(),
213            NotificationEventType::ObjectRemovedDelete => "s3:ObjectRemoved:Delete".to_string(),
214            NotificationEventType::ObjectRemovedDeleteMarkerCreated => "s3:ObjectRemoved:DeleteMarkerCreated".to_string(),
215            NotificationEventType::ObjectRestorePost => "s3:ObjectRestore:Post".to_string(),
216            NotificationEventType::ObjectRestoreCompleted => "s3:ObjectRestore:Completed".to_string(),
217            NotificationEventType::ObjectRestoreDelete => "s3:ObjectRestore:Delete".to_string(),
218            NotificationEventType::ReducedRedundancyLostObject => "s3:ReducedRedundancyLostObject".to_string(),
219            NotificationEventType::ReplicationOperationFailedReplication => "s3:Replication:OperationFailedReplication".to_string(),
220            NotificationEventType::ReplicationOperationMissedThreshold => "s3:Replication:OperationMissedThreshold".to_string(),
221            NotificationEventType::ReplicationOperationReplicatedAfterThreshold => {
222                "s3:Replication:OperationReplicatedAfterThreshold".to_string()
223            }
224            NotificationEventType::ReplicationOperationNotTracked => "s3:Replication:OperationNotTracked".to_string(),
225            NotificationEventType::LifecycleExpiration => "s3:LifecycleExpiration:*".to_string(),
226            NotificationEventType::LifecycleExpirationDelete => "s3:LifecycleExpiration:Delete".to_string(),
227            NotificationEventType::LifecycleExpirationDeleteMarkerCreated => "s3:LifecycleExpiration:DeleteMarkerCreated".to_string(),
228            NotificationEventType::LifecycleTransition => "s3:LifecycleTransition".to_string(),
229            NotificationEventType::IntelligentTiering => "s3:IntelligentTiering".to_string(),
230            NotificationEventType::ObjectTagging => "s3:ObjectTagging:*".to_string(),
231            NotificationEventType::ObjectTaggingPut => "s3:ObjectTagging:Put".to_string(),
232            NotificationEventType::ObjectTaggingDelete => "s3:ObjectTagging:Delete".to_string(),
233            NotificationEventType::ObjectAclPut => "s3:ObjectAcl:Put".to_string(),
234            NotificationEventType::ObjectRestore => "s3:ObjectRestore:*".to_string(),
235            NotificationEventType::REPLICATION => "s3:Replication:*".to_string(),
236        }
237    }
238}
239
240type_state!(BucketBuilderState, StartState, WebsiteState,);
241
242/// Builder for S3 buckets.
243///
244/// Provides configuration for S3 buckets including versioning, lifecycle rules, encryption, CORS, and static website hosting.
245///
246/// # Example
247///
248/// ```rust,compile_fail
249/// use rusty_cdk_core::stack::StackBuilder;
250/// use rusty_cdk_core::s3::{BucketBuilder, VersioningConfig, Encryption, VersioningConfiguration};
251/// use rusty_cdk_core::wrappers::*;
252/// use rusty_cdk_macros::bucket_name;
253///
254/// let mut stack_builder = StackBuilder::new();
255///
256/// // Create a simple bucket
257/// let bucket = BucketBuilder::new("my-bucket")
258///     .name(bucket_name!("my-unique-bucket"))
259///     .versioning_configuration(VersioningConfiguration::Enabled)
260///     .encryption(Encryption::S3Managed)
261///     .build(&mut stack_builder);
262///
263/// // Create a website bucket
264/// let (website_bucket, policy) = BucketBuilder::new("website-bucket")
265///     .website("index.html")
266///     .error_document("error.html")
267///     .build(&mut stack_builder);
268/// ```
269pub struct BucketBuilder<T: BucketBuilderState> {
270    phantom_data: PhantomData<T>,
271    id: Id,
272    abac_status: Option<String>,
273    acceleration_status: Option<String>,
274    name: Option<String>,
275    access: Option<PublicAccessBlockConfiguration>,
276    intelligent_tiering_configurations: Option<Vec<IntelligentTieringConfiguration>>,
277    metadata_configuration: Option<MetadataConfiguration>,
278    versioning_configuration: Option<VersioningConfiguration>,
279    lifecycle_configuration: Option<LifecycleConfiguration>,
280    index_document: Option<String>,
281    error_document: Option<String>,
282    redirect_all_requests_to: Option<(String, Option<Protocol>)>,
283    cors_config: Option<CorsConfiguration>,
284    bucket_encryption: Option<Encryption>,
285    bucket_notification_lambda_destinations: Vec<(Value, String)>,
286    bucket_notification_sns_destinations: Vec<(Value, String)>,
287    bucket_notification_sqs_destinations: Vec<(Value, Value, String)>,
288}
289
290impl BucketBuilder<StartState> {
291    /// Creates a new S3 bucket builder.
292    ///
293    /// # Arguments
294    /// * `id` - Unique identifier for the bucket
295    pub fn new(id: &str) -> Self {
296        Self {
297            id: Id(id.to_string()),
298            phantom_data: Default::default(),
299            abac_status: None,
300            acceleration_status: None,
301            name: None,
302            access: None,
303            intelligent_tiering_configurations: None,
304            metadata_configuration: None,
305            versioning_configuration: None,
306            lifecycle_configuration: None,
307            index_document: None,
308            error_document: None,
309            redirect_all_requests_to: None,
310            cors_config: None,
311            bucket_encryption: None,
312            bucket_notification_lambda_destinations: vec![],
313            bucket_notification_sns_destinations: vec![],
314            bucket_notification_sqs_destinations: vec![],
315        }
316    }
317
318    pub fn build(self, stack_builder: &mut StackBuilder) -> BucketRef {
319        let (bucket, _) = self.build_internal(false, stack_builder);
320        bucket
321    }
322}
323
324impl<T: BucketBuilderState> BucketBuilder<T> {
325    pub fn name(self, name: BucketName) -> Self {
326        Self {
327            name: Some(name.0),
328            ..self
329        }
330    }
331
332    pub fn abac_status(self, abac_status: AbacStatus) -> Self {
333        Self {
334            abac_status: Some(abac_status.into()),
335            ..self
336        }
337    }
338
339    pub fn acceleration_status(self, acceleration_status: AccelerationStatus) -> Self {
340        Self {
341            acceleration_status: Some(acceleration_status.into()),
342            ..self
343        }
344    }
345
346    pub fn metadata_configuration(self, config: MetadataConfiguration) -> Self {
347        Self {
348            metadata_configuration: Some(config),
349            ..self
350        }
351    }
352
353    pub fn versioning_configuration(self, config: VersioningConfiguration) -> Self {
354        Self {
355            versioning_configuration: Some(config),
356            ..self
357        }
358    }
359
360    pub fn lifecycle_configuration(self, config: LifecycleConfiguration) -> Self {
361        Self {
362            lifecycle_configuration: Some(config),
363            ..self
364        }
365    }
366
367    pub fn public_access_block_configuration(self, access: PublicAccessBlockConfiguration) -> Self {
368        Self {
369            access: Some(access),
370            ..self
371        }
372    }
373
374    pub fn encryption(self, encryption: Encryption) -> Self {
375        Self {
376            bucket_encryption: Some(encryption),
377            ..self
378        }
379    }
380
381    pub fn add_intelligent_tiering(mut self, tiering: IntelligentTieringConfiguration) -> Self {
382        if let Some(mut config) = self.intelligent_tiering_configurations {
383            config.push(tiering);
384            self.intelligent_tiering_configurations = Some(config);
385        } else {
386            self.intelligent_tiering_configurations = Some(vec![tiering]);
387        }
388        self
389    }
390
391    pub fn add_notification(mut self, destination: NotificationDestination) -> Self {
392        match destination {
393            NotificationDestination::Lambda(l, e) => self.bucket_notification_lambda_destinations.push((l.get_arn(), e.into())),
394            NotificationDestination::Sns(s, e) => self.bucket_notification_sns_destinations.push((s.get_ref(), e.into())),
395            NotificationDestination::Sqs(q, e) => self.bucket_notification_sqs_destinations.push((q.get_ref(), q.get_arn(), e.into())),
396        }
397        self
398    }
399
400    /// Configures the bucket for static website hosting.
401    ///
402    /// Automatically disables public access blocks and creates a bucket policy
403    /// allowing public GetObject access.
404    pub fn website<I: Into<String>>(self, index_document: I) -> BucketBuilder<WebsiteState> {
405        BucketBuilder {
406            phantom_data: Default::default(),
407            id: self.id,
408            abac_status: self.abac_status,
409            acceleration_status: self.acceleration_status,
410            name: self.name,
411            access: self.access,
412            intelligent_tiering_configurations: self.intelligent_tiering_configurations,
413            metadata_configuration: self.metadata_configuration,
414            versioning_configuration: self.versioning_configuration,
415            lifecycle_configuration: self.lifecycle_configuration,
416            index_document: Some(index_document.into()),
417            error_document: self.error_document,
418            redirect_all_requests_to: self.redirect_all_requests_to,
419            cors_config: self.cors_config,
420            bucket_encryption: self.bucket_encryption,
421            bucket_notification_lambda_destinations: self.bucket_notification_lambda_destinations,
422            bucket_notification_sns_destinations: self.bucket_notification_sns_destinations,
423            bucket_notification_sqs_destinations: self.bucket_notification_sqs_destinations,
424        }
425    }
426
427    fn build_internal(self, website: bool, stack_builder: &mut StackBuilder) -> (BucketRef, Option<BucketPolicyRef>) {
428        let resource_id = Resource::generate_id("S3Bucket");
429
430        let versioning_configuration = self.versioning_configuration.map(|c| dto::VersioningConfig { status: c.into() });
431
432        let website_configuration = if website {
433            let redirect_all_requests_to = self.redirect_all_requests_to.map(|r| RedirectAllRequestsTo {
434                host_name: r.0,
435                protocol: r.1.map(Into::into),
436            });
437
438            Some(WebsiteConfiguration {
439                index_document: self.index_document,
440                error_document: self.error_document,
441                redirect_all_requests_to,
442            })
443        } else {
444            None
445        };
446
447        let access = if self.access.is_none() && website {
448            // required for an S3 website
449            Some(PublicAccessBlockConfiguration {
450                block_public_policy: Some(false),
451                restrict_public_buckets: Some(false),
452                block_public_acls: Some(true),
453                ignore_public_acls: Some(true),
454            })
455        } else {
456            self.access
457        };
458
459        let encryption = self.bucket_encryption.map(|v| {
460            let rule = ServerSideEncryptionRule {
461                server_side_encryption_by_default: ServerSideEncryptionByDefault {
462                    sse_algorithm: v.into(),
463                    kms_master_key_id: None,
464                },
465                bucket_key_enabled: None,
466            };
467
468            BucketEncryption {
469                server_side_encryption_configuration: vec![rule],
470            }
471        });
472
473        let properties = BucketProperties {
474            abac_status: self.abac_status,
475            accelerate_configuration: self.acceleration_status.map(|v| {
476                AccelerateConfiguration {
477                    acceleration_status: v,
478                }
479            }),
480            bucket_name: self.name,
481            cors_configuration: self.cors_config,
482            intelligent_tiering_configurations: self.intelligent_tiering_configurations,
483            lifecycle_configuration: self.lifecycle_configuration,
484            public_access_block_configuration: access,
485            versioning_configuration,
486            website_configuration,
487            bucket_encryption: encryption,
488            metadata_configuration: self.metadata_configuration,
489        };
490
491        stack_builder.add_resource(Bucket {
492            id: self.id.clone(),
493            resource_id: resource_id.clone(),
494            r#type: "AWS::S3::Bucket".to_string(),
495            properties,
496        });
497
498        let bucket = BucketRef::new(resource_id);
499
500        let policy = if website {
501            // website needs a policy to allow GETs
502            let bucket_resource = vec![join("", vec![bucket.get_arn(), Value::String("/*".to_string())])];
503            let statement = StatementBuilder::new(vec![IamAction("s3:GetObject".to_string())], Effect::Allow)
504                .resources(bucket_resource)
505                .principal(PrincipalBuilder::new().normal("*").build())
506                .build();
507            let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
508            let bucket_policy_id = Id::generate_id(&self.id, "S3Policy");
509            let s3_policy = BucketPolicyBuilder::new(&bucket_policy_id, &bucket, policy_doc).build(stack_builder);
510            Some(s3_policy)
511        } else {
512            None
513        };
514
515        for (i, (arn, event)) in self.bucket_notification_lambda_destinations.into_iter().enumerate() {
516            let permission_id = Id::generate_id(&self.id, format!("LambdaDestPerm{}", i).as_str());
517            let permission = PermissionBuilder::new(
518                &permission_id,
519                LambdaPermissionAction("lambda:InvokeFunction".to_string()),
520                arn.clone(),
521                "s3.amazonaws.com",
522            )
523            .source_arn(bucket.get_arn())
524            .current_account()
525            .build(stack_builder);
526            let handler = Self::notification_handler(&self.id, "Lambda", i, stack_builder);
527            let notification_id = Id::generate_id(&self.id, &format!("LambdaNotification{}", i));
528            BucketNotificationBuilder::new(
529                &notification_id,
530                handler.get_arn(),
531                bucket.get_ref(),
532                event,
533                Some(permission.get_id()),
534            )
535            .lambda(arn)
536            .build(stack_builder);
537        }
538
539        for (i, (reference, event)) in self.bucket_notification_sns_destinations.into_iter().enumerate() {
540            let handler = Self::notification_handler(&self.id, "SNS", i, stack_builder);
541
542            let bucket_arn = bucket.get_arn();
543            let condition = json!({
544                "ArnLike": {
545                    "aws:SourceArn": bucket_arn
546                }
547            });
548            let principal = PrincipalBuilder::new().service("s3.amazonaws.com".to_string()).build();
549            let statement = StatementBuilder::new(vec![IamAction("sns:Publish".to_string())], Effect::Allow)
550                .principal(principal)
551                .condition(condition)
552                .resources(vec![reference.clone()])
553                .build();
554            let doc = PolicyDocumentBuilder::new(vec![statement]).build();
555
556            let topic_policy_id = Id::generate_id(&self.id, &format!("SNSDestinationPolicy{}", i));
557            let topic_ref =
558                TopicPolicyBuilder::new_with_values(&topic_policy_id, doc, vec![reference.clone()])
559                    .build(stack_builder);
560
561            let notification_id = Id::generate_id(&self.id, &format!("SNSNotification{}", i));
562            BucketNotificationBuilder::new(
563                &notification_id,
564                handler.get_arn(),
565                bucket.get_ref(),
566                event,
567                Some(topic_ref.get_id()),
568            )
569            .sns(reference)
570            .build(stack_builder);
571        }
572
573        for (i, (reference, arn, event)) in self.bucket_notification_sqs_destinations.into_iter().enumerate() {
574            let handler = Self::notification_handler(&self.id, "SQS", i, stack_builder);
575
576            let bucket_arn = bucket.get_arn();
577            let condition = json!({
578                "ArnLike": {
579                    "aws:SourceArn": bucket_arn
580                }
581            });
582            let principal = PrincipalBuilder::new().service("s3.amazonaws.com".to_string()).build();
583            let statement = StatementBuilder::new(
584                vec![
585                    IamAction("sqs:GetQueueAttributes".to_string()),
586                    IamAction("sqs:GetQueueUrl".to_string()),
587                    IamAction("sqs:SendMessage".to_string()),
588                ],
589                Effect::Allow,
590            )
591            .principal(principal)
592            .condition(condition)
593            .resources(vec![arn.clone()])
594            .build();
595            let doc = PolicyDocumentBuilder::new(vec![statement]).build();
596            let queue_policy_id = Id::generate_id(&self.id, &format!("SQSDestinationPolicy{}", i));
597            let queue_policy_ref =
598                QueuePolicyBuilder::new_with_values(&queue_policy_id, doc, vec![reference.clone()])
599                    .build(stack_builder);
600
601            let notification_id = Id::generate_id(&self.id, format!("SQSNotification{}", i).as_str());
602            BucketNotificationBuilder::new(
603                &notification_id,
604                handler.get_arn(),
605                bucket.get_ref(),
606                event,
607                Some(queue_policy_ref.get_id()),
608            )
609            .sqs(arn)
610            .build(stack_builder);
611        }
612
613        (bucket, policy)
614    }
615
616    fn notification_handler(id: &Id, target: &str, num: usize, stack_builder: &mut StackBuilder) -> FunctionRef {
617        let handler_id = Id::generate_id(id, &format!("{}Handler{}", target, num));
618        let (handler, ..) = FunctionBuilder::new(
619            &handler_id,
620            Architecture::X86_64,
621            Memory(128),
622            Timeout(300),
623        )
624        .code(Code::Inline(BUCKET_NOTIFICATION_HANDLER_CODE.to_string()))
625        .handler("index.handler")
626        .runtime(Runtime::Python313)
627        .add_permission(Permission::Custom(CustomPermission::new(
628            "NotificationPermission",
629            StatementBuilder::new(vec![IamAction("s3:PutBucketNotification".to_string())], Effect::Allow)
630                .all_resources()
631                .build(),
632        )))
633        .build(stack_builder);
634        handler
635    }
636}
637
638impl BucketBuilder<WebsiteState> {
639    pub fn error_document<I: Into<String>>(self, error: I) -> Self {
640        Self {
641            error_document: Some(error.into()),
642            ..self
643        }
644    }
645
646    pub fn redirect_all<I: Into<String>>(self, hostname: I, protocol: Option<Protocol>) -> Self {
647        Self {
648            redirect_all_requests_to: Some((hostname.into(), protocol)),
649            ..self
650        }
651    }
652
653    pub fn cors_config(self, config: CorsConfiguration) -> Self {
654        Self {
655            cors_config: Some(config),
656            ..self
657        }
658    }
659
660    /// Builds the website bucket and adds it to the stack.
661    ///
662    /// Returns both the bucket and the automatically created bucket policy
663    /// that allows public read access.
664    pub fn build(self, stack_builder: &mut StackBuilder) -> (BucketRef, BucketPolicyRef) {
665        let (bucket, policy) = self.build_internal(true, stack_builder);
666        (bucket, policy.expect("for website, bucket policy should always be present"))
667    }
668}
669
670/// Builder for S3 CORS configuration.
671pub struct CorsConfigurationBuilder {
672    rules: Vec<CorsRule>,
673}
674
675impl CorsConfigurationBuilder {
676    pub fn new(rules: Vec<CorsRule>) -> CorsConfigurationBuilder {
677        CorsConfigurationBuilder { rules }
678    }
679
680    pub fn build(self) -> CorsConfiguration {
681        CorsConfiguration { cors_rules: self.rules }
682    }
683}
684
685/// Builder for individual CORS rules.
686pub struct CorsRuleBuilder {
687    allow_origins: Vec<String>,
688    allow_methods: Vec<HttpMethod>,
689    allow_headers: Option<Vec<String>>,
690    expose_headers: Option<Vec<String>>,
691    max_age: Option<u64>,
692}
693
694impl CorsRuleBuilder {
695    pub fn new<T: Into<String>>(allow_origins: Vec<T>, allow_methods: Vec<HttpMethod>) -> Self {
696        Self {
697            allow_origins: allow_origins.into_iter().map(Into::into).collect(),
698            allow_methods,
699            allow_headers: None,
700            expose_headers: None,
701            max_age: None,
702        }
703    }
704
705    pub fn allow_headers(self, headers: Vec<String>) -> Self {
706        Self {
707            allow_headers: Some(headers),
708            ..self
709        }
710    }
711
712    pub fn expose_headers(self, headers: Vec<String>) -> Self {
713        Self {
714            expose_headers: Some(headers),
715            ..self
716        }
717    }
718
719    pub fn max_age(self, age: Duration) -> Self {
720        Self {
721            max_age: Some(age.as_secs()),
722            ..self
723        }
724    }
725
726    #[must_use]
727    pub fn build(self) -> CorsRule {
728        CorsRule {
729            allowed_headers: self.allow_headers,
730            allowed_methods: self.allow_methods.into_iter().map(Into::into).collect(),
731            allowed_origins: self.allow_origins,
732            exposed_headers: self.expose_headers,
733            max_age: self.max_age,
734        }
735    }
736}
737
738#[derive(Debug, Clone)]
739pub enum TransitionDefaultMinimumObjectSize {
740    VariesByStorageClass,
741    AllStorageClasses128k,
742}
743
744impl From<TransitionDefaultMinimumObjectSize> for String {
745    fn from(value: TransitionDefaultMinimumObjectSize) -> Self {
746        match value {
747            TransitionDefaultMinimumObjectSize::VariesByStorageClass => "varies_by_storage_class".to_string(),
748            TransitionDefaultMinimumObjectSize::AllStorageClasses128k => "all_storage_classes_128K".to_string(),
749        }
750    }
751}
752
753#[derive(Debug, Clone)]
754pub enum LifecycleStorageClass {
755    IntelligentTiering,
756    OneZoneIA,
757    StandardIA,
758    GlacierDeepArchive,
759    Glacier,
760    GlacierInstantRetrieval,
761}
762
763impl From<LifecycleStorageClass> for String {
764    fn from(value: LifecycleStorageClass) -> Self {
765        match value {
766            LifecycleStorageClass::GlacierDeepArchive => "DEEP_ARCHIVE".to_string(),
767            LifecycleStorageClass::Glacier => "GLACIER".to_string(),
768            LifecycleStorageClass::GlacierInstantRetrieval => "GLACIER_IR".to_string(),
769            LifecycleStorageClass::IntelligentTiering => "INTELLIGENT_TIERING".to_string(),
770            LifecycleStorageClass::OneZoneIA => "ONEZONE_IA".to_string(),
771            LifecycleStorageClass::StandardIA => "STANDARD_IA".to_string(),
772        }
773    }
774}
775
776/// Builder for S3 lifecycle rule transitions.
777///
778/// Configures automatic transitions of objects to different storage classes.
779pub struct LifecycleRuleTransitionBuilder {
780    storage_class: LifecycleStorageClass,
781    transition_in_days: Option<u16>,
782}
783
784impl LifecycleRuleTransitionBuilder {
785    pub fn new(storage_class: LifecycleStorageClass) -> Self {
786        Self {
787            storage_class,
788            transition_in_days: None,
789        }
790    }
791
792    pub fn transition_in_days(self, days: LifecycleTransitionInDays) -> Self {
793        Self {
794            transition_in_days: Some(days.0),
795            ..self
796        }
797    }
798
799    #[must_use]
800    pub fn build(self) -> LifecycleRuleTransition {
801        LifecycleRuleTransition {
802            storage_class: self.storage_class.into(),
803            transition_in_days: self.transition_in_days.unwrap_or(0),
804        }
805    }
806}
807
808/// Builder for non-current version transitions in versioned buckets.
809///
810/// Configures automatic transitions for previous versions of objects.
811pub struct NonCurrentVersionTransitionBuilder {
812    storage_class: LifecycleStorageClass,
813    transition_in_days: u32,
814    newer_non_current_versions: Option<u32>,
815}
816
817impl NonCurrentVersionTransitionBuilder {
818    pub fn new(storage_class: LifecycleStorageClass, transition_in_days: u32) -> Self {
819        Self {
820            storage_class,
821            transition_in_days,
822            newer_non_current_versions: None,
823        }
824    }
825
826    pub fn newer_non_current_versions(self, versions: u32) -> Self {
827        Self {
828            newer_non_current_versions: Some(versions),
829            ..self
830        }
831    }
832
833    #[must_use]
834    pub fn build(self) -> NonCurrentVersionTransition {
835        NonCurrentVersionTransition {
836            storage_class: self.storage_class.into(),
837            transition_in_days: self.transition_in_days,
838            newer_non_current_versions: self.newer_non_current_versions,
839        }
840    }
841}
842
843#[derive(Debug, Clone)]
844pub enum LifecycleRuleStatus {
845    Enabled,
846    Disabled,
847}
848
849impl From<LifecycleRuleStatus> for String {
850    fn from(value: LifecycleRuleStatus) -> Self {
851        match value {
852            LifecycleRuleStatus::Enabled => "Enabled".to_string(),
853            LifecycleRuleStatus::Disabled => "Disabled".to_string(),
854        }
855    }
856}
857
858/// Builder for S3 lifecycle rules.
859///
860/// Defines rules for automatic object expiration and transitions between storage classes.
861pub struct LifecycleRuleBuilder {
862    id: Option<String>,
863    status: LifecycleRuleStatus,
864    expiration_in_days: Option<u16>, // expiration must be > than expiration in transition (ow boy...)
865    prefix: Option<String>,
866    object_size_greater_than: Option<u32>,
867    object_size_less_than: Option<u32>,
868    abort_incomplete_multipart_upload: Option<u16>,
869    non_current_version_expiration: Option<u16>,
870    transitions: Option<Vec<LifecycleRuleTransition>>,
871    non_current_version_transitions: Option<Vec<NonCurrentVersionTransition>>,
872}
873
874impl LifecycleRuleBuilder {
875    pub fn new(status: LifecycleRuleStatus) -> Self {
876        Self {
877            status,
878            id: None,
879            expiration_in_days: None,
880            prefix: None,
881            object_size_greater_than: None,
882            object_size_less_than: None,
883            abort_incomplete_multipart_upload: None,
884            non_current_version_expiration: None,
885            transitions: None,
886            non_current_version_transitions: None,
887        }
888    }
889
890    pub fn id<T: Into<String>>(self, id: T) -> Self {
891        Self {
892            id: Some(id.into()),
893            ..self
894        }
895    }
896
897    pub fn expiration_in_days(self, days: u16) -> Self {
898        Self {
899            expiration_in_days: Some(days),
900            ..self
901        }
902    }
903
904    pub fn prefix<T: Into<String>>(self, prefix: T) -> Self {
905        Self {
906            prefix: Some(prefix.into()),
907            ..self
908        }
909    }
910
911    pub fn object_size(self, sizes: S3LifecycleObjectSizes) -> Self {
912        Self {
913            object_size_less_than: sizes.0,
914            object_size_greater_than: sizes.1,
915            ..self
916        }
917    }
918
919    pub fn abort_incomplete_multipart_upload(self, days: u16) -> Self {
920        Self {
921            abort_incomplete_multipart_upload: Some(days),
922            ..self
923        }
924    }
925
926    pub fn non_current_version_expiration(self, days: u16) -> Self {
927        Self {
928            non_current_version_expiration: Some(days),
929            ..self
930        }
931    }
932
933    pub fn add_transition(mut self, transition: LifecycleRuleTransition) -> Self {
934        if let Some(mut transitions) = self.transitions {
935            transitions.push(transition);
936            self.transitions = Some(transitions);
937        } else {
938            self.transitions = Some(vec![transition]);
939        }
940
941        Self { ..self }
942    }
943
944    pub fn add_non_current_version_transitions(mut self, transition: NonCurrentVersionTransition) -> Self {
945        if let Some(mut transitions) = self.non_current_version_transitions {
946            transitions.push(transition);
947            self.non_current_version_transitions = Some(transitions);
948        } else {
949            self.non_current_version_transitions = Some(vec![transition]);
950        }
951
952        Self { ..self }
953    }
954
955    pub fn build(self) -> LifecycleRule {
956        LifecycleRule {
957            id: self.id,
958            status: self.status.into(),
959            expiration_in_days: self.expiration_in_days,
960            prefix: self.prefix,
961            object_size_greater_than: self.object_size_greater_than,
962            object_size_less_than: self.object_size_less_than,
963            transitions: self.transitions,
964            abort_incomplete_multipart_upload: self.abort_incomplete_multipart_upload,
965            non_current_version_expiration: self.non_current_version_expiration,
966            non_current_version_transitions: self.non_current_version_transitions,
967        }
968    }
969}
970
971/// Builder for S3 lifecycle configuration.
972///
973/// Combines multiple lifecycle rules into a configuration for a bucket.
974pub struct LifecycleConfigurationBuilder {
975    rules: Vec<LifecycleRule>,
976    transition_minimum_size: Option<TransitionDefaultMinimumObjectSize>,
977}
978
979impl Default for LifecycleConfigurationBuilder {
980    fn default() -> Self {
981        Self::new()
982    }
983}
984
985impl LifecycleConfigurationBuilder {
986    pub fn new() -> Self {
987        Self {
988            rules: vec![],
989            transition_minimum_size: None,
990        }
991    }
992
993    pub fn transition_minimum_size(self, size: TransitionDefaultMinimumObjectSize) -> Self {
994        Self {
995            transition_minimum_size: Some(size),
996            ..self
997        }
998    }
999
1000    pub fn add_rule(mut self, rule: LifecycleRule) -> Self {
1001        self.rules.push(rule);
1002        self
1003    }
1004
1005    #[must_use]
1006    pub fn build(self) -> LifecycleConfiguration {
1007        LifecycleConfiguration {
1008            rules: self.rules,
1009            transition_minimum_size: self.transition_minimum_size.map(|v| v.into()),
1010        }
1011    }
1012}
1013
1014/// Builder for S3 public access block configuration.
1015///
1016/// Controls public access to the bucket at the bucket level.
1017pub struct PublicAccessBlockConfigurationBuilder {
1018    block_public_acls: Option<bool>,
1019    block_public_policy: Option<bool>,
1020    ignore_public_acls: Option<bool>,
1021    restrict_public_buckets: Option<bool>,
1022}
1023
1024impl Default for PublicAccessBlockConfigurationBuilder {
1025    fn default() -> Self {
1026        Self::new()
1027    }
1028}
1029
1030impl PublicAccessBlockConfigurationBuilder {
1031    pub fn new() -> Self {
1032        Self {
1033            block_public_acls: None,
1034            block_public_policy: None,
1035            ignore_public_acls: None,
1036            restrict_public_buckets: None,
1037        }
1038    }
1039
1040    pub fn block_public_acls(self, config: bool) -> Self {
1041        Self {
1042            block_public_acls: Some(config),
1043            ..self
1044        }
1045    }
1046
1047    pub fn block_public_policy(self, config: bool) -> Self {
1048        Self {
1049            block_public_policy: Some(config),
1050            ..self
1051        }
1052    }
1053
1054    pub fn ignore_public_acls(self, config: bool) -> Self {
1055        Self {
1056            ignore_public_acls: Some(config),
1057            ..self
1058        }
1059    }
1060
1061    pub fn restrict_public_buckets(self, config: bool) -> Self {
1062        Self {
1063            restrict_public_buckets: Some(config),
1064            ..self
1065        }
1066    }
1067
1068    #[must_use]
1069    pub fn build(self) -> PublicAccessBlockConfiguration {
1070        PublicAccessBlockConfiguration {
1071            block_public_acls: self.block_public_acls,
1072            block_public_policy: self.block_public_policy,
1073            ignore_public_acls: self.ignore_public_acls,
1074            restrict_public_buckets: self.restrict_public_buckets,
1075        }
1076    }
1077}
1078
1079pub enum IntelligentTieringStatus {
1080    Enabled,
1081    Disabled,
1082}
1083
1084impl From<IntelligentTieringStatus> for String {
1085    fn from(value: IntelligentTieringStatus) -> String {
1086        match value {
1087            IntelligentTieringStatus::Enabled => "Enabled".to_string(),
1088            IntelligentTieringStatus::Disabled => "Disabled".to_string(),
1089        }
1090    }
1091}
1092
1093pub struct TagFilterBuilder {
1094    key: String,
1095    value: String,
1096}
1097
1098impl TagFilterBuilder {
1099    pub fn new<T: Into<String>>(key: T, value: T) -> Self {
1100        Self {
1101            key: key.into(),
1102            value: value.into(),
1103        }
1104    }
1105    
1106    pub fn build(self) -> TagFilter {
1107        TagFilter {
1108            key: self.key,
1109            value: self.value,
1110        }
1111    }
1112}
1113
1114pub struct IntelligentTieringConfigurationBuilder {
1115    id: String,
1116    status: String,
1117    prefix: Option<String>,
1118    tag_filters: Option<Vec<TagFilter>>,
1119    tierings: Vec<Tiering>,
1120}
1121
1122impl IntelligentTieringConfigurationBuilder {
1123    pub fn new(id: &str, status: IntelligentTieringStatus, tierings: Vec<BucketTiering>) -> Self {
1124        IntelligentTieringConfigurationBuilder {
1125            id: id.to_string(),
1126            status: status.into(),
1127            prefix: None,
1128            tag_filters: None,
1129            tierings: tierings.into_iter().map(|t| {
1130                Tiering {
1131                    access_tier: t.0,
1132                    days: t.1,
1133                }
1134            }).collect(),
1135        }
1136    }
1137    
1138    pub fn prefix<T: Into<String>>(self, prefix: T) -> Self {
1139        Self {
1140            prefix: Some(prefix.into()),
1141            ..self
1142        }
1143    }
1144    
1145    pub fn add_tag_filter(mut self, tag_filter: TagFilter) -> Self {
1146        if let Some(mut filters) = self.tag_filters {
1147            filters.push(tag_filter);
1148            self.tag_filters = Some(filters);
1149        } else {
1150            self.tag_filters = Some(vec![tag_filter]);
1151        }
1152        
1153        self
1154    }
1155
1156    pub fn build(self) -> IntelligentTieringConfiguration {
1157        IntelligentTieringConfiguration {
1158            id: self.id,
1159            prefix: self.prefix,
1160            status: self.status,
1161            tag_filters: self.tag_filters,
1162            tierings: self.tierings,
1163        }
1164    }
1165}
1166
1167pub enum TableBucketType {
1168    Aws,
1169    Customer,
1170}
1171
1172impl From<TableBucketType> for String {
1173    fn from(value: TableBucketType) -> String {
1174        match value {
1175            TableBucketType::Aws => "aws".to_string(),
1176            TableBucketType::Customer => "customer".to_string(),
1177        }
1178    }
1179}
1180
1181pub enum Expiration {
1182    Enabled,
1183    Disabled,
1184}
1185
1186impl From<Expiration> for String {
1187    fn from(value: Expiration) -> String {
1188        match value {
1189            Expiration::Enabled => "ENABLED".to_string(),
1190            Expiration::Disabled => "DISABLED".to_string(),
1191        }
1192    }
1193}
1194
1195pub struct RecordExpirationBuilder {
1196    days: Option<u32>,
1197    expiration: String,
1198}
1199
1200impl RecordExpirationBuilder {
1201    pub fn new(expiration: Expiration) -> Self {
1202        Self {
1203            expiration: expiration.into(),
1204            days: None,
1205        }
1206    }
1207
1208    pub fn days(self, days: RecordExpirationDays) -> Self {
1209        Self {
1210            days: Some(days.0),
1211            ..self
1212        }
1213    }
1214
1215    pub fn build(self) -> RecordExpiration {
1216        RecordExpiration {
1217            days: self.days,
1218            expiration: self.expiration,
1219        }
1220    }
1221}
1222
1223pub struct JournalTableConfigurationBuilder {
1224    record_expiration: RecordExpiration,
1225    table_arn: Option<Value>,
1226    table_name: Option<String>,
1227}
1228
1229impl JournalTableConfigurationBuilder {
1230    pub fn new(record_expiration: RecordExpiration) -> Self {
1231        Self {
1232            record_expiration,
1233            table_arn: None,
1234            table_name: None,
1235        }
1236    }
1237
1238    pub fn table_name<T: Into<String>>(self, name: T) -> Self {
1239        Self {
1240            table_name: Some(name.into()),
1241           ..self
1242        }
1243    }
1244
1245    pub fn table_arn(self, arn: Value) -> Self {
1246        Self {
1247            table_arn: Some(arn),
1248           ..self
1249        }
1250    }
1251
1252    pub fn build(self) -> JournalTableConfiguration {
1253        JournalTableConfiguration {
1254            record_expiration: self.record_expiration,
1255            table_arn: self.table_arn,
1256            table_name: self.table_name,
1257        }
1258    }
1259}
1260
1261pub struct MetadataDestinationBuilder {
1262    table_bucket_type: String,
1263    table_bucket_arn: Option<Value>,
1264    table_namespace: Option<String>,
1265}
1266
1267impl MetadataDestinationBuilder {
1268    pub fn new(table_bucket_type: TableBucketType) -> Self {
1269        Self {
1270            table_bucket_type: table_bucket_type.into(),
1271            table_bucket_arn: None,
1272            table_namespace: None,
1273        }
1274    }
1275
1276    pub fn table_bucket_arn(self, table_bucket_arn: Value) -> Self {
1277        Self {
1278            table_bucket_arn: Some(table_bucket_arn),
1279            ..self
1280        }
1281    }
1282
1283    pub fn table_namespace<T: Into<String>>(self, table_namespace: T) -> Self {
1284        Self {
1285            table_namespace: Some(table_namespace.into()),
1286            ..self
1287        }
1288    }
1289
1290    pub fn build(self) -> MetadataDestination {
1291        MetadataDestination {
1292            table_bucket_arn: self.table_bucket_arn,
1293            table_bucket_type: self.table_bucket_type,
1294            table_namespace: self.table_namespace,
1295        }
1296    }
1297}
1298
1299pub enum ConfigurationState {
1300    Enabled,
1301    Disabled,
1302}
1303
1304impl From<ConfigurationState> for String {
1305    fn from(value: ConfigurationState) -> String {
1306        match value {
1307            ConfigurationState::Enabled => "ENABLED".to_string(),
1308            ConfigurationState::Disabled => "DISABLED".to_string(),
1309        }
1310    }
1311}
1312
1313pub struct InventoryTableConfigurationBuilder {
1314    configuration_state: String,
1315    table_arn: Option<Value>,
1316    table_name: Option<String>,
1317}
1318
1319impl InventoryTableConfigurationBuilder {
1320    pub fn new(configuration_state: ConfigurationState) -> Self {
1321        Self {
1322            configuration_state: configuration_state.into(),
1323            table_arn: None,
1324            table_name: None,
1325        }
1326    }
1327
1328    pub fn table_arn(self, table_arn: Value) -> Self {
1329        Self {
1330            table_arn: Some(table_arn),
1331            ..self
1332        }
1333    }
1334
1335    pub fn table_name<T: Into<String>>(self, table_name: T) -> Self {
1336        Self {
1337            table_name: Some(table_name.into()),
1338            ..self
1339        }
1340    }
1341
1342    pub fn build(self) -> InventoryTableConfiguration {
1343        InventoryTableConfiguration {
1344            configuration_state: self.configuration_state,
1345            table_arn: self.table_arn,
1346            table_name: self.table_name,
1347        }
1348    }
1349}
1350
1351pub struct MetadataConfigurationBuilder {
1352    destination: Option<MetadataDestination>,
1353    inventory_table_configuration: Option<InventoryTableConfiguration>,
1354    journal_table_configuration: JournalTableConfiguration,
1355}
1356
1357impl MetadataConfigurationBuilder {
1358    pub fn new(journal_table_configuration: JournalTableConfiguration) -> Self {
1359        MetadataConfigurationBuilder {
1360            journal_table_configuration,
1361            destination: None,
1362            inventory_table_configuration: None,
1363        }
1364    }
1365
1366    pub fn destination(self, destination: MetadataDestination) -> Self {
1367        Self {
1368            destination: Some(destination),
1369            ..self
1370        }
1371    }
1372
1373    pub fn inventory_table_configuration(self, inventory_table_configuration: InventoryTableConfiguration) -> Self {
1374        Self {
1375            inventory_table_configuration: Some(inventory_table_configuration),
1376            ..self
1377        }
1378    }
1379
1380    pub fn build(self) -> MetadataConfiguration {
1381        MetadataConfiguration {
1382            destination: self.destination,
1383            inventory_table_configuration: self.inventory_table_configuration,
1384            journal_table_configuration: self.journal_table_configuration,
1385        }
1386    }
1387}