diff --git a/builtin/providers/aws/resource_aws_s3_bucket.go b/builtin/providers/aws/resource_aws_s3_bucket.go index 170f88970..d615151f1 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket.go +++ b/builtin/providers/aws/resource_aws_s3_bucket.go @@ -1,6 +1,7 @@ package aws import ( + "encoding/json" "fmt" "log" @@ -31,6 +32,12 @@ func resourceAwsS3Bucket() *schema.Resource { ForceNew: true, }, + "policy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + StateFunc: normalizeJson, + }, + "website": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -121,8 +128,16 @@ func resourceAwsS3BucketUpdate(d *schema.ResourceData, meta interface{}) error { return err } - if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil { - return err + if d.HasChange("policy") { + if err := resourceAwsS3BucketPolicyUpdate(s3conn, d); err != nil { + return err + } + } + + if d.HasChange("website") { + if err := resourceAwsS3BucketWebsiteUpdate(s3conn, d); err != nil { + return err + } } return resourceAwsS3BucketRead(d, meta) @@ -144,6 +159,25 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error { } } + // Read the policy + pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{ + Bucket: aws.String(d.Id()), + }) + log.Printf("[DEBUG] S3 bucket: %s, read policy: %v", d.Id(), pol) + if err != nil { + if err := d.Set("policy", ""); err != nil { + return err + } + } else { + if v := pol.Policy; v == nil { + if err := d.Set("policy", ""); err != nil { + return err + } + } else if err := d.Set("policy", normalizeJson(*v)); err != nil { + return err + } + } + // Read the website configuration ws, err := s3conn.GetBucketWebsite(&s3.GetBucketWebsiteInput{ Bucket: aws.String(d.Id()), @@ -228,11 +262,36 @@ func resourceAwsS3BucketDelete(d *schema.ResourceData, meta interface{}) error { return nil } -func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error { - if !d.HasChange("website") { - return nil +func resourceAwsS3BucketPolicyUpdate(s3conn *s3.S3, d *schema.ResourceData) error { + bucket := d.Get("bucket").(string) + policy := d.Get("policy").(string) + + if policy != "" { + log.Printf("[DEBUG] S3 bucket: %s, put policy: %s", bucket, policy) + + _, err := s3conn.PutBucketPolicy(&s3.PutBucketPolicyInput{ + Bucket: aws.String(bucket), + Policy: aws.String(policy), + }) + + if err != nil { + return fmt.Errorf("Error putting S3 policy: %s", err) + } + } else { + log.Printf("[DEBUG] S3 bucket: %s, delete policy: %s", bucket, policy) + _, err := s3conn.DeleteBucketPolicy(&s3.DeleteBucketPolicyInput{ + Bucket: aws.String(bucket), + }) + + if err != nil { + return fmt.Errorf("Error deleting S3 policy: %s", err) + } } + return nil +} + +func resourceAwsS3BucketWebsiteUpdate(s3conn *s3.S3, d *schema.ResourceData) error { ws := d.Get("website").([]interface{}) if len(ws) == 1 { @@ -330,6 +389,19 @@ func WebsiteEndpointUrl(bucket string, region string) string { return fmt.Sprintf("%s.s3-website-%s.amazonaws.com", bucket, region) } +func normalizeJson(jsonString interface{}) string { + if jsonString == nil { + return "" + } + j := make(map[string]interface{}) + err := json.Unmarshal([]byte(jsonString.(string)), &j) + if err != nil { + return fmt.Sprintf("Error parsing JSON: %s", err) + } + b, _ := json.Marshal(j) + return string(b[:]) +} + func normalizeRegion(region string) string { // Default to us-east-1 if the bucket doesn't have a region: // http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETlocation.html diff --git a/builtin/providers/aws/resource_aws_s3_bucket_test.go b/builtin/providers/aws/resource_aws_s3_bucket_test.go index 051ab5474..8e601d279 100644 --- a/builtin/providers/aws/resource_aws_s3_bucket_test.go +++ b/builtin/providers/aws/resource_aws_s3_bucket_test.go @@ -1,8 +1,11 @@ package aws import ( + "encoding/json" "fmt" "math/rand" + "reflect" + "strconv" "testing" "time" @@ -35,6 +38,32 @@ func TestAccAWSS3Bucket_basic(t *testing.T) { }) } +func TestAccAWSS3Bucket_Policy(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSS3BucketDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSS3BucketConfigWithPolicy, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketPolicy( + "aws_s3_bucket.bucket", testAccAWSS3BucketPolicy), + ), + }, + resource.TestStep{ + Config: testAccAWSS3BucketConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSS3BucketExists("aws_s3_bucket.bucket"), + testAccCheckAWSS3BucketPolicy( + "aws_s3_bucket.bucket", ""), + ), + }, + }, + }) +} + func TestAccAWSS3Bucket_Website(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -145,6 +174,46 @@ func testAccCheckAWSS3BucketExists(n string) resource.TestCheckFunc { } } +func testAccCheckAWSS3BucketPolicy(n string, policy string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, _ := s.RootModule().Resources[n] + conn := testAccProvider.Meta().(*AWSClient).s3conn + + out, err := conn.GetBucketPolicy(&s3.GetBucketPolicyInput{ + Bucket: aws.String(rs.Primary.ID), + }) + + if err != nil { + if policy == "" { + // expected + return nil + } else { + return fmt.Errorf("GetBucketPolicy error: %v, expected %s", err, policy) + } + } + + if v := out.Policy; v == nil { + if policy != "" { + return fmt.Errorf("bad policy, found nil, expected: %s", policy) + } + } else { + expected := make(map[string]interface{}) + if err := json.Unmarshal([]byte(policy), &expected); err != nil { + return err + } + actual := make(map[string]interface{}) + if err := json.Unmarshal([]byte(*v), &actual); err != nil { + return err + } + + if !reflect.DeepEqual(expected, actual) { + return fmt.Errorf("bad policy, expected: %#v, got %#v", expected, actual) + } + } + + return nil + } +} func testAccCheckAWSS3BucketWebsite(n string, indexDoc string, errorDoc string, redirectTo string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, _ := s.RootModule().Resources[n] @@ -202,6 +271,8 @@ func testAccCheckAWSS3BucketWebsite(n string, indexDoc string, errorDoc string, // within AWS var randInt = rand.New(rand.NewSource(time.Now().UnixNano())).Int() var testAccWebsiteEndpoint = fmt.Sprintf("tf-test-bucket-%d.s3-website-us-west-2.amazonaws.com", randInt) +var testAccAWSS3BucketPolicy = fmt.Sprintf(`{ "Version": "2008-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::tf-test-bucket-%d/*" } ] }`, randInt) + var testAccAWSS3BucketConfig = fmt.Sprintf(` resource "aws_s3_bucket" "bucket" { bucket = "tf-test-bucket-%d" @@ -242,3 +313,11 @@ resource "aws_s3_bucket" "bucket" { } } `, randInt) + +var testAccAWSS3BucketConfigWithPolicy = fmt.Sprintf(` +resource "aws_s3_bucket" "bucket" { + bucket = "tf-test-bucket-%d" + acl = "public-read" + policy = %s +} +`, randInt, strconv.Quote(testAccAWSS3BucketPolicy)) diff --git a/website/source/docs/providers/aws/r/s3_bucket.html.markdown b/website/source/docs/providers/aws/r/s3_bucket.html.markdown index 78e0938f8..83c6b241b 100644 --- a/website/source/docs/providers/aws/r/s3_bucket.html.markdown +++ b/website/source/docs/providers/aws/r/s3_bucket.html.markdown @@ -32,6 +32,7 @@ resource "aws_s3_bucket" "b" { resource "aws_s3_bucket" "b" { bucket = "s3-website-test.hashicorp.com" acl = "public-read" + policy = "#{file("policy.json")}" website { index_document = "index.html" @@ -46,6 +47,7 @@ The following arguments are supported: * `bucket` - (Required) The name of the bucket. * `acl` - (Optional) The [canned ACL](http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) to apply. Defaults to "private". +* `policy` - (Optional) A valid [bucket policy](http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html) JSON document. * `tags` - (Optional) A mapping of tags to assign to the bucket. * `website` - (Optional) A website object (documented below).