diff --git a/builtin/providers/aws/diff_suppress_funcs.go b/builtin/providers/aws/diff_suppress_funcs.go new file mode 100644 index 000000000000..408063e2668c --- /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 e95ad615b51a..9d1580d2b6ef 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 f4637d93ece2..28ae61e34395 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 000000000000..4485f11a77be --- /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 000000000000..8dedae1a09e7 --- /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/vendor/github.com/jen20/awspolicyequivalence/README.md b/vendor/github.com/jen20/awspolicyequivalence/README.md new file mode 100644 index 000000000000..3883fafaaad5 --- /dev/null +++ b/vendor/github.com/jen20/awspolicyequivalence/README.md @@ -0,0 +1,7 @@ +## AWS Policy Equivalence Library + +This library checks for structural equivalence of two AWS policy documents. See Godoc for more information on usage. + +### CI + +Travis CI Build Status diff --git a/vendor/github.com/jen20/awspolicyequivalence/aws_policy_equivalence.go b/vendor/github.com/jen20/awspolicyequivalence/aws_policy_equivalence.go new file mode 100644 index 000000000000..8c2a66c48628 --- /dev/null +++ b/vendor/github.com/jen20/awspolicyequivalence/aws_policy_equivalence.go @@ -0,0 +1,334 @@ +package awspolicy + +import ( + "reflect" + "encoding/json" + + "github.com/hashicorp/errwrap" +) + +// PoliciesAreEquivalent tests for the structural equivalence of two +// AWS policies. It does not read into the semantics, other than treating +// single element string arrays as equivalent to a string without an +// array, as the AWS endpoints do. +// +// It will, however, detect reordering and ignore whitespace. +// +// Returns true if the policies are structurally equivalent, false +// otherwise. If either of the input strings are not valid JSON, +// false is returned along with an error. +func PoliciesAreEquivalent(policy1, policy2 string) (bool, error) { + policy1doc := &awsPolicyDocument{} + if err := json.Unmarshal([]byte(policy1), policy1doc); err != nil { + return false, errwrap.Wrapf("Error unmarshaling policy: {{err}}", err) + } + + policy2doc := &awsPolicyDocument{} + if err := json.Unmarshal([]byte(policy2), policy2doc); err != nil { + return false, errwrap.Wrapf("Error unmarshaling policy: {{err}}", err) + } + + return policy1doc.equals(policy2doc), nil +} + +type awsPolicyDocument struct { + Version string `json:",omitempty"` + Id string `json:",omitempty"` + Statements []*awsPolicyStatement `json:"Statement"` +} + +func (doc *awsPolicyDocument) equals(other *awsPolicyDocument) bool { + // Check the basic fields of the document + if doc.Version != other.Version { + return false + } + if doc.Id != other.Id { + return false + } + + // If we have different number of statements we are very unlikely + // to have them be equivalent. + if len(doc.Statements) != len(other.Statements) { + return false + } + + // If we have the same number of statements in the policy, does + // each statement in the doc have a corresponding statement in + // other which is equal? If no, policies are not equal, if yes, + // then they may be. + for _, ours := range doc.Statements { + found := false + for _, theirs := range other.Statements { + if ours.equals(theirs) { + found = true + } + } + + if !found { + return false + } + } + + // Now we need to repeat this process the other way around to + // ensure we don't have any matching errors. + for _, theirs := range other.Statements { + found := false + for _, ours := range doc.Statements { + if theirs.equals(ours) { + found = true + } + } + + if !found { + return false + } + } + + return true +} + +type awsPolicyStatement struct { + Sid string `json:",omitempty"` + Effect string `json:",omitempty"` + Actions interface{} `json:"Action,omitempty"` + NotActions interface{} `json:"NotAction,omitempty"` + Resources interface{} `json:"Resource,omitempty"` + NotResources interface{} `json:"NotResource,omitempty"` + Principals interface{} `json:"Principal,omitempty"` + NotPrincipals interface{} `json:"NotPrincipal,omitempty"` + Conditions map[string]map[string]interface{} `json:"Condition,omitempty"` +} + +func (statement *awsPolicyStatement) equals(other *awsPolicyStatement) bool { + if statement.Sid != other.Sid { + return false + } + + if statement.Effect != other.Effect { + return false + } + + ourActions := newAWSStringSet(statement.Actions) + theirActions := newAWSStringSet(other.Actions) + if !ourActions.equals(theirActions) { + return false + } + + ourNotActions := newAWSStringSet(statement.NotActions) + theirNotActions := newAWSStringSet(other.NotActions) + if !ourNotActions.equals(theirNotActions) { + return false + } + + ourResources := newAWSStringSet(statement.Resources) + theirResources := newAWSStringSet(other.Resources) + if !ourResources.equals(theirResources) { + return false + } + + ourNotResources := newAWSStringSet(statement.NotResources) + theirNotResources := newAWSStringSet(other.NotResources) + if !ourNotResources.equals(theirNotResources) { + return false + } + + ourConditionsBlock := awsConditionsBlock(statement.Conditions) + theirConditionsBlock := awsConditionsBlock(other.Conditions) + if !ourConditionsBlock.Equals(theirConditionsBlock) { + return false + } + + if statement.Principals != nil || other.Principals != nil { + stringPrincipalsEqual := stringPrincipalsEqual(statement.Principals, other.Principals) + mapPrincipalsEqual := mapPrincipalsEqual(statement.Principals, other.Principals) + if !(stringPrincipalsEqual || mapPrincipalsEqual) { + return false + } + } + + if statement.NotPrincipals != nil || other.NotPrincipals != nil { + stringNotPrincipalsEqual := stringPrincipalsEqual(statement.NotPrincipals, other.NotPrincipals) + mapNotPrincipalsEqual := mapPrincipalsEqual(statement.NotPrincipals, other.NotPrincipals) + if !(stringNotPrincipalsEqual || mapNotPrincipalsEqual) { + return false + } + } + + return true +} + +func mapPrincipalsEqual(ours, theirs interface{}) bool { + ourPrincipalMap, ok := ours.(map[string]interface{}) + if !ok { + return false + } + + theirPrincipalMap, ok := theirs.(map[string]interface{}) + if ! ok { + return false + } + + oursNormalized := make(map[string]awsStringSet) + for key, val := range ourPrincipalMap { + oursNormalized[key] = newAWSStringSet(val) + } + + theirsNormalized := make(map[string]awsStringSet) + for key, val := range theirPrincipalMap { + theirsNormalized[key] = newAWSStringSet(val) + } + + for key, ours := range oursNormalized { + theirs, ok := theirsNormalized[key] + if !ok { + return false + } + + if !ours.equals(theirs) { + return false + } + } + + for key, theirs := range theirsNormalized { + ours, ok := oursNormalized[key] + if !ok { + return false + } + + if !theirs.equals(ours) { + return false + } + } + + return true +} + +func stringPrincipalsEqual(ours, theirs interface{}) bool { + ourPrincipal, oursIsString := ours.(string) + theirPrincipal, theirsIsString := theirs.(string) + + if !(oursIsString && theirsIsString) { + return false + } + + if ourPrincipal == theirPrincipal { + return true + } + + return false +} + + +type awsConditionsBlock map[string]map[string]interface{} + +func (conditions awsConditionsBlock) Equals(other awsConditionsBlock) bool { + if conditions == nil && other != nil || other == nil && conditions != nil { + return false + } + + if len(conditions) != len(other) { + return false + } + + oursNormalized := make(map[string]map[string]awsStringSet) + for key, condition := range conditions { + normalizedCondition := make(map[string]awsStringSet) + for innerKey, val := range condition { + normalizedCondition[innerKey] = newAWSStringSet(val) + } + oursNormalized[key] = normalizedCondition + } + + theirsNormalized := make(map[string]map[string]awsStringSet) + for key, condition := range other { + normalizedCondition := make(map[string]awsStringSet) + for innerKey, val := range condition { + normalizedCondition[innerKey] = newAWSStringSet(val) + } + theirsNormalized[key] = normalizedCondition + } + + for key, ours := range oursNormalized { + theirs, ok := theirsNormalized[key] + if !ok { + return false + } + + for innerKey, oursInner := range ours { + theirsInner, ok := theirs[innerKey] + if ! ok { + return false + } + + if !oursInner.equals(theirsInner) { + return false + } + } + } + + for key, theirs := range theirsNormalized { + ours, ok := oursNormalized[key] + if !ok { + return false + } + + for innerKey, theirsInner := range theirs { + oursInner, ok := ours[innerKey] + if ! ok { + return false + } + + if !theirsInner.equals(oursInner) { + return false + } + } + } + + return true +} + + +type awsStringSet []string + +// newAWSStringSet constructs an awsStringSet from an interface{} - which +// may be nil, a single string, or []interface{} (each of which is a string). +// This corresponds with how structures come off the JSON unmarshaler +// without any custom encoding rules. +func newAWSStringSet(members interface{}) awsStringSet { + if members == nil { + return awsStringSet{} + } + + if single, ok := members.(string); ok { + return awsStringSet{single} + } + + if multiple, ok := members.([]interface{}); ok { + actions := make([]string, len(multiple)) + for i, action := range multiple { + actions[i] = action.(string) + } + return awsStringSet(actions) + } + + return nil +} + +func (actions awsStringSet) equals(other awsStringSet) bool { + if len(actions) != len(other) { + return false + } + + ourMap := map[string]struct{}{} + theirMap := map[string]struct{}{} + + for _, action := range actions { + ourMap[action] = struct{}{} + } + + for _, action := range other { + theirMap[action] = struct{}{} + } + + return reflect.DeepEqual(ourMap, theirMap) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index e4ea8389781f..524759386e6e 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1295,6 +1295,12 @@ "path": "github.com/influxdata/influxdb/pkg/escape", "revision": "f233a8bac88d1f2dc282a98186f5a3363b806181" }, + { + "checksumSHA1": "cCSJGF1h+suYcgMq7wEm1carknw=", + "path": "github.com/jen20/awspolicyequivalence", + "revision": "6b9230008577fc3dcd10c104ce8fb16ed679bf66", + "revisionTime": "2016-09-01T18:24:20Z" + }, { "checksumSHA1": "oPpOfZn11Ef6DWOoETxSW9Venzs=", "path": "github.com/jen20/riviera/azure", 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 000000000000..b9789e2a6fa6 --- /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 04dbccfbd733..6a2ae1614bee 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 +