From 93f31fce17f15e518da2ffb1093680fd9b3e379d Mon Sep 17 00:00:00 2001 From: James Nugent Date: Thu, 1 Sep 2016 17:34:45 -0500 Subject: [PATCH] provider/aws: Add aws_s3_bucket_policy resource This commit adds a new "attachment" style resource for setting the policy of an AWS S3 bucket. This is desirable such that the ARN of the bucket can be referenced in an IAM Policy Document. In addition, we now suppress diffs on the (now-computed) policy in the S3 bucket for structurally equivalent policies, which prevents flapping because of whitespace and map ordering changes made by the S3 endpoint. --- builtin/providers/aws/diff_suppress_funcs.go | 15 ++ builtin/providers/aws/provider.go | 1 + .../providers/aws/resource_aws_s3_bucket.go | 12 +- .../aws/resource_aws_s3_bucket_policy.go | 106 +++++++++++ .../aws/resource_aws_s3_bucket_policy_test.go | 180 ++++++++++++++++++ .../aws/r/s3_bucket_policy.html.markdown | 37 ++++ website/source/layouts/aws.erb | 3 + 7 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 builtin/providers/aws/diff_suppress_funcs.go create mode 100644 builtin/providers/aws/resource_aws_s3_bucket_policy.go create mode 100644 builtin/providers/aws/resource_aws_s3_bucket_policy_test.go create mode 100644 website/source/docs/providers/aws/r/s3_bucket_policy.html.markdown diff --git a/builtin/providers/aws/diff_suppress_funcs.go b/builtin/providers/aws/diff_suppress_funcs.go new file mode 100644 index 0000000000..408063e266 --- /dev/null +++ b/builtin/providers/aws/diff_suppress_funcs.go @@ -0,0 +1,15 @@ +package aws + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/jen20/awspolicyequivalence" +) + +func suppressEquivalentAwsPolicyDiffs(k, old, new string, d *schema.ResourceData) bool { + equivalent, err := awspolicy.PoliciesAreEquivalent(old, new) + if err != nil { + return false + } + + return equivalent +} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index e95ad615b5..9d1580d2b6 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -308,6 +308,7 @@ func Provider() terraform.ResourceProvider { "aws_ses_receipt_rule": resourceAwsSesReceiptRule(), "aws_ses_receipt_rule_set": resourceAwsSesReceiptRuleSet(), "aws_s3_bucket": resourceAwsS3Bucket(), + "aws_s3_bucket_policy": resourceAwsS3BucketPolicy(), "aws_s3_bucket_object": resourceAwsS3BucketObject(), "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), "aws_security_group": resourceAwsSecurityGroup(), diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index f4637d93ec..28ae61e343 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -8,13 +8,12 @@ import ( "net/url" "time" - "github.com/hashicorp/terraform/helper/resource" - "github.com/hashicorp/terraform/helper/schema" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" ) func resourceAwsS3Bucket() *schema.Resource { @@ -47,9 +46,10 @@ func resourceAwsS3Bucket() *schema.Resource { }, "policy": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - StateFunc: normalizeJson, + Type: schema.TypeString, + Optional: true, + Computed: true, + DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, }, "cors_rule": &schema.Schema{ diff --git a/builtin/providers/aws/resource_aws_s3_bucket_policy.go b/builtin/providers/aws/resource_aws_s3_bucket_policy.go new file mode 100644 index 0000000000..4485f11a77 --- /dev/null +++ b/builtin/providers/aws/resource_aws_s3_bucket_policy.go @@ -0,0 +1,106 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsS3BucketPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsS3BucketPolicyPut, + Read: resourceAwsS3BucketPolicyRead, + Update: resourceAwsS3BucketPolicyPut, + Delete: resourceAwsS3BucketPolicyDelete, + + Schema: map[string]*schema.Schema{ + "bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "policy": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, + }, + }, + } +} + +func resourceAwsS3BucketPolicyPut(d *schema.ResourceData, meta interface{}) error { + s3conn := meta.(*AWSClient).s3conn + + bucket := d.Get("bucket").(string) + policy := d.Get("policy").(string) + + d.SetId(bucket) + + log.Printf("[DEBUG] S3 bucket: %s, put policy: %s", bucket, policy) + + params := &s3.PutBucketPolicyInput{ + Bucket: aws.String(bucket), + Policy: aws.String(policy), + } + + err := resource.Retry(1*time.Minute, func() *resource.RetryError { + if _, err := s3conn.PutBucketPolicy(params); err != nil { + if awserr, ok := err.(awserr.Error); ok { + if awserr.Code() == "MalformedPolicy" { + return resource.RetryableError(awserr) + } + } + return resource.NonRetryableError(err) + } + return nil + }) + + if err != nil { + return fmt.Errorf("Error putting S3 policy: %s", err) + } + + return nil +} + +func resourceAwsS3BucketPolicyRead(d *schema.ResourceData, meta interface{}) error { + s3conn := meta.(*AWSClient).s3conn + + log.Printf("[DEBUG] S3 bucket policy, read for bucket: %s", d.Id()) + pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{ + Bucket: aws.String(d.Id()), + }) + + v := "" + if err == nil && pol.Policy != nil { + v = *pol.Policy + } + if err := d.Set("policy", v); err != nil { + return err + } + + return nil +} + +func resourceAwsS3BucketPolicyDelete(d *schema.ResourceData, meta interface{}) error { + s3conn := meta.(*AWSClient).s3conn + + bucket := d.Get("bucket").(string) + + log.Printf("[DEBUG] S3 bucket: %s, delete policy", bucket) + _, err := s3conn.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{ + Bucket: aws.String(bucket), + }) + + if err != nil { + return fmt.Errorf("Error deleting S3 policy: %s", err) + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_s3_bucket_policy_test.go b/builtin/providers/aws/resource_aws_s3_bucket_policy_test.go new file mode 100644 index 0000000000..8dedae1a09 --- /dev/null +++ b/builtin/providers/aws/resource_aws_s3_bucket_policy_test.go @@ -0,0 +1,180 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/jen20/awspolicyequivalence" +) + +func TestAccAWSS3BucketPolicy_basic(t *testing.T) { + name := fmt.Sprintf("tf-test-bucket-%d", acctest.RandInt()) + + expectedPolicyText := fmt.Sprintf( + `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"s3:*","Resource":["arn:aws:s3:::%s","arn:aws:s3:::%s/*"]}]}`, + name, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketPolicyConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketHasPolicy("aws_s3_bucket.bucket", expectedPolicyText), + ), + }, + }, + }) +} + +func TestAccAWSS3BucketPolicy_policyUpdate(t *testing.T) { + name := fmt.Sprintf("tf-test-bucket-%d", acctest.RandInt()) + + expectedPolicyText1 := fmt.Sprintf( + `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"s3:*","Resource":["arn:aws:s3:::%s","arn:aws:s3:::%s/*"]}]}`, + name, name) + + expectedPolicyText2 := fmt.Sprintf( + `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":["s3:DeleteBucket", "s3:ListBucket", "s3:ListBucketVersions"], "Resource":["arn:aws:s3:::%s","arn:aws:s3:::%s/*"]}]}`, + name, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSS3BucketPolicyConfig(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketHasPolicy("aws_s3_bucket.bucket", expectedPolicyText1), + ), + }, + + { + Config: testAccAWSS3BucketPolicyConfig_updated(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketHasPolicy("aws_s3_bucket.bucket", expectedPolicyText2), + ), + }, + }, + }) +} + +func testAccCheckAWSS3BucketHasPolicy(n string, expectedPolicyText 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 S3 Bucket ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).s3conn + + policy, err := conn.GetBucketPolicy(&s3.GetBucketPolicyInput{ + Bucket: aws.String(rs.Primary.ID), + }) + if err != nil { + return fmt.Errorf("GetBucketPolicy error: %v", err) + } + + actualPolicyText := *policy.Policy + + equivalent, err := awspolicy.PoliciesAreEquivalent(actualPolicyText, expectedPolicyText) + if err != nil { + return fmt.Errorf("Error testing policy equivalence: %s", err) + } + if !equivalent { + return fmt.Errorf("Non-equivalent policy error:\n\nexpected: %s\n\n got: %s\n", + expectedPolicyText, actualPolicyText) + } + + return nil + } +} + +func testAccAWSS3BucketPolicyConfig(bucketName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "%s" + tags { + TestName = "TestAccAWSS3BucketPolicy_basic" + } +} + +resource "aws_s3_bucket_policy" "bucket" { + bucket = "${aws_s3_bucket.bucket.bucket}" + policy = "${data.aws_iam_policy_document.policy.json}" +} + +data "aws_iam_policy_document" "policy" { + statement { + effect = "Allow" + + actions = [ + "s3:*", + ] + + resources = [ + "${aws_s3_bucket.bucket.arn}", + "${aws_s3_bucket.bucket.arn}/*", + ] + + principals { + type = "AWS" + identifiers = ["*"] + } + } +} +`, bucketName) +} + +func testAccAWSS3BucketPolicyConfig_updated(bucketName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "%s" + tags { + TestName = "TestAccAWSS3BucketPolicy_basic" + } +} + +resource "aws_s3_bucket_policy" "bucket" { + bucket = "${aws_s3_bucket.bucket.bucket}" + policy = "${data.aws_iam_policy_document.policy.json}" +} + +data "aws_iam_policy_document" "policy" { + statement { + effect = "Allow" + + actions = [ + "s3:DeleteBucket", + "s3:ListBucket", + "s3:ListBucketVersions" + ] + + resources = [ + "${aws_s3_bucket.bucket.arn}", + "${aws_s3_bucket.bucket.arn}/*", + ] + + principals { + type = "AWS" + identifiers = ["*"] + } + } +} +`, bucketName) +} diff --git a/website/source/docs/providers/aws/r/s3_bucket_policy.html.markdown b/website/source/docs/providers/aws/r/s3_bucket_policy.html.markdown new file mode 100644 index 0000000000..b9789e2a6f --- /dev/null +++ b/website/source/docs/providers/aws/r/s3_bucket_policy.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "aws" +page_title: "AWS: aws_s3_bucket_policy" +sidebar_current: "docs-aws-resource-s3-bucket-policy" +description: |- + Attaches a policy to an S3 bucket resource. +--- + +# aws\_s3\_bucket\_policy + +Attaches a policy to an S3 bucket resource. + +## Example Usage + +### Using versioning + +``` +resource "aws_s3_bucket" "b" { + # Arguments +} + +data "aws_iam_policy_document" "b" { + # Policy statements +} + +resource "aws_s3_bucket_policy" "b" { + bucket = "${aws_s3_bucket.b.bucket}" + policy = "${data.aws_iam_policy_document.b.json}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `bucket` - (Required) The name of the bucket to which to apply the policy. +* `policy` - (Required) The text of the policy. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 04dbccfbd7..6a2ae1614b 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -788,6 +788,9 @@ aws_s3_bucket_object + > + aws_s3_bucket_policy +