Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws_s3_bucket - Fix policy normalization to match AWS API #4278

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion builtin/providers/aws/resource_aws_elasticsearch_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource {
Schema: map[string]*schema.Schema{
"access_policies": &schema.Schema{
Type: schema.TypeString,
StateFunc: normalizeJson,
StateFunc: normalizePolicyDocument,
Optional: true,
},
"advanced_options": &schema.Schema{
Expand Down
17 changes: 2 additions & 15 deletions builtin/providers/aws/resource_aws_s3_bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func resourceAwsS3Bucket() *schema.Resource {
"policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
StateFunc: normalizeJson,
StateFunc: normalizePolicyDocument,
},

"cors_rule": &schema.Schema{
Expand Down Expand Up @@ -436,7 +436,7 @@ func resourceAwsS3BucketRead(d *schema.ResourceData, meta interface{}) error {
if err := d.Set("policy", ""); err != nil {
return err
}
} else if err := d.Set("policy", normalizeJson(*v)); err != nil {
} else if err := d.Set("policy", normalizePolicyDocument(*v)); err != nil {
return err
}
}
Expand Down Expand Up @@ -1267,19 +1267,6 @@ func removeNil(data map[string]interface{}) map[string]interface{} {
return withoutNil
}

func normalizeJson(jsonString interface{}) string {
if jsonString == nil || jsonString == "" {
return ""
}
var j 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
Expand Down
159 changes: 159 additions & 0 deletions builtin/providers/aws/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"log"
"sort"
"strconv"
"strings"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/aws/aws-sdk-go/service/redshift"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/mapstructure"
)

// Takes the result of flatmap.Expand for an array of listeners and
Expand Down Expand Up @@ -1274,3 +1276,160 @@ func flattenApiGatewayThrottleSettings(settings *apigateway.ThrottleSettings) []

return result
}

// normalizeJson is a JSON-in, JSON-out function that assures JSON strings
// supplied by the user appear the same as they would in AWS.
//
// Note that this function does not *sort* JSON, and as such may be insufficient
// to guard against unneeded changes in things such as IAM and resource policy
// documents. For such functionality, see normalizePolicyDocument.
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[:])
}

// IAM policy document normalization logic.
//
// AWS will change policies that are submitted to conform to the following
// guidelines, even though other forms are not necessarily syntatically correct:
//
// * When a policy statement has no Sid specified, it will create a Sid entry
// with blank data.
// * When a policy statement only has one action, but is specified in an array,
// it will truncate the array and use a single element string action instead.
// (ie: "Action": [ "s3:ListBucket" ] becomes "Action": "s3:ListBucket")
// * Multiple actions will be sorted alphabetically.
// * The general policy document structure also follows an ordered structure
// that needs to be mimicked in order for the document to truly match.
//
// The objective of these functions is to accept a valid Policy JSON file,
// fix it to conform the following above guidelines, and then return the JSON.
//

// struct for the top-level of the document.
type policyDocument struct {
Version string
Id string `json:",omitempty"`
Statement []policyDocumentStatement
}

// struct for policy document statements.
type policyDocumentStatement struct {
Sid string
Effect string
Principal interface{} `json:",omitempty"`
NotPrincipal interface{} `json:",omitempty"`
Action interface{} `json:",omitempty"`
NotAction interface{} `json:",omitempty"`
Resource interface{} `json:",omitempty"`
NotResource interface{} `json:",omitempty"`
Condition map[string]map[string]interface{} `json:",omitempty"`
}

type policyStatementPrincipal struct {
AWS interface{} `json:",omitempty"`
CanonicalUser interface{} `json:",omitempty"`
Federated interface{} `json:",omitempty"`
Service interface{} `json:",omitempty"`
}

// policyDocArray allows us to define a slice of interfaces to sort, versus
// a slice of strings.
type policyDocArray []interface{}

func (a policyDocArray) Len() int { return len(a) }
func (a policyDocArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// policyDocArray.Less() is implemented to sort only on neighboring string values,
// so if the array contains non-string values, this will give unstable results.
func (a policyDocArray) Less(i, j int) bool {
switch ival := a[i].(type) {
case string:
if jval, ok := a[j].(string); ok {
log.Printf("[DEBUG] sort: %s < %s", ival, jval)
return ival < jval
}
}
return false
}

// normalizePolicyDocument takes the JSON policy, normalizes the statements
// and sorts any arrays (namely actions).
func normalizePolicyDocument(jsonString interface{}) string {
if jsonString == nil {
return ""
}
var j policyDocument
err := json.Unmarshal([]byte(jsonString.(string)), &j)
if err != nil {
return fmt.Sprintf("Error parsing JSON: %s", err)
}
normalizePolicyStatements(j.Statement)
b, _ := json.Marshal(j)
return string(b[:])
}

// normalizePolicyStatements calls out to normalize the various fields in the
// policy document struct that could have arrays. This then calls out to
// normalizeStatementElements.
func normalizePolicyStatements(statements []policyDocumentStatement) {
for i, v := range statements {
statements[i].Action = normalizeStatementElements(v.Action)
statements[i].NotAction = normalizeStatementElements(v.NotAction)
statements[i].Resource = normalizeStatementElements(v.Resource)
statements[i].NotResource = normalizeStatementElements(v.NotResource)
// Principal and NotPrincipal
// more than likely if this fails, we are not dealing with a principal
// struct, possibly just a "*" in as the principal. we ignore errors
var principalStruct policyStatementPrincipal
var notPrincipalStruct policyStatementPrincipal
if err := mapstructure.Decode(statements[i].Principal, &principalStruct); err == nil && statements[i].Principal != nil {
principalStruct.AWS = normalizeStatementElements(principalStruct.AWS)
principalStruct.CanonicalUser = normalizeStatementElements(principalStruct.CanonicalUser)
principalStruct.Federated = normalizeStatementElements(principalStruct.Federated)
principalStruct.Service = normalizeStatementElements(principalStruct.Service)
statements[i].Principal = principalStruct
}
if err := mapstructure.Decode(statements[i].NotPrincipal, &notPrincipalStruct); err == nil && statements[i].NotPrincipal != nil {
notPrincipalStruct.AWS = normalizeStatementElements(notPrincipalStruct.AWS)
notPrincipalStruct.CanonicalUser = normalizeStatementElements(notPrincipalStruct.CanonicalUser)
notPrincipalStruct.Federated = normalizeStatementElements(notPrincipalStruct.Federated)
notPrincipalStruct.Service = normalizeStatementElements(notPrincipalStruct.Service)
statements[i].NotPrincipal = notPrincipalStruct
}
// The Condition object - 2-level deep flattening
for ce := range statements[i].Condition {
for ck, cv := range statements[i].Condition[ce] {
statements[i].Condition[ce][ck] = normalizeStatementElements(cv)
}
}
}
}

// normalizeStatementElements does one of two things:
// * If element is a single-element array/slice, it will "flatten" it and
// return the individual value.
// * If element is a multi-value array, the string values in the array are
// sorted.
func normalizeStatementElements(element interface{}) interface{} {
log.Printf("[DEBUG] checking to either truncate or sort policy doc element %v ", element)
switch element := element.(type) {
case []interface{}:
if len(element) < 2 {
log.Printf("[DEBUG] single element %s found in policy, returning single element", element[0])
return element[0]
}
log.Printf("[DEBUG] multiple elements %v found in policy, sorting strings", element)
sort.Sort(policyDocArray(element))
return element
}
return element
}
Loading