Skip to content

Commit

Permalink
provider/aws: Add aws_s3_bucket_policy resource
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jen20 committed Sep 1, 2016
1 parent 66d4298 commit 3abd21c
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 6 deletions.
15 changes: 15 additions & 0 deletions builtin/providers/aws/diff_suppress_funcs.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
12 changes: 6 additions & 6 deletions builtin/providers/aws/resource_aws_s3_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down
112 changes: 112 additions & 0 deletions builtin/providers/aws/resource_aws_s3_bucket_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package aws

import (
//"encoding/json"
"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

pol, err := s3conn.GetBucketPolicy(&s3.GetBucketPolicyInput{
Bucket: aws.String(d.Id()),
})
log.Printf("[DEBUG] S3 bucket: %s, read policy", d.Id())
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", 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
}
180 changes: 180 additions & 0 deletions builtin/providers/aws/resource_aws_s3_bucket_policy_test.go
Original file line number Diff line number Diff line change
@@ -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/iampolicyequivalence"
)

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)
}

0 comments on commit 3abd21c

Please sign in to comment.