diff --git a/.changelog/00000.txt b/.changelog/00000.txt new file mode 100644 index 000000000000..12f283e0d98e --- /dev/null +++ b/.changelog/00000.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_wafv2_rule_group_permission_policy +``` diff --git a/internal/service/wafv2/exports_test.go b/internal/service/wafv2/exports_test.go index 6541b5d38805..1f5cbe9561d4 100644 --- a/internal/service/wafv2/exports_test.go +++ b/internal/service/wafv2/exports_test.go @@ -12,12 +12,14 @@ var ( ResourceWebACLAssociation = resourceWebACLAssociation ResourceWebACLLoggingConfiguration = resourceWebACLLoggingConfiguration ResourceAPIKey = newAPIKeyResource + ResourceRuleGroupPermissionPolicy = newRuleGroupPermissionPolicyResource ResourceWebACLRuleGroupAssociation = newResourceWebACLRuleGroupAssociation CloudFrontDistributionIDFromARN = cloudFrontDistributionIDFromARN FindAPIKeyByTwoPartKey = findAPIKeyByTwoPartKey FindIPSetByThreePartKey = findIPSetByThreePartKey FindLoggingConfigurationByARN = findLoggingConfigurationByARN + FindPermissionPolicyByARN = findPermissionPolicyByARN FindRegexPatternSetByThreePartKey = findRegexPatternSetByThreePartKey FindRuleGroupByThreePartKey = findRuleGroupByThreePartKey FindWebACLByResourceARN = findWebACLByResourceARN diff --git a/internal/service/wafv2/rule_group_permission_policy.go b/internal/service/wafv2/rule_group_permission_policy.go new file mode 100644 index 000000000000..8a225b387323 --- /dev/null +++ b/internal/service/wafv2/rule_group_permission_policy.go @@ -0,0 +1,214 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package wafv2 + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/wafv2" + awstypes "github.com/aws/aws-sdk-go-v2/service/wafv2/types" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_wafv2_rule_group_permission_policy", name="Rule Group Permission Policy") +// @ArnIdentity("resource_arn", identityDuplicateAttributes="id") +// @Testing(existsType="github.com/aws/aws-sdk-go-v2/service/wafv2;wafv2.GetPermissionPolicyOutput") +// @Testing(hasNoPreExistingResource=true) +// @Testing(importIgnore="policy") +func newRuleGroupPermissionPolicyResource(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &ruleGroupPermissionPolicyResource{} + + return r, nil +} + +type ruleGroupPermissionPolicyResource struct { + framework.ResourceWithModel[ruleGroupPermissionPolicyResourceModel] + framework.WithImportByIdentity +} + +func (r *ruleGroupPermissionPolicyResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrID: framework.IDAttribute(), + names.AttrPolicy: schema.StringAttribute{ + CustomType: fwtypes.IAMPolicyType, + Required: true, + }, + names.AttrResourceARN: schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *ruleGroupPermissionPolicyResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data ruleGroupPermissionPolicyResourceModel + + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().WAFV2Client(ctx) + + input := &wafv2.PutPermissionPolicyInput{ + Policy: flex.StringFromFramework(ctx, data.Policy), + ResourceArn: flex.StringFromFramework(ctx, data.ResourceARN), + } + + _, err := conn.PutPermissionPolicy(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("creating WAFv2 Rule Group Permission Policy (%s)", data.ResourceARN.ValueString()), err.Error()) + + return + } + + // Set values for unknowns. + data.setID() + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *ruleGroupPermissionPolicyResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data ruleGroupPermissionPolicyResourceModel + + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().WAFV2Client(ctx) + + output, err := findPermissionPolicyByARN(ctx, conn, data.ResourceARN.ValueString()) + + if retry.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading WAFv2 Rule Group Permission Policy (%s)", data.ID.ValueString()), err.Error()) + + return + } + + // GetPermissionPolicy only returns Policy, not ResourceArn. + data.Policy = fwtypes.IAMPolicyValue(aws.ToString(output.Policy)) + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *ruleGroupPermissionPolicyResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new ruleGroupPermissionPolicyResourceModel + + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().WAFV2Client(ctx) + + input := &wafv2.PutPermissionPolicyInput{ + Policy: flex.StringFromFramework(ctx, new.Policy), + ResourceArn: flex.StringFromFramework(ctx, new.ResourceARN), + } + + _, err := conn.PutPermissionPolicy(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("updating WAFv2 Rule Group Permission Policy (%s)", new.ID.ValueString()), err.Error()) + + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *ruleGroupPermissionPolicyResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data ruleGroupPermissionPolicyResourceModel + + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().WAFV2Client(ctx) + + input := wafv2.DeletePermissionPolicyInput{ + ResourceArn: flex.StringFromFramework(ctx, data.ResourceARN), + } + + _, err := conn.DeletePermissionPolicy(ctx, &input) + + if errs.IsA[*awstypes.WAFNonexistentItemException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting WAFv2 Rule Group Permission Policy (%s)", data.ID.ValueString()), err.Error()) + + return + } +} + +func findPermissionPolicyByARN(ctx context.Context, conn *wafv2.Client, resourceARN string) (*wafv2.GetPermissionPolicyOutput, error) { + input := &wafv2.GetPermissionPolicyInput{ + ResourceArn: aws.String(resourceARN), + } + + output, err := conn.GetPermissionPolicy(ctx, input) + + if errs.IsA[*awstypes.WAFNonexistentItemException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Policy == nil { + return nil, tfresource.NewEmptyResultError() + } + + return output, nil +} + +type ruleGroupPermissionPolicyResourceModel struct { + framework.WithRegionModel + ID types.String `tfsdk:"id"` + Policy fwtypes.IAMPolicy `tfsdk:"policy"` + ResourceARN fwtypes.ARN `tfsdk:"resource_arn"` +} + +func (data *ruleGroupPermissionPolicyResourceModel) setID() { + data.ID = data.ResourceARN.StringValue +} diff --git a/internal/service/wafv2/rule_group_permission_policy_test.go b/internal/service/wafv2/rule_group_permission_policy_test.go new file mode 100644 index 000000000000..67f23e180344 --- /dev/null +++ b/internal/service/wafv2/rule_group_permission_policy_test.go @@ -0,0 +1,226 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package wafv2_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfwafv2 "github.com/hashicorp/terraform-provider-aws/internal/service/wafv2" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccWAFV2RuleGroupPermissionPolicy_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_wafv2_rule_group_permission_policy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckAlternateAccount(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), + CheckDestroy: testAccCheckRuleGroupPermissionPolicyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccRuleGroupPermissionPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRuleGroupPermissionPolicyExists(ctx, t, resourceName), + resource.TestCheckResourceAttrSet(resourceName, names.AttrPolicy), + resource.TestCheckResourceAttrPair(resourceName, names.AttrResourceARN, "aws_wafv2_rule_group.test", names.AttrARN), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{names.AttrPolicy}, + }, + }, + }) +} + +func TestAccWAFV2RuleGroupPermissionPolicy_disappears(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_wafv2_rule_group_permission_policy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckAlternateAccount(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), + CheckDestroy: testAccCheckRuleGroupPermissionPolicyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccRuleGroupPermissionPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRuleGroupPermissionPolicyExists(ctx, t, resourceName), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfwafv2.ResourceRuleGroupPermissionPolicy, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccWAFV2RuleGroupPermissionPolicy_update(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_wafv2_rule_group_permission_policy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckAlternateAccount(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.WAFV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), + CheckDestroy: testAccCheckRuleGroupPermissionPolicyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccRuleGroupPermissionPolicyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRuleGroupPermissionPolicyExists(ctx, t, resourceName), + ), + }, + { + Config: testAccRuleGroupPermissionPolicyConfig_updated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckRuleGroupPermissionPolicyExists(ctx, t, resourceName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{names.AttrPolicy}, + }, + }, + }) +} + +func testAccCheckRuleGroupPermissionPolicyExists(ctx context.Context, t *testing.T, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.ProviderMeta(ctx, t).WAFV2Client(ctx) + + _, err := tfwafv2.FindPermissionPolicyByARN(ctx, conn, rs.Primary.ID) + + return err + } +} + +func testAccCheckRuleGroupPermissionPolicyDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).WAFV2Client(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_wafv2_rule_group_permission_policy" { + continue + } + + _, err := tfwafv2.FindPermissionPolicyByARN(ctx, conn, rs.Primary.ID) + + if retry.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("WAFv2 Rule Group Permission Policy %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccRuleGroupPermissionPolicyConfig_basic(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAlternateAccountProvider(), fmt.Sprintf(` +data "aws_caller_identity" "target" { + provider = "awsalternate" +} + +resource "aws_wafv2_rule_group" "test" { + name = %[1]q + scope = "REGIONAL" + capacity = 2 + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } +} + +resource "aws_wafv2_rule_group_permission_policy" "test" { + resource_arn = aws_wafv2_rule_group.test.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.target.account_id}:root" + } + Action = [ + "wafv2:CreateWebACL", + "wafv2:UpdateWebACL", + "wafv2:PutFirewallManagerRuleGroups", + "wafv2:GetRuleGroup", + ] + }] + }) +} + +data "aws_partition" "current" {} +`, rName)) +} + +func testAccRuleGroupPermissionPolicyConfig_updated(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAlternateAccountProvider(), fmt.Sprintf(` +data "aws_caller_identity" "target" { + provider = "awsalternate" +} + +resource "aws_wafv2_rule_group" "test" { + name = %[1]q + scope = "REGIONAL" + capacity = 2 + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = %[1]q + sampled_requests_enabled = false + } +} + +resource "aws_wafv2_rule_group_permission_policy" "test" { + resource_arn = aws_wafv2_rule_group.test.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + AWS = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.target.account_id}:root" + } + Action = [ + "wafv2:CreateWebACL", + "wafv2:UpdateWebACL", + "wafv2:PutFirewallManagerRuleGroups", + ] + }] + }) +} + +data "aws_partition" "current" {} +`, rName)) +} diff --git a/internal/service/wafv2/service_package_gen.go b/internal/service/wafv2/service_package_gen.go index 382f0ec2b4f6..7721d13eac25 100644 --- a/internal/service/wafv2/service_package_gen.go +++ b/internal/service/wafv2/service_package_gen.go @@ -39,6 +39,16 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.Ser Name: "API Key", Region: unique.Make(inttypes.ResourceRegionDefault()), }, + { + Factory: newRuleGroupPermissionPolicyResource, + TypeName: "aws_wafv2_rule_group_permission_policy", + Name: "Rule Group Permission Policy", + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentityNamed(names.AttrResourceARN, inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + }, + }, { Factory: newResourceWebACLRuleGroupAssociation, TypeName: "aws_wafv2_web_acl_rule_group_association", diff --git a/website/docs/r/wafv2_rule_group_permission_policy.html.markdown b/website/docs/r/wafv2_rule_group_permission_policy.html.markdown new file mode 100644 index 000000000000..d7207e8baf58 --- /dev/null +++ b/website/docs/r/wafv2_rule_group_permission_policy.html.markdown @@ -0,0 +1,168 @@ +--- +subcategory: "WAF" +layout: "aws" +page_title: "AWS: aws_wafv2_rule_group_permission_policy" +description: |- + Attaches a permission policy to a WAFv2 Rule Group to share it with other AWS accounts. +--- + +# Resource: aws_wafv2_rule_group_permission_policy + +Attaches a permission policy to a WAFv2 Rule Group, enabling cross-account sharing. +The policy allows specified AWS accounts to reference the rule group in their web ACLs. + +For more information, see [Sharing a rule group](https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-group-sharing.html) in the AWS WAF Developer Guide. + +## Example Usage + +### Share with a specific account + +```terraform +resource "aws_wafv2_rule_group" "example" { + name = "example" + scope = "REGIONAL" + capacity = 10 + + visibility_config { + cloudwatch_metrics_enabled = false + metric_name = "example" + sampled_requests_enabled = false + } +} + +resource "aws_wafv2_rule_group_permission_policy" "example" { + resource_arn = aws_wafv2_rule_group.example.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::111111111111:root" + } + Action = [ + "wafv2:CreateWebACL", + "wafv2:UpdateWebACL", + "wafv2:PutFirewallManagerRuleGroups", + "wafv2:GetRuleGroup", + ] + }] + }) +} +``` + +### Share with multiple accounts + +```terraform +resource "aws_wafv2_rule_group_permission_policy" "example" { + resource_arn = aws_wafv2_rule_group.example.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + AWS = [ + "arn:aws:iam::111111111111:root", + "arn:aws:iam::222222222222:root", + ] + } + Action = [ + "wafv2:CreateWebACL", + "wafv2:UpdateWebACL", + "wafv2:PutFirewallManagerRuleGroups", + "wafv2:GetRuleGroup", + ] + }] + }) +} +``` + +### Share with all accounts in an organization + +```terraform +resource "aws_wafv2_rule_group_permission_policy" "example" { + resource_arn = aws_wafv2_rule_group.example.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = "*" + Action = [ + "wafv2:CreateWebACL", + "wafv2:UpdateWebACL", + "wafv2:PutFirewallManagerRuleGroups", + "wafv2:GetRuleGroup", + ] + Condition = { + StringEquals = { + "aws:PrincipalOrgID" = "o-example1234" + } + } + }] + }) +} +``` + +### Share with a specific organizational unit (OU) + +```terraform +resource "aws_wafv2_rule_group_permission_policy" "example" { + resource_arn = aws_wafv2_rule_group.example.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = "*" + Action = [ + "wafv2:CreateWebACL", + "wafv2:UpdateWebACL", + "wafv2:PutFirewallManagerRuleGroups", + "wafv2:GetRuleGroup", + ] + Condition = { + "ForAnyValue:StringLike" = { + "aws:PrincipalOrgPaths" = "o-example1234/r-ab12/ou-ab12-example/*" + } + } + }] + }) +} +``` + +## Argument Reference + +The following arguments are required: + +* `policy` - (Required) The IAM policy to attach to the rule group. The policy must conform to the following: + * IAM Policy version must be `2012-10-17`. + * Must include specifications for `Effect`, `Action`, and `Principal`. + * `Effect` must be `Allow`. + * `Action` must include `wafv2:CreateWebACL`, `wafv2:UpdateWebACL`, and `wafv2:PutFirewallManagerRuleGroups`. May optionally include `wafv2:GetRuleGroup`. AWS WAF rejects any extra actions or wildcard actions. + * Must not include a `Resource` parameter. +* `resource_arn` - (Required, Forces new resource) The ARN of the WAFv2 Rule Group to attach the policy to. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - The ARN of the WAFv2 Rule Group. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import a WAFv2 Rule Group Permission Policy using the rule group ARN. For example: + +```terraform +import { + to = aws_wafv2_rule_group_permission_policy.example + id = "arn:aws:wafv2:us-east-1:123456789012:regional/rulegroup/example/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111" +} +``` + +Using `terraform import`, import a WAFv2 Rule Group Permission Policy using the rule group ARN. For example: + +```console +% terraform import aws_wafv2_rule_group_permission_policy.example arn:aws:wafv2:us-east-1:123456789012:regional/rulegroup/example/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111 +```