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