1use crate::iam::{Effect, PolicyDocument, PolicyDocumentBuilder, PrincipalBuilder, StatementBuilder};
2use crate::intrinsic::join;
3use crate::s3::dto;
4use crate::s3::{
5 Bucket, BucketEncryption, BucketPolicy, BucketPolicyRef, BucketProperties, BucketRef, CorsConfiguration, CorsRule,
6 LifecycleConfiguration, LifecycleRule, LifecycleRuleTransition, NonCurrentVersionTransition, PublicAccessBlockConfiguration,
7 RedirectAllRequestsTo, S3BucketPolicyProperties, ServerSideEncryptionByDefault, ServerSideEncryptionRule, WebsiteConfiguration,
8};
9use crate::shared::http::{HttpMethod, Protocol};
10use crate::shared::Id;
11use crate::stack::{Resource, StackBuilder};
12use crate::wrappers::{BucketName, IamAction, LifecycleTransitionInDays, S3LifecycleObjectSizes};
13use serde_json::Value;
14use std::marker::PhantomData;
15use std::time::Duration;
16use crate::type_state;
17
18pub struct BucketPolicyBuilder {
54 id: Id,
55 bucket_name: Value,
56 policy_document: PolicyDocument,
57}
58
59impl BucketPolicyBuilder {
60 pub fn new(id: &str, bucket: &BucketRef, policy_document: PolicyDocument) -> Self {
67 Self {
68 id: Id(id.to_string()),
69 bucket_name: bucket.get_ref(),
70 policy_document,
71 }
72 }
73
74 pub(crate) fn new_with_bucket_ref(id: &str, bucket_name: Value, policy_document: PolicyDocument) -> Self {
75 Self {
76 id: Id(id.to_string()),
77 bucket_name,
78 policy_document,
79 }
80 }
81
82 pub(crate) fn raw_build(self) -> (String, BucketPolicy) {
83 let resource_id = Resource::generate_id("S3BucketPolicy");
84 let policy = BucketPolicy {
85 id: self.id,
86 resource_id: resource_id.to_string(),
87 r#type: "AWS::S3::BucketPolicy".to_string(),
88 properties: S3BucketPolicyProperties {
89 bucket_name: self.bucket_name,
90 policy_document: self.policy_document,
91 },
92 };
93 (resource_id, policy)
94 }
95
96 pub fn build(self, stack_builder: &mut StackBuilder) -> BucketPolicyRef {
97 let (resource_id, policy) = self.raw_build();
98 stack_builder.add_resource(policy);
99 BucketPolicyRef::new(resource_id)
100 }
101}
102
103pub enum VersioningConfiguration {
104 Enabled,
105 Suspended,
106}
107
108impl From<VersioningConfiguration> for String {
109 fn from(value: VersioningConfiguration) -> Self {
110 match value {
111 VersioningConfiguration::Enabled => "Enabled".to_string(),
112 VersioningConfiguration::Suspended => "Suspended".to_string(),
113 }
114 }
115}
116
117pub enum Encryption {
118 S3Managed,
119 KmsManaged,
120 DsseManaged,
121 }
124
125impl From<Encryption> for String {
126 fn from(value: Encryption) -> Self {
127 match value {
128 Encryption::S3Managed => "AES256".to_string(),
129 Encryption::KmsManaged => "aws:kms".to_string(),
130 Encryption::DsseManaged => "aws:kms:dsse".to_string(),
131 }
132 }
133}
134
135type_state!(
136 BucketBuilderState,
137 StartState,
138 WebsiteState,
139);
140
141pub struct BucketBuilder<T: BucketBuilderState> {
169 phantom_data: PhantomData<T>,
170 id: Id,
171 name: Option<String>,
172 access: Option<PublicAccessBlockConfiguration>,
173 versioning_configuration: Option<VersioningConfiguration>,
174 lifecycle_configuration: Option<LifecycleConfiguration>,
175 index_document: Option<String>,
176 error_document: Option<String>,
177 redirect_all_requests_to: Option<(String, Option<Protocol>)>,
178 cors_config: Option<CorsConfiguration>,
179 bucket_encryption: Option<Encryption>,
180}
181
182impl BucketBuilder<StartState> {
183 pub fn new(id: &str) -> Self {
188 Self {
189 id: Id(id.to_string()),
190 phantom_data: Default::default(),
191 name: None,
192 access: None,
193 versioning_configuration: None,
194 lifecycle_configuration: None,
195 index_document: None,
196 error_document: None,
197 redirect_all_requests_to: None,
198 cors_config: None,
199 bucket_encryption: None,
200 }
201 }
202
203 pub fn build(self, stack_builder: &mut StackBuilder) -> BucketRef {
204 let (bucket, _) = self.build_internal(false, stack_builder);
205 bucket
206 }
207}
208
209impl<T: BucketBuilderState> BucketBuilder<T> {
210 pub fn name(self, name: BucketName) -> Self {
211 Self {
212 name: Some(name.0),
213 ..self
214 }
215 }
216
217 pub fn versioning_configuration(self, config: VersioningConfiguration) -> Self {
218 Self {
219 versioning_configuration: Some(config),
220 ..self
221 }
222 }
223
224 pub fn lifecycle_configuration(self, config: LifecycleConfiguration) -> Self {
225 Self {
226 lifecycle_configuration: Some(config),
227 ..self
228 }
229 }
230
231 pub fn public_access_block_configuration(self, access: PublicAccessBlockConfiguration) -> Self {
232 Self {
233 access: Some(access),
234 ..self
235 }
236 }
237
238 pub fn encryption(self, encryption: Encryption) -> Self {
239 Self {
240 bucket_encryption: Some(encryption),
241 ..self
242 }
243 }
244
245 pub fn website<I: Into<String>>(self, index_document: I) -> BucketBuilder<WebsiteState> {
250 BucketBuilder {
251 phantom_data: Default::default(),
252 id: self.id,
253 name: self.name,
254 access: self.access,
255 versioning_configuration: self.versioning_configuration,
256 lifecycle_configuration: self.lifecycle_configuration,
257 index_document: Some(index_document.into()),
258 error_document: self.error_document,
259 redirect_all_requests_to: self.redirect_all_requests_to,
260 cors_config: self.cors_config,
261 bucket_encryption: self.bucket_encryption,
262 }
263 }
264
265 fn build_internal(self, website: bool, stack_builder: &mut StackBuilder) -> (BucketRef, Option<BucketPolicyRef>) {
266 let resource_id = Resource::generate_id("S3Bucket");
267
268 let versioning_configuration = self
269 .versioning_configuration
270 .map(|c| dto::VersioningConfig { status: c.into() });
271
272 let website_configuration = if website {
273 let redirect_all_requests_to = self.redirect_all_requests_to.map(|r| RedirectAllRequestsTo {
274 host_name: r.0,
275 protocol: r.1.map(Into::into),
276 });
277
278 Some(WebsiteConfiguration {
279 index_document: self.index_document,
280 error_document: self.error_document,
281 redirect_all_requests_to,
282 })
283 } else {
284 None
285 };
286
287 let access = if self.access.is_none() && website {
288 Some(PublicAccessBlockConfiguration {
290 block_public_acls: Some(false),
291 block_public_policy: Some(false),
292 ignore_public_acls: Some(false),
293 restrict_public_buckets: Some(false),
294 })
295 } else {
296 self.access
297 };
298
299 let encryption = self.bucket_encryption.map(|v| {
300 let rule = ServerSideEncryptionRule {
301 server_side_encryption_by_default: ServerSideEncryptionByDefault {
302 sse_algorithm: v.into(),
303 kms_master_key_id: None,
304 },
305 bucket_key_enabled: None,
306 };
307
308 BucketEncryption {
309 server_side_encryption_configuration: vec![rule],
310 }
311 });
312
313 let properties = BucketProperties {
314 bucket_name: self.name,
315 cors_configuration: self.cors_config,
316 lifecycle_configuration: self.lifecycle_configuration,
317 public_access_block_configuration: access,
318 versioning_configuration,
319 website_configuration,
320 bucket_encryption: encryption,
321 notification_configuration: None,
322 };
323
324 stack_builder.add_resource(Bucket {
325 id: self.id.clone(),
326 resource_id: resource_id.clone(),
327 r#type: "AWS::S3::Bucket".to_string(),
328 properties,
329 });
330
331 let bucket = BucketRef::new(resource_id);
332
333 let policy = if website {
334 let bucket_resource = vec![join("", vec![bucket.get_arn(), Value::String("/*".to_string())])];
336 let statement = StatementBuilder::new(vec![IamAction("s3:GetObject".to_string())], Effect::Allow)
337 .resources(bucket_resource)
338 .principal(PrincipalBuilder::new().normal("*").build())
339 .build();
340 let policy_doc = PolicyDocumentBuilder::new(vec![statement]).build();
341 let bucket_policy_id = format!("{}-website-s3-policy", self.id);
342 let s3_policy = BucketPolicyBuilder::new(bucket_policy_id.as_str(), &bucket, policy_doc).build(stack_builder);
343 Some(s3_policy)
344 } else {
345 None
346 };
347
348 (bucket, policy)
349 }
350}
351
352impl BucketBuilder<WebsiteState> {
353 pub fn error_document<I: Into<String>>(self, error: I) -> Self {
354 Self {
355 error_document: Some(error.into()),
356 ..self
357 }
358 }
359
360 pub fn redirect_all<I: Into<String>>(self, hostname: I, protocol: Option<Protocol>) -> Self {
361 Self {
362 redirect_all_requests_to: Some((hostname.into(), protocol)),
363 ..self
364 }
365 }
366
367 pub fn cors_config(self, config: CorsConfiguration) -> Self {
368 Self {
369 cors_config: Some(config),
370 ..self
371 }
372 }
373
374 pub fn build(self, stack_builder: &mut StackBuilder) -> (BucketRef, BucketPolicyRef) {
379 let (bucket, policy) = self.build_internal(true, stack_builder);
380 (bucket, policy.expect("for website, bucket policy should always be present"))
381 }
382}
383
384pub struct CorsConfigurationBuilder {
386 rules: Vec<CorsRule>
387}
388
389impl CorsConfigurationBuilder {
390 pub fn new(rules: Vec<CorsRule>) -> CorsConfigurationBuilder {
391 CorsConfigurationBuilder { rules }
392 }
393
394 pub fn build(self) -> CorsConfiguration {
395 CorsConfiguration {
396 cors_rules: self.rules,
397 }
398 }
399}
400
401pub struct CorsRuleBuilder {
403 allow_origins: Vec<String>,
404 allow_methods: Vec<HttpMethod>,
405 allow_headers: Option<Vec<String>>,
406 expose_headers: Option<Vec<String>>,
407 max_age: Option<u64>,
408}
409
410impl CorsRuleBuilder {
411 pub fn new<T: Into<String>>(allow_origins: Vec<T>, allow_methods: Vec<HttpMethod>) -> Self {
412 Self {
413 allow_origins: allow_origins.into_iter().map(Into::into).collect(),
414 allow_methods,
415 allow_headers: None,
416 expose_headers: None,
417 max_age: None,
418 }
419 }
420
421 pub fn allow_headers(self, headers: Vec<String>) -> Self {
422 Self {
423 allow_headers: Some(headers),
424 ..self
425 }
426 }
427
428 pub fn expose_headers(self, headers: Vec<String>) -> Self {
429 Self {
430 expose_headers: Some(headers),
431 ..self
432 }
433 }
434
435 pub fn max_age(self, age: Duration) -> Self {
436 Self {
437 max_age: Some(age.as_secs()),
438 ..self
439 }
440 }
441
442 #[must_use]
443 pub fn build(self) -> CorsRule {
444 CorsRule {
445 allowed_headers: self.allow_headers,
446 allowed_methods: self.allow_methods.into_iter().map(Into::into).collect(),
447 allowed_origins: self.allow_origins,
448 exposed_headers: self.expose_headers,
449 max_age: self.max_age,
450 }
451 }
452}
453
454pub enum TransitionDefaultMinimumObjectSize {
455 VariesByStorageClass,
456 AllStorageClasses128k,
457}
458
459impl From<TransitionDefaultMinimumObjectSize> for String {
460 fn from(value: TransitionDefaultMinimumObjectSize) -> Self {
461 match value {
462 TransitionDefaultMinimumObjectSize::VariesByStorageClass => "varies_by_storage_class".to_string(),
463 TransitionDefaultMinimumObjectSize::AllStorageClasses128k => "all_storage_classes_128K".to_string(),
464 }
465 }
466}
467
468pub enum LifecycleStorageClass {
469 IntelligentTiering,
470 OneZoneIA,
471 StandardIA,
472 GlacierDeepArchive,
473 Glacier,
474 GlacierInstantRetrieval,
475}
476
477impl From<LifecycleStorageClass> for String {
478 fn from(value: LifecycleStorageClass) -> Self {
479 match value {
480 LifecycleStorageClass::GlacierDeepArchive => "DEEP_ARCHIVE".to_string(),
481 LifecycleStorageClass::Glacier => "GLACIER".to_string(),
482 LifecycleStorageClass::GlacierInstantRetrieval => "GLACIER_IR".to_string(),
483 LifecycleStorageClass::IntelligentTiering => "INTELLIGENT_TIERING".to_string(),
484 LifecycleStorageClass::OneZoneIA => "ONEZONE_IA".to_string(),
485 LifecycleStorageClass::StandardIA => "STANDARD_IA".to_string(),
486 }
487 }
488}
489
490pub struct LifecycleRuleTransitionBuilder {
494 storage_class: LifecycleStorageClass,
495 transition_in_days: Option<u16>,
496}
497
498impl LifecycleRuleTransitionBuilder {
499 pub fn new(storage_class: LifecycleStorageClass) -> Self {
500 Self {
501 storage_class,
502 transition_in_days: None,
503 }
504 }
505
506 pub fn transition_in_days(self, days: LifecycleTransitionInDays) -> Self {
507 Self {
508 transition_in_days: Some(days.0),
509 ..self
510 }
511 }
512
513 #[must_use]
514 pub fn build(self) -> LifecycleRuleTransition {
515 LifecycleRuleTransition {
516 storage_class: self.storage_class.into(),
517 transition_in_days: self.transition_in_days.unwrap_or(0),
518 }
519 }
520}
521
522pub struct NonCurrentVersionTransitionBuilder {
526 storage_class: LifecycleStorageClass,
527 transition_in_days: u32,
528 newer_non_current_versions: Option<u32>,
529}
530
531impl NonCurrentVersionTransitionBuilder {
532 pub fn new(storage_class: LifecycleStorageClass, transition_in_days: u32) -> Self {
533 Self {
534 storage_class,
535 transition_in_days,
536 newer_non_current_versions: None,
537 }
538 }
539
540 pub fn newer_non_current_versions(self, versions: u32) -> Self {
541 Self {
542 newer_non_current_versions: Some(versions),
543 ..self
544 }
545 }
546
547 #[must_use]
548 pub fn build(self) -> NonCurrentVersionTransition {
549 NonCurrentVersionTransition {
550 storage_class: self.storage_class.into(),
551 transition_in_days: self.transition_in_days,
552 newer_non_current_versions: self.newer_non_current_versions,
553 }
554 }
555}
556
557pub enum LifecycleRuleStatus {
558 Enabled,
559 Disabled,
560}
561
562impl From<LifecycleRuleStatus> for String {
563 fn from(value: LifecycleRuleStatus) -> Self {
564 match value {
565 LifecycleRuleStatus::Enabled => "Enabled".to_string(),
566 LifecycleRuleStatus::Disabled => "Disabled".to_string(),
567 }
568 }
569}
570
571pub struct LifecycleRuleBuilder {
575 id: Option<String>,
576 status: LifecycleRuleStatus,
577 expiration_in_days: Option<u16>, prefix: Option<String>,
579 object_size_greater_than: Option<u32>,
580 object_size_less_than: Option<u32>,
581 abort_incomplete_multipart_upload: Option<u16>,
582 non_current_version_expiration: Option<u16>,
583 transitions: Option<Vec<LifecycleRuleTransition>>,
584 non_current_version_transitions: Option<Vec<NonCurrentVersionTransition>>,
585}
586
587impl LifecycleRuleBuilder {
588 pub fn new(status: LifecycleRuleStatus) -> Self {
589 Self {
590 status,
591 id: None,
592 expiration_in_days: None,
593 prefix: None,
594 object_size_greater_than: None,
595 object_size_less_than: None,
596 abort_incomplete_multipart_upload: None,
597 non_current_version_expiration: None,
598 transitions: None,
599 non_current_version_transitions: None,
600 }
601 }
602
603 pub fn id<T: Into<String>>(self, id: T) -> Self {
604 Self {
605 id: Some(id.into()),
606 ..self
607 }
608 }
609
610 pub fn expiration_in_days(self, days: u16) -> Self {
611 Self {
612 expiration_in_days: Some(days),
613 ..self
614 }
615 }
616
617 pub fn prefix<T: Into<String>>(self, prefix: T) -> Self {
618 Self {
619 prefix: Some(prefix.into()),
620 ..self
621 }
622 }
623
624 pub fn object_size(self, sizes: S3LifecycleObjectSizes) -> Self {
625 Self {
626 object_size_less_than: sizes.0,
627 object_size_greater_than: sizes.1,
628 ..self
629 }
630 }
631
632 pub fn abort_incomplete_multipart_upload(self, days: u16) -> Self {
633 Self {
634 abort_incomplete_multipart_upload: Some(days),
635 ..self
636 }
637 }
638
639 pub fn non_current_version_expiration(self, days: u16) -> Self {
640 Self {
641 non_current_version_expiration: Some(days),
642 ..self
643 }
644 }
645
646 pub fn add_transition(mut self, transition: LifecycleRuleTransition) -> Self {
647 if let Some(mut transitions) = self.transitions {
648 transitions.push(transition);
649 self.transitions = Some(transitions);
650 } else {
651 self.transitions = Some(vec![transition]);
652 }
653
654 Self { ..self }
655 }
656
657 pub fn add_non_current_version_transitions(mut self, transition: NonCurrentVersionTransition) -> Self {
658 if let Some(mut transitions) = self.non_current_version_transitions {
659 transitions.push(transition);
660 self.non_current_version_transitions = Some(transitions);
661 } else {
662 self.non_current_version_transitions = Some(vec![transition]);
663 }
664
665 Self { ..self }
666 }
667
668 pub fn build(self) -> LifecycleRule {
669 LifecycleRule {
670 id: self.id,
671 status: self.status.into(),
672 expiration_in_days: self.expiration_in_days,
673 prefix: self.prefix,
674 object_size_greater_than: self.object_size_greater_than,
675 object_size_less_than: self.object_size_less_than,
676 transitions: self.transitions,
677 abort_incomplete_multipart_upload: self.abort_incomplete_multipart_upload,
678 non_current_version_expiration: self.non_current_version_expiration,
679 non_current_version_transitions: self.non_current_version_transitions,
680 }
681 }
682}
683
684pub struct LifecycleConfigurationBuilder {
688 rules: Vec<LifecycleRule>,
689 transition_minimum_size: Option<TransitionDefaultMinimumObjectSize>,
690}
691
692impl Default for LifecycleConfigurationBuilder {
693 fn default() -> Self {
694 Self::new()
695 }
696}
697
698impl LifecycleConfigurationBuilder {
699 pub fn new() -> Self {
700 Self {
701 rules: vec![],
702 transition_minimum_size: None,
703 }
704 }
705
706 pub fn transition_minimum_size(self, size: TransitionDefaultMinimumObjectSize) -> Self {
707 Self {
708 transition_minimum_size: Some(size),
709 ..self
710 }
711 }
712
713 pub fn add_rule(mut self, rule: LifecycleRule) -> Self {
714 self.rules.push(rule);
715 self
716 }
717
718 #[must_use]
719 pub fn build(self) -> LifecycleConfiguration {
720 LifecycleConfiguration {
721 rules: self.rules,
722 transition_minimum_size: self.transition_minimum_size.map(|v| v.into()),
723 }
724 }
725}
726
727pub struct PublicAccessBlockConfigurationBuilder {
731 block_public_acls: Option<bool>,
732 block_public_policy: Option<bool>,
733 ignore_public_acls: Option<bool>,
734 restrict_public_buckets: Option<bool>,
735}
736
737impl Default for PublicAccessBlockConfigurationBuilder {
738 fn default() -> Self {
739 Self::new()
740 }
741}
742
743impl PublicAccessBlockConfigurationBuilder {
744 pub fn new() -> Self {
745 Self {
746 block_public_acls: None,
747 block_public_policy: None,
748 ignore_public_acls: None,
749 restrict_public_buckets: None,
750 }
751 }
752
753 pub fn block_public_acls(self, config: bool) -> Self {
754 Self {
755 block_public_acls: Some(config),
756 ..self
757 }
758 }
759
760 pub fn block_public_policy(self, config: bool) -> Self {
761 Self {
762 block_public_policy: Some(config),
763 ..self
764 }
765 }
766
767 pub fn ignore_public_acls(self, config: bool) -> Self {
768 Self {
769 ignore_public_acls: Some(config),
770 ..self
771 }
772 }
773
774 pub fn restrict_public_buckets(self, config: bool) -> Self {
775 Self {
776 restrict_public_buckets: Some(config),
777 ..self
778 }
779 }
780
781 #[must_use]
782 pub fn build(self) -> PublicAccessBlockConfiguration {
783 PublicAccessBlockConfiguration {
784 block_public_acls: self.block_public_acls,
785 block_public_policy: self.block_public_policy,
786 ignore_public_acls: self.ignore_public_acls,
787 restrict_public_buckets: self.restrict_public_buckets,
788 }
789 }
790}