Skip to content

Commit

Permalink
Merge pull request #29226 from hashicorp/b-sns-policy-principal-race
Browse files Browse the repository at this point in the history
Fix race condition in SNS topic and topic policy resources
  • Loading branch information
gdavison authored Feb 6, 2023
2 parents 75a49fc + b8d8f3e commit 20bc8d9
Show file tree
Hide file tree
Showing 7 changed files with 437 additions and 32 deletions.
8 changes: 8 additions & 0 deletions .changelog/29226.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

```release-note:bug
resource/aws_sns_topic: Fixes potential race condition when reading policy document.
```

```release-note:bug
resource/aws_sns_topic_policy: Fixes potential race condition when reading policy document.
```
48 changes: 48 additions & 0 deletions internal/acctest/acctest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package acctest

import (
"context"
"encoding/json"
"fmt"
"log"
"os"
Expand Down Expand Up @@ -37,6 +38,7 @@ import (
tforganizations "github.com/hashicorp/terraform-provider-aws/internal/service/organizations"
tfsts "github.com/hashicorp/terraform-provider-aws/internal/service/sts"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/jmespath/go-jmespath"
)

const (
Expand Down Expand Up @@ -582,6 +584,52 @@ func CheckResourceAttrEquivalentJSON(resourceName, attributeName, expectedJSON s
}
}

func CheckResourceAttrJMESPair(nameFirst, keyFirst, jmesPath, nameSecond, keySecond string) resource.TestCheckFunc {
return func(s *terraform.State) error {
first, err := PrimaryInstanceState(s, nameFirst)
if err != nil {
return err
}

second, err := PrimaryInstanceState(s, nameSecond)
if err != nil {
return err
}

vFirst, okFirst := first.Attributes[keyFirst]
if !okFirst {
return fmt.Errorf("%s: Attribute %q not set", nameFirst, keyFirst)
}

var jsonData any
err = json.Unmarshal([]byte(vFirst), &jsonData)
if err != nil {
return fmt.Errorf("%s: Expected attribute %q to be JSON: %w", nameFirst, keyFirst, err)
}

result, err := jmespath.Search(jmesPath, jsonData)
if err != nil {
return fmt.Errorf("Invalid JMESPath %q: %w", jmesPath, err)
}

value, ok := result.(string)
if !ok {
return fmt.Errorf("%s: Attribute %q, JMESPath %q, expected single string, got %#v", nameFirst, keyFirst, jmesPath, result)
}

vSecond, okSecond := second.Attributes[keySecond]
if !okSecond {
return fmt.Errorf("%s: Attribute %q, JMESPath %q is %q, but %q is not set in %s", nameFirst, keyFirst, jmesPath, value, keySecond, nameSecond)
}

if value != vSecond {
return fmt.Errorf("%s: Attribute %q, JMESPath %q, expected %q, got %q", nameFirst, keyFirst, jmesPath, vSecond, value)
}

return nil
}
}

// Copied and inlined from the SDK testing code
func PrimaryInstanceState(s *terraform.State, name string) (*terraform.InstanceState, error) {
rs, ok := s.RootModule().Resources[name]
Expand Down
28 changes: 28 additions & 0 deletions internal/service/sns/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sns

import (
"context"
"errors"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/sns"
Expand Down Expand Up @@ -35,7 +36,34 @@ func FindPlatformApplicationAttributesByARN(ctx context.Context, conn *sns.SNS,
return aws.StringValueMap(output.Attributes), nil
}

// FindTopicAttributesByARN returns topic attributes, ensuring that any Policy field is populated with
// valid principals, i.e. the principal is either an AWS Account ID or an ARN
func FindTopicAttributesByARN(ctx context.Context, conn *sns.SNS, arn string) (map[string]string, error) {
var attributes map[string]string
err := tfresource.Retry(ctx, propagationTimeout, func() *resource.RetryError {
var err error
attributes, err = GetTopicAttributesByARN(ctx, conn, arn)
if err != nil {
return resource.NonRetryableError(err)
}

valid, err := policyHasValidAWSPrincipals(attributes[TopicAttributeNamePolicy])
if err != nil {
return resource.NonRetryableError(err)
}
if !valid {
return resource.RetryableError(errors.New("contains invalid principals"))
}

return nil
})

return attributes, err
}

// GetTopicAttributesByARN returns topic attributes without any validation. Any principals in a Policy field
// may contain Unique IDs instead of valid values. To ensure policies are valid, use FindTopicAttributesByARN
func GetTopicAttributesByARN(ctx context.Context, conn *sns.SNS, arn string) (map[string]string, error) {
input := &sns.GetTopicAttributesInput{
TopicArn: aws.String(arn),
}
Expand Down
68 changes: 62 additions & 6 deletions internal/service/sns/topic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sns

import (
"context"
"encoding/json"
"fmt"
"log"
"regexp"
Expand All @@ -19,9 +20,11 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/attrmap"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/jmespath/go-jmespath"
)

var (
Expand Down Expand Up @@ -290,7 +293,8 @@ func resourceTopicCreate(ctx context.Context, d *schema.ResourceData, meta inter
return resourceTopicRead(ctx, d, meta)
}

func resourceTopicRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceTopicRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).SNSConn()
defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig
Expand All @@ -302,9 +306,8 @@ func resourceTopicRead(ctx context.Context, d *schema.ResourceData, meta interfa
d.SetId("")
return nil
}

if err != nil {
return diag.Errorf("reading SNS Topic (%s): %s", d.Id(), err)
return sdkdiag.AppendErrorf(diags, "reading SNS Topic (%s): %s", d.Id(), err)
}

err = topicAttributeMap.APIAttributesToResourceData(attributes, d)
Expand Down Expand Up @@ -336,23 +339,76 @@ func resourceTopicRead(ctx context.Context, d *schema.ResourceData, meta interfa
}

if err != nil {
return diag.Errorf("listing tags for SNS Topic (%s): %s", d.Id(), err)
return sdkdiag.AppendErrorf(diags, "listing tags for SNS Topic (%s): %s", d.Id(), err)
}

tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig)

//lintignore:AWSR002
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
return diag.Errorf("setting tags: %s", err)
return sdkdiag.AppendErrorf(diags, "setting tags: %s", err)
}

if err := d.Set("tags_all", tags.Map()); err != nil {
return diag.Errorf("setting tags_all: %s", err)
return sdkdiag.AppendErrorf(diags, "setting tags_all: %s", err)
}

return nil
}

// policyHasValidAWSPrincipals validates that the Principals in an IAM Policy are valid
// Assumes that non-"AWS" Principals are valid
// The value can be a single string or a slice of strings
// Valid strings are either an ARN or an AWS account ID
func policyHasValidAWSPrincipals(policy string) (bool, error) { // nosemgrep:ci.aws-in-func-name
var policyData any
err := json.Unmarshal([]byte(policy), &policyData)
if err != nil {
return false, fmt.Errorf("parsing policy: %w", err)
}

result, err := jmespath.Search("Statement[*].Principal.AWS", policyData)
if err != nil {
return false, fmt.Errorf("parsing policy: %w", err)
}

principals, ok := result.([]any)
if !ok {
return false, fmt.Errorf(`parsing policy: unexpected result: (%[1]T) "%[1]v"`, result)
}

for _, principal := range principals {
switch x := principal.(type) {
case string:
if !isValidAWSPrincipal(x) {
return false, nil
}
case []string:
for _, s := range x {
if !isValidAWSPrincipal(s) {
return false, nil
}
}
}
}

return true, nil
}

// isValidAWSPrincipal returns true if a string is either an ARN, an AWS account ID, or `*`
func isValidAWSPrincipal(principal string) bool { // nosemgrep:ci.aws-in-func-name
if principal == "*" {
return true
}
if arn.IsARN(principal) {
return true
}
if regexp.MustCompile(`^\d{12}$`).MatchString(principal) {
return true
}
return false
}

func resourceTopicUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).SNSConn()

Expand Down
Loading

0 comments on commit 20bc8d9

Please sign in to comment.