diff --git a/.changelog/30008.txt b/.changelog/30008.txt new file mode 100644 index 00000000000..03e91065d2b --- /dev/null +++ b/.changelog/30008.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_sns_topic_data_protection_policy +``` diff --git a/internal/service/sns/service_package_gen.go b/internal/service/sns/service_package_gen.go index 17f888b5594..c7dddd11024 100644 --- a/internal/service/sns/service_package_gen.go +++ b/internal/service/sns/service_package_gen.go @@ -46,6 +46,10 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: "id", }, }, + { + Factory: ResourceTopicDataProtectionPolicy, + TypeName: "aws_sns_topic_data_protection_policy", + }, { Factory: ResourceTopicPolicy, TypeName: "aws_sns_topic_policy", diff --git a/internal/service/sns/topic_data_protection_policy.go b/internal/service/sns/topic_data_protection_policy.go new file mode 100644 index 00000000000..a92e57b5c32 --- /dev/null +++ b/internal/service/sns/topic_data_protection_policy.go @@ -0,0 +1,126 @@ +package sns + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +// @SDKResource("aws_sns_topic_data_protection_policy") +func ResourceTopicDataProtectionPolicy() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceTopicDataProtectionPolicyUpsert, + ReadWithoutTimeout: resourceTopicDataProtectionPolicyRead, + UpdateWithoutTimeout: resourceTopicDataProtectionPolicyUpsert, + DeleteWithoutTimeout: resourceTopicDataProtectionPolicyDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + "policy": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: verify.SuppressEquivalentPolicyDiffs, + DiffSuppressOnRefresh: true, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + }, + } +} + +func resourceTopicDataProtectionPolicyUpsert(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SNSConn() + + topicArn := d.Get("arn").(string) + policy, err := structure.NormalizeJsonString(d.Get("policy").(string)) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "policy (%s) is invalid JSON: %s", d.Get("policy").(string), err) + } + + input := &sns.PutDataProtectionPolicyInput{ + DataProtectionPolicy: aws.String(policy), + ResourceArn: aws.String(topicArn), + } + + _, err = conn.PutDataProtectionPolicyWithContext(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating SNS Data Protection Policy (%s): %s", d.Id(), err) + } + + if d.IsNewResource() { + d.SetId(topicArn) + } + + return resourceTopicDataProtectionPolicyRead(ctx, d, meta) +} + +func resourceTopicDataProtectionPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SNSConn() + + output, err := conn.GetDataProtectionPolicyWithContext(ctx, &sns.GetDataProtectionPolicyInput{ + ResourceArn: aws.String(d.Id()), + }) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, sns.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] SNS Data Protection Policy (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading SNS Data Protection Policy: %s", err) + } + + if output == nil || output.DataProtectionPolicy == nil { + return sdkdiag.AppendErrorf(diags, "reading SNS Data Protection Policy (%s): empty output", d.Id()) + } + + dataProtectionPolicy := output.DataProtectionPolicy + + d.Set("arn", d.Id()) + d.Set("policy", dataProtectionPolicy) + + return diags +} + +func resourceTopicDataProtectionPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).SNSConn() + + _, err := conn.PutDataProtectionPolicyWithContext(ctx, &sns.PutDataProtectionPolicyInput{ + DataProtectionPolicy: aws.String(""), + ResourceArn: aws.String(d.Get("arn").(string)), + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting SNS Data Protection Policy (%s): %s", d.Id(), err) + } + + return diags +} diff --git a/internal/service/sns/topic_data_protection_policy_test.go b/internal/service/sns/topic_data_protection_policy_test.go new file mode 100644 index 00000000000..e9cdf3e9489 --- /dev/null +++ b/internal/service/sns/topic_data_protection_policy_test.go @@ -0,0 +1,132 @@ +package sns_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/sns" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfsns "github.com/hashicorp/terraform-provider-aws/internal/service/sns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccSNSTopicDataProtectionPolicy_basic(t *testing.T) { + ctx := acctest.Context(t) + var attributes map[string]string + resourceName := "aws_sns_topic_data_protection_policy.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, sns.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTopicDataProtectionPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTopicDataProtectionPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTopicExists(ctx, "aws_sns_topic.test", &attributes), + resource.TestCheckResourceAttrPair(resourceName, "arn", "aws_sns_topic.test", "arn"), + resource.TestMatchResourceAttr(resourceName, "policy", regexp.MustCompile(fmt.Sprintf("\"Sid\":\"%[1]s\"", rName))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSNSTopicDataProtectionPolicy_disappears(t *testing.T) { + ctx := acctest.Context(t) + var attributes map[string]string + resourceName := "aws_sns_topic_data_protection_policy.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, sns.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTopicDataProtectionPolicyDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTopicDataProtectionPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTopicExists(ctx, "aws_sns_topic.test", &attributes), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfsns.ResourceTopicDataProtectionPolicy(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckTopicDataProtectionPolicyDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).SNSConn() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_sns_topic_data_protection_policy" { + continue + } + + _, err := tfsns.GetTopicAttributesByARN(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("SNS Data Protection Topic Policy %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccTopicDataProtectionPolicyConfig_basic(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_sns_topic" "test" { + name = %[1]q +} + +resource "aws_sns_topic_data_protection_policy" "test" { + arn = aws_sns_topic.test.arn + policy = jsonencode( + { + "Description" = "Default data protection policy" + "Name" = "__default_data_protection_policy" + "Statement" = [ + { + "DataDirection" = "Inbound" + "DataIdentifier" = [ + "arn:${data.aws_partition.current.partition}:dataprotection::aws:data-identifier/EmailAddress", + ] + "Operation" = { + "Deny" = {} + } + "Principal" = [ + "*", + ] + "Sid" = %[1]q + }, + ] + "Version" = "2021-06-01" + } + ) +} +`, rName) +} diff --git a/website/docs/r/sns_topic_data_protection_policy.html.markdown b/website/docs/r/sns_topic_data_protection_policy.html.markdown new file mode 100644 index 00000000000..d365b5364d7 --- /dev/null +++ b/website/docs/r/sns_topic_data_protection_policy.html.markdown @@ -0,0 +1,64 @@ +--- +subcategory: "SNS (Simple Notification)" +layout: "aws" +page_title: "AWS: aws_sns_topic_data_protection_policy" +description: |- + Provides an SNS data protection topic policy resource. +--- + +# Resource: aws_sns_topic_data_protection_policy + +Provides an SNS data protection topic policy resource + +## Example Usage + +```terraform +resource "aws_sns_topic" "example" { + name = "example" +} + +resource "aws_sns_topic_data_protection_policy" "example" { + arn = aws_sns_topic.example.arn + policy = jsonencode( + { + "Description" = "Example data protection policy" + "Name" = "__example_data_protection_policy" + "Statement" = [ + { + "DataDirection" = "Inbound" + "DataIdentifier" = [ + "arn:aws:dataprotection::aws:data-identifier/EmailAddress", + ] + "Operation" = { + "Deny" = {} + } + "Principal" = [ + "*", + ] + "Sid" = "__deny_statement_11ba9d96" + }, + ] + "Version" = "2021-06-01" + } + ) +} +``` + +## Argument Reference + +The following arguments are supported: + +* `arn` - (Required) The ARN of the SNS topic +* `policy` - (Required) The fully-formed AWS policy as JSON. For more information about building AWS IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://learn.hashicorp.com/terraform/aws/iam-policy). + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +## Import + +SNS Data Protection Topic Policy can be imported using the topic ARN, e.g., + +``` +$ terraform import aws_sns_topic_data_protection_policy.example arn:aws:sns:us-west-2:0123456789012:example +```