From cf6dd2671331bc1aebc2fc88cab56b904148f676 Mon Sep 17 00:00:00 2001 From: angie pinilla Date: Fri, 4 Feb 2022 21:19:02 -0500 Subject: [PATCH] r/s3_bucket_acl: new resource (#22853) * r/s3_bucket_acl: new resource * Update CHANGELOG for #22853 * r/s3_bucket: acctest fixes * add note about resource deletion * update acl resource names * update s3_bucket docs to use acl resource * r/s3_bucket_acl: add warning log when deleting from state * simplify resource ID parsing with regex; create resource ID with only comma separators; add id test coverage * remove unnecessary import ignore field --- .changelog/22853.txt | 3 + internal/provider/provider.go | 1 + internal/service/s3/bucket_acl.go | 507 +++++++++++++++ internal/service/s3/bucket_acl_test.go | 535 ++++++++++++++++ internal/service/s3/bucket_test.go | 694 +++++++++++++-------- website/docs/r/s3_bucket.html.markdown | 49 +- website/docs/r/s3_bucket_acl.html.markdown | 134 ++++ 7 files changed, 1669 insertions(+), 254 deletions(-) create mode 100644 .changelog/22853.txt create mode 100644 internal/service/s3/bucket_acl.go create mode 100644 internal/service/s3/bucket_acl_test.go create mode 100644 website/docs/r/s3_bucket_acl.html.markdown diff --git a/.changelog/22853.txt b/.changelog/22853.txt new file mode 100644 index 00000000000..8b647407b26 --- /dev/null +++ b/.changelog/22853.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_s3_bucket_acl +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 383f86dd6f2..ff75d0d1c21 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1624,6 +1624,7 @@ func Provider() *schema.Provider { "aws_s3_bucket": s3.ResourceBucket(), "aws_s3_bucket_accelerate_configuration": s3.ResourceBucketAccelerateConfiguration(), + "aws_s3_bucket_acl": s3.ResourceBucketAcl(), "aws_s3_bucket_analytics_configuration": s3.ResourceBucketAnalyticsConfiguration(), "aws_s3_bucket_cors_configuration": s3.ResourceBucketCorsConfiguration(), "aws_s3_bucket_intelligent_tiering_configuration": s3.ResourceBucketIntelligentTieringConfiguration(), diff --git a/internal/service/s3/bucket_acl.go b/internal/service/s3/bucket_acl.go new file mode 100644 index 00000000000..000e5ba2291 --- /dev/null +++ b/internal/service/s3/bucket_acl.go @@ -0,0 +1,507 @@ +package s3 + +import ( + "context" + "fmt" + "log" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +const BucketAclSeparator = "," + +func ResourceBucketAcl() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBucketAclCreate, + ReadContext: resourceBucketAclRead, + UpdateContext: resourceBucketAclUpdate, + DeleteContext: resourceBucketAclDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "access_control_policy": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + ConflictsWith: []string{"acl"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "grant": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "grantee": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "email_address": { + Type: schema.TypeString, + Optional: true, + }, + "display_name": { + Type: schema.TypeString, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Optional: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(s3.Type_Values(), false), + }, + "uri": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "permission": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(s3.Permission_Values(), false), + }, + }, + }, + }, + "owner": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "id": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "acl": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"access_control_policy"}, + ValidateFunc: validation.StringInSlice(BucketCannedACL_Values(), false), + }, + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 63), + }, + "expected_bucket_owner": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + }, + } +} + +func resourceBucketAclCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket := d.Get("bucket").(string) + expectedBucketOwner := d.Get("expected_bucket_owner").(string) + acl := d.Get("acl").(string) + + input := &s3.PutBucketAclInput{ + Bucket: aws.String(bucket), + } + + if acl != "" { + input.ACL = aws.String(acl) + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + if v, ok := d.GetOk("access_control_policy"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.AccessControlPolicy = expandBucketAclAccessControlPolicy(v.([]interface{})) + } + + _, err := verify.RetryOnAWSCode(s3.ErrCodeNoSuchBucket, func() (interface{}, error) { + return conn.PutBucketAclWithContext(ctx, input) + }) + + if err != nil { + return diag.FromErr(fmt.Errorf("error creating S3 bucket ACL for %s: %w", bucket, err)) + } + + d.SetId(BucketACLCreateResourceID(bucket, expectedBucketOwner, acl)) + + return resourceBucketAclRead(ctx, d, meta) +} + +func resourceBucketAclRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, acl, err := BucketACLParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.GetBucketAclInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + output, err := conn.GetBucketAclWithContext(ctx, input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, s3.ErrCodeNoSuchBucket) { + log.Printf("[WARN] S3 Bucket ACL (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error getting S3 bucket ACL (%s): %w", d.Id(), err)) + } + + if output == nil { + return diag.FromErr(fmt.Errorf("error getting S3 bucket ACL (%s): empty output", d.Id())) + } + + d.Set("acl", acl) + d.Set("bucket", bucket) + d.Set("expected_bucket_owner", expectedBucketOwner) + if err := d.Set("access_control_policy", flattenBucketAclAccessControlPolicy(output)); err != nil { + return diag.FromErr(fmt.Errorf("error setting access_control_policy: %w", err)) + } + + return nil +} + +func resourceBucketAclUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, acl, err := BucketACLParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + input := &s3.PutBucketAclInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + if d.HasChange("access_control_policy") { + input.AccessControlPolicy = expandBucketAclAccessControlPolicy(d.Get("access_control_policy").([]interface{})) + } + + if d.HasChange("acl") { + acl = d.Get("acl").(string) + input.ACL = aws.String(acl) + } + + _, err = conn.PutBucketAclWithContext(ctx, input) + + if err != nil { + return diag.FromErr(fmt.Errorf("error updating S3 bucket ACL (%s): %w", d.Id(), err)) + } + + if d.HasChange("acl") { + // Set new ACL value back in resource ID + d.SetId(BucketACLCreateResourceID(bucket, expectedBucketOwner, acl)) + } + + return resourceBucketAclRead(ctx, d, meta) +} + +func resourceBucketAclDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + log.Printf("[WARN] Cannot destroy S3 Bucket ACL. Terraform will remove this resource from the state file, however resources may remain.") + return nil +} + +func expandBucketAclAccessControlPolicy(l []interface{}) *s3.AccessControlPolicy { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.AccessControlPolicy{} + + if v, ok := tfMap["grant"].(*schema.Set); ok && v.Len() > 0 { + result.Grants = expandBucketAclAccessControlPolicyGrants(v.List()) + } + + if v, ok := tfMap["owner"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.Owner = expandBucketAclAccessControlPolicyOwner(v) + } + + return result +} + +func expandBucketAclAccessControlPolicyGrants(l []interface{}) []*s3.Grant { + var grants []*s3.Grant + + for _, tfMapRaw := range l { + tfMap, ok := tfMapRaw.(map[string]interface{}) + if !ok { + continue + } + + grant := &s3.Grant{} + + if v, ok := tfMap["grantee"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + grant.Grantee = expandBucketAclAccessControlPolicyGrantsGrantee(v) + } + + if v, ok := tfMap["permission"].(string); ok && v != "" { + grant.Permission = aws.String(v) + } + + grants = append(grants, grant) + } + + return grants +} + +func expandBucketAclAccessControlPolicyGrantsGrantee(l []interface{}) *s3.Grantee { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &s3.Grantee{} + + if v, ok := tfMap["email_address"].(string); ok && v != "" { + result.EmailAddress = aws.String(v) + } + + if v, ok := tfMap["id"].(string); ok && v != "" { + result.ID = aws.String(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + result.Type = aws.String(v) + } + + if v, ok := tfMap["uri"].(string); ok && v != "" { + result.URI = aws.String(v) + } + + return result +} + +func expandBucketAclAccessControlPolicyOwner(l []interface{}) *s3.Owner { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + owner := &s3.Owner{} + + if v, ok := tfMap["display_name"].(string); ok && v != "" { + owner.DisplayName = aws.String(v) + } + + if v, ok := tfMap["id"].(string); ok && v != "" { + owner.ID = aws.String(v) + } + + return owner +} + +func flattenBucketAclAccessControlPolicy(output *s3.GetBucketAclOutput) []interface{} { + if output == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if len(output.Grants) > 0 { + m["grant"] = flattenBucketAclAccessControlPolicyGrants(output.Grants) + } + + if output.Owner != nil { + m["owner"] = flattenBucketAclAccessControlPolicyOwner(output.Owner) + } + + return []interface{}{m} +} + +func flattenBucketAclAccessControlPolicyGrants(grants []*s3.Grant) []interface{} { + var results []interface{} + + for _, grant := range grants { + if grant == nil { + continue + } + + m := make(map[string]interface{}) + + if grant.Grantee != nil { + m["grantee"] = flattenBucketAclAccessControlPolicyGrantsGrantee(grant.Grantee) + } + + if grant.Permission != nil { + m["permission"] = aws.StringValue(grant.Permission) + } + + results = append(results, m) + } + + return results +} + +func flattenBucketAclAccessControlPolicyGrantsGrantee(grantee *s3.Grantee) []interface{} { + if grantee == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if grantee.DisplayName != nil { + m["display_name"] = aws.StringValue(grantee.DisplayName) + } + + if grantee.EmailAddress != nil { + m["email_address"] = aws.StringValue(grantee.EmailAddress) + } + + if grantee.ID != nil { + m["id"] = aws.StringValue(grantee.ID) + } + + if grantee.Type != nil { + m["type"] = aws.StringValue(grantee.Type) + } + + if grantee.URI != nil { + m["uri"] = aws.StringValue(grantee.URI) + } + + return []interface{}{m} +} + +func flattenBucketAclAccessControlPolicyOwner(owner *s3.Owner) []interface{} { + if owner == nil { + return []interface{}{} + } + + m := make(map[string]interface{}) + + if owner.DisplayName != nil { + m["display_name"] = aws.StringValue(owner.DisplayName) + } + + if owner.ID != nil { + m["id"] = aws.StringValue(owner.ID) + } + + return []interface{}{m} +} + +// BucketACLCreateResourceID is a method for creating an ID string +// with the bucket name and optional accountID and/or ACL. +func BucketACLCreateResourceID(bucket, expectedBucketOwner, acl string) string { + if expectedBucketOwner == "" { + if acl == "" { + return bucket + } + return strings.Join([]string{bucket, acl}, BucketAclSeparator) + } + + if acl == "" { + return strings.Join([]string{bucket, expectedBucketOwner}, BucketAclSeparator) + } + + return strings.Join([]string{bucket, expectedBucketOwner, acl}, BucketAclSeparator) +} + +// BucketACLParseResourceID is a method for parsing the ID string +// for the bucket name, accountID, and ACL if provided. +func BucketACLParseResourceID(id string) (string, string, string, error) { + // For only bucket name in the ID e.g. bucket + // ~> Bucket names can consist of only lowercase letters, numbers, dots, and hyphens; Max 63 characters + bucketRegex := regexp.MustCompile(`^[a-z0-9.-]{1,63}$`) + // For bucket and accountID in the ID e.g. bucket,123456789101 + // ~> Account IDs must consist of 12 digits + bucketAndOwnerRegex := regexp.MustCompile(`^[a-z0-9.-]{1,63},\d{12}$`) + // For bucket and ACL in the ID e.g. bucket,public-read + // ~> (Canned) ACL values include: private, public-read, public-read-write, authenticated-read, aws-exec-read, and log-delivery-write + bucketAndAclRegex := regexp.MustCompile(`^[a-z0-9.-]{1,63},[a-z-]+$`) + // For bucket, accountID, and ACL in the ID e.g. bucket,123456789101,public-read + bucketOwnerAclRegex := regexp.MustCompile(`^[a-z0-9.-]{1,63},\d{12},[a-z-]+$`) + + // Bucket name ONLY + if bucketRegex.MatchString(id) { + return id, "", "", nil + } + + // Bucket and Account ID ONLY + if bucketAndOwnerRegex.MatchString(id) { + parts := strings.Split(id, BucketAclSeparator) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", "", fmt.Errorf("unexpected format for ID (%s), expected BUCKET%sEXPECTED_BUCKET_OWNER", id, BucketAclSeparator) + } + return parts[0], parts[1], "", nil + } + + // Bucket and ACL ONLY + if bucketAndAclRegex.MatchString(id) { + parts := strings.Split(id, BucketAclSeparator) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", "", fmt.Errorf("unexpected format for ID (%s), expected BUCKET%sACL", id, BucketAclSeparator) + } + return parts[0], "", parts[1], nil + } + + // Bucket, Account ID, and ACL + if bucketOwnerAclRegex.MatchString(id) { + parts := strings.Split(id, BucketAclSeparator) + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return "", "", "", fmt.Errorf("unexpected format for ID (%s), expected BUCKET%[2]sEXPECTED_BUCKET_OWNER%[2]sACL", id, BucketAclSeparator) + } + return parts[0], parts[1], parts[2], nil + } + + return "", "", "", fmt.Errorf("unexpected format for ID (%s), expected BUCKET or BUCKET%[2]sEXPECTED_BUCKET_OWNER or BUCKET%[2]sACL "+ + "or BUCKET%[2]sEXPECTED_BUCKET_OWNER%[2]sACL", id, BucketAclSeparator) +} diff --git a/internal/service/s3/bucket_acl_test.go b/internal/service/s3/bucket_acl_test.go new file mode 100644 index 00000000000..7a706180815 --- /dev/null +++ b/internal/service/s3/bucket_acl_test.go @@ -0,0 +1,535 @@ +package s3_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfs3 "github.com/hashicorp/terraform-provider-aws/internal/service/s3" +) + +func TestBucketACLParseResourceID(t *testing.T) { + testCases := []struct { + TestName string + InputID string + ExpectError bool + ExpectedACL string + ExpectedBucket string + ExpectedBucketOwner string + }{ + { + TestName: "empty ID", + InputID: "", + ExpectError: true, + }, + { + TestName: "incorrect bucket and account ID format with slash separator", + InputID: "test/123456789012", + ExpectError: true, + }, + { + TestName: "incorrect bucket, account ID, and ACL format with slash separators", + InputID: "test/123456789012/private", + ExpectError: true, + }, + { + TestName: "incorrect bucket, account ID, and ACL format with mixed separators", + InputID: "test/123456789012,private", + ExpectError: true, + }, + { + TestName: "incorrect bucket, ACL, and account ID format", + InputID: "test,private,123456789012", + ExpectError: true, + }, + { + TestName: "valid ID with bucket", + InputID: tfs3.BucketACLCreateResourceID("example", "", ""), + ExpectedACL: "", + ExpectedBucket: "example", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket that has hyphens", + InputID: tfs3.BucketACLCreateResourceID("my-example-bucket", "", ""), + ExpectedACL: "", + ExpectedBucket: "my-example-bucket", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket that has dot and hyphens", + InputID: tfs3.BucketACLCreateResourceID("my-example.bucket", "", ""), + ExpectedACL: "", + ExpectedBucket: "my-example.bucket", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket that has dots, hyphen, and numbers", + InputID: tfs3.BucketACLCreateResourceID("my-example.bucket.4000", "", ""), + ExpectedACL: "", + ExpectedBucket: "my-example.bucket.4000", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket and acl", + InputID: tfs3.BucketACLCreateResourceID("example", "", s3.BucketCannedACLPrivate), + ExpectedACL: s3.BucketCannedACLPrivate, + ExpectedBucket: "example", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket and acl that has hyphens", + InputID: tfs3.BucketACLCreateResourceID("example", "", s3.BucketCannedACLPublicReadWrite), + ExpectedACL: s3.BucketCannedACLPublicReadWrite, + ExpectedBucket: "example", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket that has dot, hyphen, and number and acl that has hyphens", + InputID: tfs3.BucketACLCreateResourceID("my-example.bucket.4000", "", s3.BucketCannedACLPublicReadWrite), + ExpectedACL: s3.BucketCannedACLPublicReadWrite, + ExpectedBucket: "my-example.bucket.4000", + ExpectedBucketOwner: "", + }, + { + TestName: "valid ID with bucket and bucket owner", + InputID: tfs3.BucketACLCreateResourceID("example", "123456789012", ""), + ExpectedACL: "", + ExpectedBucket: "example", + ExpectedBucketOwner: "123456789012", + }, + { + TestName: "valid ID with bucket that has dot, hyphen, and number and bucket owner", + InputID: tfs3.BucketACLCreateResourceID("my-example.bucket.4000", "123456789012", ""), + ExpectedACL: "", + ExpectedBucket: "my-example.bucket.4000", + ExpectedBucketOwner: "123456789012", + }, + { + TestName: "valid ID with bucket, bucket owner, and acl", + InputID: tfs3.BucketACLCreateResourceID("example", "123456789012", s3.BucketCannedACLPrivate), + ExpectedACL: s3.BucketCannedACLPrivate, + ExpectedBucket: "example", + ExpectedBucketOwner: "123456789012", + }, + { + TestName: "valid ID with bucket, bucket owner, and acl that has hyphens", + InputID: tfs3.BucketACLCreateResourceID("example", "123456789012", s3.BucketCannedACLPublicReadWrite), + ExpectedACL: s3.BucketCannedACLPublicReadWrite, + ExpectedBucket: "example", + ExpectedBucketOwner: "123456789012", + }, + { + TestName: "valid ID with bucket that has dot, hyphen, and numbers, bucket owner, and acl that has hyphens", + InputID: tfs3.BucketACLCreateResourceID("my-example.bucket.4000", "123456789012", s3.BucketCannedACLPublicReadWrite), + ExpectedACL: s3.BucketCannedACLPublicReadWrite, + ExpectedBucket: "my-example.bucket.4000", + ExpectedBucketOwner: "123456789012", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotBucket, gotExpectedBucketOwner, gotAcl, err := tfs3.BucketACLParseResourceID(testCase.InputID) + + if err == nil && testCase.ExpectError { + t.Fatalf("expected error") + } + + if err != nil && !testCase.ExpectError { + t.Fatalf("unexpected error: %s", err) + } + + if gotAcl != testCase.ExpectedACL { + t.Errorf("got ACL %s, expected %s", gotAcl, testCase.ExpectedACL) + } + + if gotBucket != testCase.ExpectedBucket { + t.Errorf("got bucket %s, expected %s", gotBucket, testCase.ExpectedBucket) + } + + if gotExpectedBucketOwner != testCase.ExpectedBucketOwner { + t.Errorf("got ExpectedBucketOwner %s, expected %s", gotExpectedBucketOwner, testCase.ExpectedBucketOwner) + } + }) + } +} + +func TestAccS3BucketAcl_basic(t *testing.T) { + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketAclBasicConfig(bucketName, s3.BucketCannedACLPrivate), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "acl", s3.BucketCannedACLPrivate), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.owner.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "access_control_policy.0.grant.*", map[string]string{ + "grantee.#": "1", + "grantee.0.type": s3.TypeCanonicalUser, + "permission": s3.PermissionFullControl, + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketAcl_disappears(t *testing.T) { + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketAclBasicConfig(bucketName, s3.BucketCannedACLPrivate), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + // Bucket ACL cannot be destroyed, but we can verify Bucket deletion + // will result in a missing Bucket ACL resource + acctest.CheckResourceDisappears(acctest.Provider, tfs3.ResourceBucket(), "aws_s3_bucket.test"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccS3BucketAcl_updateACL(t *testing.T) { + bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") + resourceName := "aws_s3_bucket_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketAclBasicConfig(bucketName, s3.BucketCannedACLPublicRead), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "acl", s3.BucketCannedACLPublicRead), + ), + }, + { + Config: testAccBucketAclBasicConfig(bucketName, s3.BucketCannedACLPrivate), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "acl", s3.BucketCannedACLPrivate), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketAcl_updateGrant(t *testing.T) { + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3_bucket_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketAcl_GrantsConfig(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.grant.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "access_control_policy.0.grant.*", map[string]string{ + "grantee.#": "1", + "grantee.0.type": s3.TypeCanonicalUser, + "permission": s3.PermissionFullControl, + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "access_control_policy.0.grant.*", map[string]string{ + "grantee.#": "1", + "grantee.0.type": s3.TypeCanonicalUser, + "permission": s3.PermissionWrite, + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "access_control_policy.0.grant.*.grantee.0.id", "data.aws_canonical_user_id.current", "id"), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.owner.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "access_control_policy.0.owner.0.id", "data.aws_canonical_user_id.current", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBucketAcl_GrantsUpdateConfig(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.grant.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "access_control_policy.0.grant.*", map[string]string{ + "grantee.#": "1", + "grantee.0.type": s3.TypeCanonicalUser, + "permission": s3.PermissionRead, + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "access_control_policy.0.grant.*.grantee.0.id", "data.aws_canonical_user_id.current", "id"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "access_control_policy.0.grant.*", map[string]string{ + "grantee.#": "1", + "grantee.0.type": s3.TypeGroup, + "permission": s3.PermissionReadAcp, + }), + resource.TestMatchTypeSetElemNestedAttrs(resourceName, "access_control_policy.0.grant.*", map[string]*regexp.Regexp{ + "grantee.0.uri": regexp.MustCompile(`http://acs.*/groups/s3/LogDelivery`), + }), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.owner.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "access_control_policy.0.owner.0.id", "data.aws_canonical_user_id.current", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketAcl_ACLToGrant(t *testing.T) { + bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") + resourceName := "aws_s3_bucket_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketAclBasicConfig(bucketName, s3.BucketCannedACLPrivate), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "acl", s3.BucketCannedACLPrivate), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + ), + }, + { + Config: testAccBucketAcl_GrantsConfig(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.grant.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccS3BucketAcl_grantToACL(t *testing.T) { + bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") + resourceName := "aws_s3_bucket_acl.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketAcl_GrantsConfig(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.0.grant.#", "2"), + ), + }, + { + Config: testAccBucketAclBasicConfig(bucketName, s3.BucketCannedACLPrivate), + Check: resource.ComposeTestCheckFunc( + testAccCheckBucketAclExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "acl", s3.BucketCannedACLPrivate), + resource.TestCheckResourceAttr(resourceName, "access_control_policy.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckBucketAclExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).S3Conn + + bucket, expectedBucketOwner, _, err := tfs3.BucketACLParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + + input := &s3.GetBucketAclInput{ + Bucket: aws.String(bucket), + } + + if expectedBucketOwner != "" { + input.ExpectedBucketOwner = aws.String(expectedBucketOwner) + } + + output, err := conn.GetBucketAcl(input) + + if err != nil { + return err + } + + if output == nil || len(output.Grants) == 0 || output.Owner == nil { + return fmt.Errorf("S3 bucket ACL %s not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccBucketAclBasicConfig(rName, acl string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.test.id + acl = %[2]q +} +`, rName, acl) +} + +func testAccBucketAcl_GrantsConfig(bucketName string) string { + return fmt.Sprintf(` +data "aws_canonical_user_id" "current" {} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.test.id + access_control_policy { + grant { + grantee { + id = data.aws_canonical_user_id.current.id + type = "CanonicalUser" + } + permission = "FULL_CONTROL" + } + + grant { + grantee { + id = data.aws_canonical_user_id.current.id + type = "CanonicalUser" + } + permission = "WRITE" + } + + owner { + id = data.aws_canonical_user_id.current.id + } + } +} +`, bucketName) +} + +func testAccBucketAcl_GrantsUpdateConfig(bucketName string) string { + return fmt.Sprintf(` +data "aws_canonical_user_id" "current" {} + +data "aws_partition" "current" {} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.test.id + access_control_policy { + grant { + grantee { + id = data.aws_canonical_user_id.current.id + type = "CanonicalUser" + } + permission = "READ" + } + + grant { + grantee { + type = "Group" + uri = "http://acs.${data.aws_partition.current.dns_suffix}/groups/s3/LogDelivery" + } + permission = "READ_ACP" + } + + owner { + id = data.aws_canonical_user_id.current.id + } + } +} +`, bucketName) +} diff --git a/internal/service/s3/bucket_test.go b/internal/service/s3/bucket_test.go index c0b96bf01d9..8ca9395eec0 100644 --- a/internal/service/s3/bucket_test.go +++ b/internal/service/s3/bucket_test.go @@ -497,158 +497,6 @@ func TestAccS3Bucket_Security_policy(t *testing.T) { }) } -func TestAccS3Bucket_Security_updateACL(t *testing.T) { - bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") - resourceName := "aws_s3_bucket.bucket" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckBucketDestroy, - Steps: []resource.TestStep{ - { - Config: testAccBucketWithACLConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "acl", "public-read"), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"force_destroy", "acl", "grant"}, - }, - { - Config: testAccBucketWithACLUpdateConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "acl", "private"), - ), - }, - }, - }) -} - -func TestAccS3Bucket_Security_updateGrant(t *testing.T) { - bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") - resourceName := "aws_s3_bucket.bucket" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckBucketDestroy, - Steps: []resource.TestStep{ - { - Config: testAccBucketWithGrantsConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "grant.#", "1"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "grant.*", map[string]string{ - "permissions.#": "2", - "type": "CanonicalUser", - }), - resource.TestCheckTypeSetElemAttr(resourceName, "grant.*.permissions.*", "FULL_CONTROL"), - resource.TestCheckTypeSetElemAttr(resourceName, "grant.*.permissions.*", "WRITE"), - ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"force_destroy", "acl"}, - }, - { - Config: testAccBucketWithGrantsUpdateConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "grant.#", "2"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "grant.*", map[string]string{ - "permissions.#": "1", - "type": "CanonicalUser", - }), - resource.TestCheckTypeSetElemAttr(resourceName, "grant.*.permissions.*", "READ"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "grant.*", map[string]string{ - "permissions.#": "1", - "type": "Group", - "uri": "http://acs.amazonaws.com/groups/s3/LogDelivery", - }), - resource.TestCheckTypeSetElemAttr(resourceName, "grant.*.permissions.*", "READ_ACP"), - ), - }, - { - Config: testAccBucketConfig_Basic(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "grant.#", "0"), - ), - }, - }, - }) -} - -func TestAccS3Bucket_Security_aclToGrant(t *testing.T) { - bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") - resourceName := "aws_s3_bucket.bucket" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckBucketDestroy, - Steps: []resource.TestStep{ - { - Config: testAccBucketWithACLConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "acl", "public-read"), - resource.TestCheckResourceAttr(resourceName, "grant.#", "0"), - ), - }, - { - Config: testAccBucketWithGrantsConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "grant.#", "1"), - // check removed ACLs - ), - }, - }, - }) -} - -func TestAccS3Bucket_Security_grantToACL(t *testing.T) { - bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") - resourceName := "aws_s3_bucket.bucket" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ErrorCheck: acctest.ErrorCheck(t, s3.EndpointsID), - Providers: acctest.Providers, - CheckDestroy: testAccCheckBucketDestroy, - Steps: []resource.TestStep{ - { - Config: testAccBucketWithGrantsConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "grant.#", "1"), - ), - }, - { - Config: testAccBucketWithACLConfig(bucketName), - Check: resource.ComposeTestCheckFunc( - testAccCheckBucketExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "acl", "public-read"), - resource.TestCheckResourceAttr(resourceName, "grant.#", "0"), - // check removed grants - ), - }, - }, - }) -} - func TestAccS3Bucket_Security_enableDefaultEncryptionWhenTypical(t *testing.T) { bucketName := sdkacctest.RandomWithPrefix("tf-test-bucket") resourceName := "aws_s3_bucket.arbitrary" @@ -3240,8 +3088,18 @@ func testAccBucketConfig_withNoTags(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" force_destroy = false + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3250,7 +3108,6 @@ func testAccBucketConfig_withTags(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" force_destroy = false tags = { @@ -3258,6 +3115,17 @@ resource "aws_s3_bucket" "bucket" { Key2 = "BBB" Key3 = "CCC" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3266,7 +3134,6 @@ func testAccBucketConfig_withUpdatedTags(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" force_destroy = false tags = { @@ -3275,6 +3142,17 @@ resource "aws_s3_bucket" "bucket" { Key4 = "DDD" Key5 = "EEE" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3283,68 +3161,128 @@ func testAccMultiBucketWithTagsConfig(randInt int) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket1" { bucket = "tf-test-bucket-1-%[1]d" - acl = "private" force_destroy = true tags = { Name = "tf-test-bucket-1-%[1]d" Environment = "%[1]d" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test1" { + bucket = aws_s3_bucket.bucket1.id + acl = "private" } resource "aws_s3_bucket" "bucket2" { bucket = "tf-test-bucket-2-%[1]d" - acl = "private" force_destroy = true tags = { Name = "tf-test-bucket-2-%[1]d" Environment = "%[1]d" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test2" { + bucket = aws_s3_bucket.bucket2.id + acl = "private" } resource "aws_s3_bucket" "bucket3" { bucket = "tf-test-bucket-3-%[1]d" - acl = "private" force_destroy = true tags = { Name = "tf-test-bucket-3-%[1]d" Environment = "%[1]d" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test3" { + bucket = aws_s3_bucket.bucket3.id + acl = "private" } resource "aws_s3_bucket" "bucket4" { bucket = "tf-test-bucket-4-%[1]d" - acl = "private" force_destroy = true tags = { Name = "tf-test-bucket-4-%[1]d" Environment = "%[1]d" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test4" { + bucket = aws_s3_bucket.bucket4.id + acl = "private" } resource "aws_s3_bucket" "bucket5" { bucket = "tf-test-bucket-5-%[1]d" - acl = "private" force_destroy = true tags = { Name = "tf-test-bucket-5-%[1]d" Environment = "%[1]d" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test5" { + bucket = aws_s3_bucket.bucket5.id + acl = "private" } resource "aws_s3_bucket" "bucket6" { bucket = "tf-test-bucket-6-%[1]d" - acl = "private" force_destroy = true tags = { Name = "tf-test-bucket-6-%[1]d" Environment = "%[1]d" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test6" { + bucket = aws_s3_bucket.bucket6.id + acl = "private" } `, randInt) } @@ -3389,8 +3327,18 @@ func testAccBucketWithPolicyConfig(bucketName, partition string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "public-read" policy = %[2]s + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "public-read" } `, bucketName, strconv.Quote(testAccBucketPolicy(bucketName, partition))) } @@ -3399,6 +3347,16 @@ func testAccBucketDestroyedConfig(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id acl = "public-read" } `, bucketName) @@ -3493,8 +3451,18 @@ func testAccBucketWithEmptyPolicyConfig(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "public-read" policy = "" + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "public-read" } `, bucketName) } @@ -3568,77 +3536,41 @@ resource "aws_s3_bucket" "bucket" { `, bucketName) } -func testAccBucketWithACLConfig(bucketName string) string { - return fmt.Sprintf(` -resource "aws_s3_bucket" "bucket" { - bucket = %[1]q - acl = "public-read" -} -`, bucketName) -} - -func testAccBucketWithACLUpdateConfig(bucketName string) string { - return fmt.Sprintf(` -resource "aws_s3_bucket" "bucket" { - bucket = %[1]q - acl = "private" -} -`, bucketName) -} - -func testAccBucketWithGrantsConfig(bucketName string) string { - return fmt.Sprintf(` -data "aws_canonical_user_id" "current" {} - -resource "aws_s3_bucket" "bucket" { - bucket = %[1]q - - grant { - id = data.aws_canonical_user_id.current.id - type = "CanonicalUser" - permissions = ["FULL_CONTROL", "WRITE"] - } -} -`, bucketName) -} - -func testAccBucketWithGrantsUpdateConfig(bucketName string) string { +func testAccBucketWithLoggingConfig(bucketName string) string { return fmt.Sprintf(` -data "aws_canonical_user_id" "current" {} - -resource "aws_s3_bucket" "bucket" { - bucket = %[1]q - - grant { - id = data.aws_canonical_user_id.current.id - type = "CanonicalUser" - permissions = ["READ"] - } +resource "aws_s3_bucket" "log_bucket" { + bucket = "%[1]s-log" - grant { - type = "Group" - permissions = ["READ_ACP"] - uri = "http://acs.amazonaws.com/groups/s3/LogDelivery" + lifecycle { + ignore_changes = [ + grant, + ] } } -`, bucketName) -} -func testAccBucketWithLoggingConfig(bucketName string) string { - return fmt.Sprintf(` -resource "aws_s3_bucket" "log_bucket" { - bucket = "%[1]s-log" +resource "aws_s3_bucket_acl" "log_bucket" { + bucket = aws_s3_bucket.log_bucket.id acl = "log-delivery-write" } resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" logging { target_bucket = aws_s3_bucket.log_bucket.id target_prefix = "log/" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3647,7 +3579,6 @@ func testAccBucketWithLifecycleConfig(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" lifecycle_rule { id = "id1" @@ -3748,6 +3679,17 @@ resource "aws_s3_bucket" "bucket" { storage_class = "GLACIER" } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3756,7 +3698,6 @@ func testAccBucketWithLifecycleExpireMarkerConfig(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" lifecycle_rule { id = "id1" @@ -3767,6 +3708,17 @@ resource "aws_s3_bucket" "bucket" { expired_object_delete_marker = "true" } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3775,7 +3727,6 @@ func testAccBucketWithVersioningLifecycleConfig(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" versioning { enabled = false @@ -3821,6 +3772,17 @@ resource "aws_s3_bucket" "bucket" { storage_class = "GLACIER" } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } @@ -3893,11 +3855,21 @@ func testAccBucketReplicationConfig(randInt int) string { return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -3906,7 +3878,6 @@ func testAccBucketReplicationWithConfigurationConfig(randInt int, storageClass s return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -3926,6 +3897,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt, storageClass) } @@ -3936,7 +3918,6 @@ func testAccBucketReplicationWithReplicationConfigurationWithRTCConfig(randInt i fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-rtc-%[1]d" - acl = "private" versioning { enabled = true } @@ -3962,6 +3943,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt, minutes)) } @@ -3972,7 +3964,6 @@ func testAccBucketReplicationWithReplicationConfigurationWithRTCNoMinutesConfig( fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-rtc-no-minutes-%[1]d" - acl = "private" versioning { enabled = true } @@ -3994,6 +3985,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt)) } @@ -4004,7 +4006,6 @@ func testAccBucketReplicationWithReplicationConfigurationWithRTCNoStatusConfig(r fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-rtc-no-minutes-%[1]d" - acl = "private" versioning { enabled = true } @@ -4026,6 +4027,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt)) } @@ -4036,7 +4048,6 @@ func testAccBucketReplicationWithReplicationConfigurationWithRTCNoConfigConfig(r fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-rtc-no-config-%[1]d" - acl = "private" versioning { enabled = true } @@ -4056,6 +4067,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt)) } @@ -4084,7 +4106,6 @@ resource "aws_s3_bucket" "destination3" { resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4132,6 +4153,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt)) } @@ -4160,7 +4192,6 @@ resource "aws_s3_bucket" "destination3" { resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4220,6 +4251,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt)) } @@ -4239,7 +4281,6 @@ resource "aws_s3_bucket" "destination2" { resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4280,6 +4321,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt)) } @@ -4294,7 +4346,6 @@ resource "aws_kms_key" "replica" { resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4321,6 +4372,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4331,7 +4393,6 @@ data "aws_caller_identity" "current" {} resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4356,6 +4417,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4365,7 +4437,6 @@ func testAccBucketReplicationConfigurationRulesDestinationConfig(randInt int) st data "aws_caller_identity" "current" {} resource "aws_s3_bucket" "bucket" { - acl = "private" bucket = "tf-test-bucket-%[1]d" replication_configuration { @@ -4387,6 +4458,17 @@ resource "aws_s3_bucket" "bucket" { versioning { enabled = true } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4403,7 +4485,6 @@ resource "aws_kms_key" "replica" { resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4435,6 +4516,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4443,7 +4535,6 @@ func testAccBucketReplicationWithoutStorageClassConfig(randInt int) string { return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4462,6 +4553,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4470,7 +4572,6 @@ func testAccBucketReplicationWithoutPrefixConfig(randInt int) string { return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4489,6 +4590,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4497,7 +4609,6 @@ func testAccBucketReplicationNoVersioningConfig(randInt int) string { return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" replication_configuration { role = aws_iam_role.role.arn @@ -4513,6 +4624,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4521,7 +4643,6 @@ func testAccBucketSameRegionReplicationWithV2ConfigurationNoTagsConfig(rName, rN return acctest.ConfigCompose(testAccBucketReplicationConfig_iamPolicy(rName), fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = %[1]q - acl = "private" versioning { enabled = true @@ -4546,6 +4667,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } resource "aws_s3_bucket" "destination" { @@ -4562,7 +4694,6 @@ func testAccBucketReplicationWithV2ConfigurationDeleteMarkerReplicationDisabledC return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4585,6 +4716,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4593,7 +4735,6 @@ func testAccBucketReplicationWithV2ConfigurationNoTagsConfig(randInt int) string return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4618,6 +4759,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4626,7 +4778,6 @@ func testAccBucketReplicationWithV2ConfigurationOnlyOneTagConfig(randInt int) st return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4653,6 +4804,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4661,7 +4823,6 @@ func testAccBucketReplicationWithV2ConfigurationPrefixAndTagsConfig(randInt int) return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4691,6 +4852,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4699,7 +4871,6 @@ func testAccBucketReplicationWithV2ConfigurationMultipleTagsConfig(randInt int) return testAccBucketReplicationBasicConfig(randInt) + fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%[1]d" - acl = "private" versioning { enabled = true @@ -4726,6 +4897,17 @@ resource "aws_s3_bucket" "bucket" { } } } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, randInt) } @@ -4765,8 +4947,18 @@ func testAccBucketConfig_forceDestroy(bucketName string) string { return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "%s" - acl = "private" force_destroy = true + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "public-read" } `, bucketName) } @@ -4775,7 +4967,6 @@ func testAccBucketConfig_forceDestroyWithObjectLockEnabled(bucketName string) st return fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "%s" - acl = "private" force_destroy = true versioning { @@ -4785,6 +4976,17 @@ resource "aws_s3_bucket" "bucket" { object_lock_configuration { object_lock_enabled = "Enabled" } + + lifecycle { + ignore_changes = [ + grant, + ] + } +} + +resource "aws_s3_bucket_acl" "test" { + bucket = aws_s3_bucket.bucket.id + acl = "private" } `, bucketName) } diff --git a/website/docs/r/s3_bucket.html.markdown b/website/docs/r/s3_bucket.html.markdown index b9aa8e1fc5e..811ed280891 100644 --- a/website/docs/r/s3_bucket.html.markdown +++ b/website/docs/r/s3_bucket.html.markdown @@ -19,13 +19,17 @@ Provides a S3 bucket resource. ```terraform resource "aws_s3_bucket" "b" { bucket = "my-tf-test-bucket" - acl = "private" tags = { Name = "My bucket" Environment = "Dev" } } + +resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.b.id + acl = "private" +} ``` ### Static Website Hosting @@ -38,7 +42,6 @@ See the [`aws_s3_bucket_website_configuration` resource](s3_bucket_website_confi ```terraform resource "aws_s3_bucket" "b" { bucket = "s3-website-test.hashicorp.com" - acl = "public-read" cors_rule { allowed_headers = ["*"] @@ -48,6 +51,11 @@ resource "aws_s3_bucket" "b" { max_age_seconds = 3000 } } + +resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.b.id + acl = "public-read" +} ``` ### Using versioning @@ -55,12 +63,16 @@ resource "aws_s3_bucket" "b" { ```terraform resource "aws_s3_bucket" "b" { bucket = "my-tf-test-bucket" - acl = "private" versioning { enabled = true } } + +resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.b.id + acl = "private" +} ``` ### Enable Logging @@ -68,18 +80,27 @@ resource "aws_s3_bucket" "b" { ```terraform resource "aws_s3_bucket" "log_bucket" { bucket = "my-tf-log-bucket" +} + +resource "aws_s3_bucket_acl" "log_bucket_acl" { + bucket = aws_s3_bucket.log_bucket.id acl = "log-delivery-write" } resource "aws_s3_bucket" "b" { bucket = "my-tf-test-bucket" - acl = "private" logging { - target_bucket = aws_s3_bucket.log_bucket.id + # The log bucket must have its ACL configured first + target_bucket = aws_s3_bucket_acl.log_bucket_acl.bucket target_prefix = "log/" } } + +resource "aws_s3_bucket_acl" "b_bucket_acl" { + bucket = aws_s3_bucket.b.id + acl = "private" +} ``` ### Using object lifecycle @@ -87,7 +108,6 @@ resource "aws_s3_bucket" "b" { ```terraform resource "aws_s3_bucket" "bucket" { bucket = "my-bucket" - acl = "private" lifecycle_rule { id = "log" @@ -126,9 +146,13 @@ resource "aws_s3_bucket" "bucket" { } } +resource "aws_s3_bucket_acl" "bucket_acl" { + bucket = aws_s3_bucket.bucket.id + acl = "private" +} + resource "aws_s3_bucket" "versioning_bucket" { bucket = "my-versioning-bucket" - acl = "private" versioning { enabled = true @@ -153,6 +177,11 @@ resource "aws_s3_bucket" "versioning_bucket" { } } } + +resource "aws_s3_bucket_acl" "versioning_bucket_acl" { + bucket = aws_s3_bucket.versioning_bucket.id + acl = "private" +} ``` ### Using replication configuration @@ -247,7 +276,6 @@ resource "aws_s3_bucket" "destination" { resource "aws_s3_bucket" "source" { provider = aws.central bucket = "tf-test-bucket-source-12345" - acl = "private" versioning { enabled = true @@ -280,6 +308,11 @@ resource "aws_s3_bucket" "source" { } } } + +resource "aws_s3_bucket_acl" "source_bucket_acl" { + bucket = aws_s3_bucket.source.id + acl = "private" +} ``` ### Enable Default Server Side Encryption diff --git a/website/docs/r/s3_bucket_acl.html.markdown b/website/docs/r/s3_bucket_acl.html.markdown new file mode 100644 index 00000000000..d6a3292412e --- /dev/null +++ b/website/docs/r/s3_bucket_acl.html.markdown @@ -0,0 +1,134 @@ +--- +subcategory: "S3" +layout: "aws" +page_title: "AWS: aws_s3_bucket_acl" +description: |- + Provides an S3 bucket ACL resource. +--- + +# Resource: aws_s3_bucket_acl + +Provides an S3 bucket ACL resource. + +~> **Note:** `terraform destroy` does not delete the S3 Bucket ACL but does remove the resource from Terraform state. + +## Example Usage + +### With ACL + +```terraform +resource "aws_s3_bucket" "example" { + bucket = "my-tf-example-bucket" +} + +resource "aws_s3_bucket_acl" "example_bucket_acl" { + bucket = aws_s3_bucket.example.id + acl = "private" +} +``` + +### With Grants + +```terraform +data "aws_canonical_user_id" "current" {} + +resource "aws_s3_bucket" "example" { + bucket = "my-tf-example-bucket" +} + +resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.example.id + access_control_policy { + grant { + grantee { + id = data.aws_canonical_user_id.current.id + type = "CanonicalUser" + } + permission = "READ" + } + + grant { + grantee { + type = "Group" + uri = "http://acs.amazonaws.com/groups/s3/LogDelivery" + } + permission = "READ_ACP" + } + + owner { + id = data.aws_canonical_user_id.current.id + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `acl` - (Optional, Conflicts with `access_control_policy`) The canned ACL to apply to the bucket. +* `access_control_policy` - (Optional, Conflicts with `acl`) A configuration block that sets the ACL permissions for an object per grantee [documented below](#access_control_policy). +* `bucket` - (Required, Forces new resource) The name of the bucket. +* `expected_bucket_owner` - (Optional, Forces new resource) The account ID of the expected bucket owner. + +### access_control_policy + +The `access_control_policy` configuration block supports the following arguments: + +* `grant` - (Required) Set of `grant` configuration blocks [documented below](#grant). +* `owner` - (Required) Configuration block of the bucket owner's display name and ID [documented below](#owner). + +### grant + +The `grant` configuration block supports the following arguments: + +* `grantee` - (Required) Configuration block for the person being granted permissions [documented below](#grantee). +* `permission` - (Required) Logging permissions assigned to the grantee for the bucket. + +### owner + +The `owner` configuration block supports the following arguments: + +* `id` - (Required) The ID of the owner. +* `display_name` - (Optional) The display name of the owner. + +### grantee + +The `grantee` configuration block supports the following arguments: + +* `email_address` - (Optional) Email address of the grantee. See [Regions and Endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region) for supported AWS regions where this argument can be specified. +* `id` - (Optional) The canonical user ID of the grantee. +* `type` - (Required) Type of grantee. Valid values: `CanonicalUser`, `AmazonCustomerByEmail`, `Group`. +* `uri` - (Optional) URI of the grantee group. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The `bucket`, `expected_bucket_owner` (if configured), and `acl` (if configured) separated by commas (`,`). + +## Import + +S3 bucket ACL can be imported using the `bucket` e.g., + +``` +$ terraform import aws_s3_bucket_acl.example bucket-name +``` + +S3 bucket ACL can also be imported using the `bucket` and `acl` separated by a comma (`,`), e.g. + +``` +$ terraform import aws_s3_bucket_acl.example bucket-name,private +``` + +S3 bucket ACL can also be imported using the `bucket` and `expected_bucket_owner` separated by a comma (`,`), e.g. + +``` +$ terraform import aws_s3_bucket_acl.example bucket-name,123456789012 +``` + +S3 bucket ACL can also be imported using the `bucket`, `expected_bucket_owner`, and `acl` separated by commas (`,`), e.g. + +``` +$ terraform import aws_s3_bucket_acl.example bucket-name,123456789012,private +```