diff --git a/aws/provider.go b/aws/provider.go index 5109915b5b3..ec85bfbbd09 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -863,6 +863,7 @@ func Provider() *schema.Provider { "aws_s3_bucket_metric": resourceAwsS3BucketMetric(), "aws_s3_bucket_inventory": resourceAwsS3BucketInventory(), "aws_s3control_bucket": resourceAwsS3ControlBucket(), + "aws_s3control_bucket_policy": resourceAwsS3ControlBucketPolicy(), "aws_security_group": resourceAwsSecurityGroup(), "aws_network_interface_sg_attachment": resourceAwsNetworkInterfaceSGAttachment(), "aws_default_security_group": resourceAwsDefaultSecurityGroup(), diff --git a/aws/resource_aws_s3control_bucket_policy.go b/aws/resource_aws_s3control_bucket_policy.go new file mode 100644 index 00000000000..dcbc9369f73 --- /dev/null +++ b/aws/resource_aws_s3control_bucket_policy.go @@ -0,0 +1,166 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/s3control" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceAwsS3ControlBucketPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsS3ControlBucketPolicyCreate, + Read: resourceAwsS3ControlBucketPolicyRead, + Update: resourceAwsS3ControlBucketPolicyUpdate, + Delete: resourceAwsS3ControlBucketPolicyDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "policy": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, + }, + }, + } +} + +func resourceAwsS3ControlBucketPolicyCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3controlconn + + bucket := d.Get("bucket").(string) + + input := &s3control.PutBucketPolicyInput{ + Bucket: aws.String(bucket), + Policy: aws.String(d.Get("policy").(string)), + } + + _, err := conn.PutBucketPolicy(input) + + if err != nil { + return fmt.Errorf("error creating S3 Control Bucket Policy (%s): %w", bucket, err) + } + + d.SetId(bucket) + + return resourceAwsS3ControlBucketPolicyRead(d, meta) +} + +func resourceAwsS3ControlBucketPolicyRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3controlconn + + parsedArn, err := arn.Parse(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", d.Id(), err) + } + + if parsedArn.AccountID == "" { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): unknown format", d.Id()) + } + + input := &s3control.GetBucketPolicyInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(d.Id()), + } + + output, err := conn.GetBucketPolicy(input) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchBucket") { + log.Printf("[WARN] S3 Control Bucket Policy (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchBucketPolicy") { + log.Printf("[WARN] S3 Control Bucket Policy (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, "NoSuchOutpost") { + log.Printf("[WARN] S3 Control Bucket Policy (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading S3 Control Bucket Policy (%s): %w", d.Id(), err) + } + + if output == nil { + return fmt.Errorf("error reading S3 Control Bucket Policy (%s): empty response", d.Id()) + } + + d.Set("bucket", d.Id()) + d.Set("policy", output.Policy) + + return nil +} + +func resourceAwsS3ControlBucketPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3controlconn + + input := &s3control.PutBucketPolicyInput{ + Bucket: aws.String(d.Id()), + Policy: aws.String(d.Get("policy").(string)), + } + + _, err := conn.PutBucketPolicy(input) + + if err != nil { + return fmt.Errorf("error updating S3 Control Bucket Policy (%s): %w", d.Id(), err) + } + + return resourceAwsS3ControlBucketPolicyRead(d, meta) +} + +func resourceAwsS3ControlBucketPolicyDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).s3controlconn + + parsedArn, err := arn.Parse(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", d.Id(), err) + } + + input := &s3control.DeleteBucketPolicyInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(d.Id()), + } + + _, err = conn.DeleteBucketPolicy(input) + + if tfawserr.ErrCodeEquals(err, "NoSuchBucket") { + return nil + } + + if tfawserr.ErrCodeEquals(err, "NoSuchBucketPolicy") { + return nil + } + + if tfawserr.ErrCodeEquals(err, "NoSuchOutpost") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting S3 Control Bucket Policy (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_s3control_bucket_policy_test.go b/aws/resource_aws_s3control_bucket_policy_test.go new file mode 100644 index 00000000000..7d42f8723a8 --- /dev/null +++ b/aws/resource_aws_s3control_bucket_policy_test.go @@ -0,0 +1,205 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/s3control" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" +) + +func TestAccAWSS3ControlBucketPolicy_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketPolicyConfig_Policy(rName, "s3-outposts:*"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketPolicyExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "bucket", "aws_s3control_bucket.test", "arn"), + resource.TestMatchResourceAttr(resourceName, "policy", regexp.MustCompile(`s3-outposts:\*`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSS3ControlBucketPolicy_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketPolicyConfig_Policy(rName, "s3-outposts:*"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketPolicyExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsS3ControlBucketPolicy(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSS3ControlBucketPolicy_Policy(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_s3control_bucket_policy.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSOutpostsOutposts(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3ControlBucketPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3ControlBucketPolicyConfig_Policy(rName, "s3-outposts:GetObject"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketPolicyExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "policy", regexp.MustCompile(`s3-outposts:GetObject`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSS3ControlBucketPolicyConfig_Policy(rName, "s3-outposts:PutObject"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3ControlBucketPolicyExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "policy", regexp.MustCompile(`s3-outposts:PutObject`)), + ), + }, + }, + }) +} + +func testAccCheckAWSS3ControlBucketPolicyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).s3controlconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_s3control_bucket_policy" { + continue + } + + parsedArn, err := arn.Parse(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", rs.Primary.ID, err) + } + + input := &s3control.GetBucketPolicyInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(rs.Primary.ID), + } + + _, err = conn.GetBucketPolicy(input) + + if tfawserr.ErrCodeEquals(err, "NoSuchBucket") { + continue + } + + if tfawserr.ErrCodeEquals(err, "NoSuchBucketPolicy") { + continue + } + + if tfawserr.ErrCodeEquals(err, "NoSuchOutpost") { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("S3 Control Bucket Policy (%s) still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckAWSS3ControlBucketPolicyExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no resource ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).s3controlconn + + parsedArn, err := arn.Parse(rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error parsing S3 Control Bucket ARN (%s): %w", rs.Primary.ID, err) + } + + input := &s3control.GetBucketPolicyInput{ + AccountId: aws.String(parsedArn.AccountID), + Bucket: aws.String(rs.Primary.ID), + } + + _, err = conn.GetBucketPolicy(input) + + if err != nil { + return err + } + + return nil + } +} + +func testAccAWSS3ControlBucketPolicyConfig_Policy(rName, action string) string { + return fmt.Sprintf(` +data "aws_outposts_outposts" "test" {} + +data "aws_outposts_outpost" "test" { + id = tolist(data.aws_outposts_outposts.test.ids)[0] +} + +resource "aws_s3control_bucket" "test" { + bucket = %[1]q + outpost_id = data.aws_outposts_outpost.test.id +} + +resource "aws_s3control_bucket_policy" "test" { + bucket = aws_s3control_bucket.test.arn + policy = jsonencode({ + Id = "testBucketPolicy" + Statement = [ + { + Action = %[2]q + Effect = "Deny" + Principal = { + AWS = "*" + } + Resource = "${aws_s3control_bucket.test.arn}/object/test" + Sid = "st1" + } + ] + Version = "2012-10-17" + }) +} +`, rName, action) +} diff --git a/website/docs/r/s3control_bucket_policy.html.markdown b/website/docs/r/s3control_bucket_policy.html.markdown new file mode 100644 index 00000000000..8fc4ffe5fc3 --- /dev/null +++ b/website/docs/r/s3control_bucket_policy.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "S3 Control" +layout: "aws" +page_title: "AWS: aws_s3control_bucket_policy" +description: |- + Manages an S3 Control Bucket Policy. +--- + +# Resource: aws_s3control_bucket_policy + +Provides a resource to manage an S3 Control Bucket Policy. + +-> This functionality is for managing [S3 on Outposts](https://docs.aws.amazon.com/AmazonS3/latest/dev/S3onOutposts.html). To manage S3 Bucket Policies in an AWS Partition, see the [`aws_s3_bucket_policy` resource](/docs/providers/aws/r/s3_bucket_policy.html). + +## Example Usage + +```hcl +resource "aws_s3control_bucket_policy" "example" { + bucket = aws_s3control_bucket.example.arn + policy = jsonencode({ + Id = "testBucketPolicy" + Statement = [ + { + Action = "s3-outposts:PutBucketLifecycleConfiguration" + Effect = "Deny" + Principal = { + AWS = "*" + } + Resource = aws_s3control_bucket.example.arn + Sid = "statement1" + } + ] + Version = "2012-10-17" + }) +} +``` + +## Argument Reference + +The following arguments are required: + +* `bucket` - (Required) Amazon Resource Name (ARN) of the bucket. +* `policy` - (Required) JSON string of the resource policy. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy). + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Amazon Resource Name (ARN) of the bucket. + +## Import + +S3 Control Bucket Policies can be imported using the Amazon Resource Name (ARN), e.g. + +``` +$ terraform import aws_s3control_bucket_policy.example arn:aws:s3-outposts:us-east-1:123456789012:outpost/op-12345678/bucket/example +```