diff --git a/builtin/providers/aws/diff_suppress_funcs.go b/builtin/providers/aws/diff_suppress_funcs.go new file mode 100644 index 000000000..408063e26 --- /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 e95ad615b..9d1580d2b 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 f4637d93e..28ae61e34 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 000000000..4485f11a7 --- /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 000000000..8dedae1a0 --- /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 000000000..b9789e2a6 --- /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 04dbccfbd..6a2ae1614 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 +