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

internal/verify: handle policy parsing errors of state content #39842

Merged
merged 4 commits into from
Oct 23, 2024
Merged
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
19 changes: 19 additions & 0 deletions .changelog/39842.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
```release-note:bug
resource/aws_iam_policy: Fix persistent validation errors when malformed `policy` content is written to state
```
```release-note:bug
resource/aws_iam_role_policy: Fix persistent validation errors when malformed `policy` content is written to state
```
```release-note:bug
resource/aws_ecr_repository_policy: Fix persistent validation errors when malformed `policy` content is written to state
```
```release-note:bug
resource/aws_secretsmanager_secret: Fix persistent validation errors when malformed `policy` content is written to state
```
```release-note:bug
resource/aws_s3_bucket_policy: Fix persistent validation errors when malformed `policy` content is written to state
```
```release-note:bug
resource/aws_kms_key: Fix persistent validation errors when malformed `policy` content is written to state
```

122 changes: 122 additions & 0 deletions internal/service/iam/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,57 @@ func TestAccIAMPolicy_policyDuplicateKeys(t *testing.T) {
})
}

// TestAccIAMPolicy_malformedCondition verifies that malformed policy content
// that is stored in state does not prevent subsequent plan and apply operations
// from proceeding.
//
// Ref: https://github.com/hashicorp/terraform-provider-aws/issues/39833
func TestAccIAMPolicy_malformedCondition(t *testing.T) {
ctx := acctest.Context(t)
var out awstypes.Policy
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_iam_policy.test"
expectedPolicyText1 := `{"Statement":[{"Action":["s3:ListBucket"],"Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"}`
expectedPolicyText2 := `{"Statement":[{"Action":["s3:ListBucket"],"Condition":{"StringLike":["demo-prefix/"]}",Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"}`
expectedPolicyText3 := `{"Statement":[{"Action":["s3:ListBucket"],"Condition":{"StringLike":{"s3:prefix":["demo-prefix/"]}},"Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"}`

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.IAMServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckPolicyDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccPolicyConfig_MalformedCondition_setup(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckPolicyExists(ctx, resourceName, &out),
resource.TestCheckResourceAttr(resourceName, names.AttrName, rName),
resource.TestCheckResourceAttr(resourceName, names.AttrPolicy, expectedPolicyText1),
),
},
{
Config: testAccPolicyConfig_MalformedCondition_failure(rName),
ExpectError: regexache.MustCompile(`MalformedPolicyDocument: Syntax errors in policy`),
Check: resource.ComposeTestCheckFunc(
testAccCheckPolicyExists(ctx, resourceName, &out),
resource.TestCheckResourceAttr(resourceName, names.AttrName, rName),
// Because this is a Plugin SDK V2-based resource, the malformed content
// is stored in state despite the failed update.
resource.TestCheckResourceAttr(resourceName, names.AttrPolicy, expectedPolicyText2),
),
},
{
Config: testAccPolicyConfig_MalformedCondition_fix(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckPolicyExists(ctx, resourceName, &out),
resource.TestCheckResourceAttr(resourceName, names.AttrName, rName),
resource.TestCheckResourceAttr(resourceName, names.AttrPolicy, expectedPolicyText3),
),
},
},
})
}

func testAccCheckPolicyExists(ctx context.Context, n string, v *awstypes.Policy) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
Expand Down Expand Up @@ -624,3 +675,74 @@ EOF
}
`, rName)
}

func testAccPolicyConfig_MalformedCondition_setup(rName string) string {
return fmt.Sprintf(`
resource "aws_iam_policy" "test" {
name = %q

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Resource = "*"
Action = [
"s3:ListBucket",
]
},
]
})
}
`, rName)
}

func testAccPolicyConfig_MalformedCondition_failure(rName string) string {
return fmt.Sprintf(`
resource "aws_iam_policy" "test" {
name = %q

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Resource = "*"
Action = [
"s3:ListBucket",
]
"Condition" : {
"StringLike" : ["demo-prefix/"]
}
},
]
})
}
`, rName)
}

func testAccPolicyConfig_MalformedCondition_fix(rName string) string {
return fmt.Sprintf(`
resource "aws_iam_policy" "test" {
name = %q

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Resource = "*"
Action = [
"s3:ListBucket",
]
"Condition" : {
"StringLike" : {
"s3:prefix" : ["demo-prefix/"]
}
}
},
]
})
}
`, rName)
}
17 changes: 17 additions & 0 deletions internal/verify/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
awspolicy "github.com/hashicorp/awspolicyequivalence"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
)

// SuppressEquivalentPolicyDiffs returns a difference suppression function that compares
Expand Down Expand Up @@ -126,6 +127,11 @@ func JSONBytesEqual(b1, b2 []byte) bool {
return reflect.DeepEqual(o1, o2)
}

// SecondJSONUnlessEquivalent returns the second JSON string unless
// the AWS policy content is deemed equivalent.
//
// If parsing of the policy content from the first argument fails, the
// second is returned and no error is raised.
func SecondJSONUnlessEquivalent(old, new string) (string, error) {
// valid empty JSON is "{}" not "" so handle special case to avoid
// Error unmarshaling policy: unexpected end of JSON input
Expand All @@ -144,6 +150,17 @@ func SecondJSONUnlessEquivalent(old, new string) (string, error) {
equivalent, err := awspolicy.PoliciesAreEquivalent(old, new)

if err != nil {
// Plugin SDK V2 based resources can set malformed policy content in state
// despite a failed update. In these cases, parsing the "old" content
// will fail. Surfacing this error during read operations causes a
// persistent plan-time validation error, so return the "new" content
// read directly from the remote resource instead.
//
// Ref: https://github.com/hashicorp/terraform-provider-aws/issues/39833
if errs.Contains(err, "parsing policy 1") {
return new, nil
}

return "", err
}

Expand Down
42 changes: 42 additions & 0 deletions internal/verify/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,48 @@ func TestSecondJSONUnlessEquivalent(t *testing.T) {
newPolicy: "",
want: "",
},
{
name: "malformed old",
oldPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Condition" : {
"StringLike" : ["demo-prefix/"]
},
"Resource": "*"
}
]
}`,
newPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "*"
}
]
}`,
want: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "*"
}
]
}`,
},
}

for _, v := range testCases {
Expand Down
Loading