diff --git a/docs/data-sources/alb_waf_custom_rule_group.md b/docs/data-sources/alb_waf_custom_rule_group.md new file mode 100644 index 000000000..4adfc985d --- /dev/null +++ b/docs/data-sources/alb_waf_custom_rule_group.md @@ -0,0 +1,87 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_alb_waf_custom_rule_group Data Source - stackit" +subcategory: "" +description: |- + ALB WAF Custom Rule Group resource schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on resource level. + ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_alb_waf_custom_rule_group (Data Source) + +ALB WAF Custom Rule Group resource schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on resource level. + +~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +data "stackit_alb_waf_custom_rule_group" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-custom-rule-group" +} +``` + + +## Schema + +### Required + +- `name` (String) Custom rule group configuration name. +- `project_id` (String) STACKIT project ID associated with the ALB WAF Custom Rule Group. + +### Optional + +- `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`name`". +- `rules` (Attributes List) Enriched rules containing auto-generated IDs and computed severity values. (see [below for nested schema](#nestedatt--rules)) + + +### Nested Schema for `rules` + +Read-Only: + +- `behavior` (Attributes) (see [below for nested schema](#nestedatt--rules--behavior)) +- `conditions` (Attributes List) (see [below for nested schema](#nestedatt--rules--conditions)) +- `description` (String) A clear description explaining the threat vector or criteria addressed by this rule. +- `id` (Number) Backend auto-allocated unique rule ID within the valid 1-99999 threshold. + + +### Nested Schema for `rules.behavior` + +Read-Only: + +- `action` (String) The protective stance action. ACTION_DENY forces a 403 status response code. +- `log` (Boolean) Determines whether an entry should be generated in the security ledger upon a rule hit. +- `log_msg` (String) Custom notification message string mapped to underlying logdata contexts. Required if log is true. +- `severity` (String) Severity classification metric used by internal analytics graphs. + + + +### Nested Schema for `rules.conditions` + +Read-Only: + +- `operator` (Attributes) The comparison logic executed against the transformed variable. (see [below for nested schema](#nestedatt--rules--conditions--operator)) +- `transformations` (List of String) Ordered normalization steps applied before the operator runs. +- `variable` (Attributes) The part of the HTTP transaction to inspect. (see [below for nested schema](#nestedatt--rules--conditions--variable)) + + +### Nested Schema for `rules.conditions.operator` + +Read-Only: + +- `type` (String) The operational evaluation type definition macro. +- `value` (String) The text or rule regex pattern arguments applied inside the operator execution loop. + + + +### Nested Schema for `rules.conditions.variable` + +Read-Only: + +- `type` (String) The targeted validation engine variable macro. +- `value` (String) Optional key element context for map variables (e.g., matching a 'Host' header key). diff --git a/docs/data-sources/alb_waf_managed_rule_set.md b/docs/data-sources/alb_waf_managed_rule_set.md index 1f335dabf..c14a428bb 100644 --- a/docs/data-sources/alb_waf_managed_rule_set.md +++ b/docs/data-sources/alb_waf_managed_rule_set.md @@ -39,7 +39,6 @@ data "stackit_alb_waf_managed_rule_set" "example" { - `groups` (Attributes Map) Inventory of all available Managed Rule Set groups and their current configuration. (see [below for nested schema](#nestedatt--groups)) - `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`name`". - `type` (String) Type of the Managed Rule Set. -- `usage` (Attributes) Managed Rule Set usage (see [below for nested schema](#nestedatt--usage)) - `version` (String) Managed Rule Set version. @@ -59,13 +58,3 @@ Read-Only: - `description` (String) A description of what this rule does. - `mode` (String) The current mode of the rule. - `severity` (String) Impact level. - - - - -### Nested Schema for `usage` - -Read-Only: - -- `count` (Number) Number of WAFs using this Managed Rule Set. -- `items` (List of String) List of WAFs that use this Managed Rule Set. diff --git a/docs/resources/alb_waf_custom_rule_group.md b/docs/resources/alb_waf_custom_rule_group.md new file mode 100644 index 000000000..cd8d5b9df --- /dev/null +++ b/docs/resources/alb_waf_custom_rule_group.md @@ -0,0 +1,133 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_alb_waf_custom_rule_group Resource - stackit" +subcategory: "" +description: |- + ALB WAF Custom Rule Group resource schema. Uses the default_region specified in the provider configuration as a fallback in case no region is defined on resource level. + ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_alb_waf_custom_rule_group (Resource) + +ALB WAF Custom Rule Group resource schema. Uses the `default_region` specified in the provider configuration as a fallback in case no `region` is defined on resource level. + +~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +resource "stackit_alb_waf_custom_rule_group" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-custom-rule-group" + rules = [ + { + description = "My custom rule group" + behavior = { + action = "ACTION_DENY" + log = true + log_msg = "Some custom notification message string" + } + conditions = [ + { + operator = { + type = "OPERATOR_BEGINS_WITH" + value = "allowed objects" + } + transformations = [ + "TRANSFORMATION_LOWERCASE" + ] + variable = { + type = "VARIABLE_REQUEST_HEADERS" + value = "Host" + } + } + ] + } + ] +} +``` + + +## Schema + +### Required + +- `name` (String) Custom rule group configuration name. +- `project_id` (String) STACKIT project ID associated with the ALB WAF Custom Rule Group. +- `rules` (Attributes List) Enriched rules containing auto-generated IDs and computed severity values. (see [below for nested schema](#nestedatt--rules)) + +### Optional + +- `region` (String) STACKIT region name the resource is located in. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`name`". + + +### Nested Schema for `rules` + +Required: + +- `behavior` (Attributes) (see [below for nested schema](#nestedatt--rules--behavior)) +- `conditions` (Attributes List) (see [below for nested schema](#nestedatt--rules--conditions)) + +Optional: + +- `description` (String) A clear description explaining the threat vector or criteria addressed by this rule. + +Read-Only: + +- `id` (Number) Backend auto-allocated unique rule ID within the valid 1-99999 threshold. + + +### Nested Schema for `rules.behavior` + +Required: + +- `action` (String) The protective stance action. ACTION_DENY forces a 403 status response code. + +Optional: + +- `log` (Boolean) Determines whether an entry should be generated in the security ledger upon a rule hit. +- `log_msg` (String) Custom notification message string mapped to underlying logdata contexts. Required if log is true. + +Read-Only: + +- `severity` (String) Severity classification metric used by internal analytics graphs. + + + +### Nested Schema for `rules.conditions` + +Required: + +- `operator` (Attributes) The comparison logic executed against the transformed variable. (see [below for nested schema](#nestedatt--rules--conditions--operator)) +- `variable` (Attributes) The part of the HTTP transaction to inspect. (see [below for nested schema](#nestedatt--rules--conditions--variable)) + +Optional: + +- `transformations` (List of String) Ordered normalization steps applied before the operator runs. + + +### Nested Schema for `rules.conditions.operator` + +Required: + +- `type` (String) The operational evaluation type definition macro. + +Optional: + +- `value` (String) The text or rule regex pattern arguments applied inside the operator execution loop. + + + +### Nested Schema for `rules.conditions.variable` + +Required: + +- `type` (String) The targeted validation engine variable macro. + +Optional: + +- `value` (String) Optional key element context for map variables (e.g., matching a 'Host' header key). diff --git a/docs/resources/alb_waf_managed_rule_set.md b/docs/resources/alb_waf_managed_rule_set.md index eeb2c93f3..390e30ded 100644 --- a/docs/resources/alb_waf_managed_rule_set.md +++ b/docs/resources/alb_waf_managed_rule_set.md @@ -40,7 +40,6 @@ resource "stackit_alb_waf_managed_rule_set" "example" { - `groups` (Attributes Map) Inventory of all available Managed Rule Set groups and their current configuration. (see [below for nested schema](#nestedatt--groups)) - `id` (String) Terraform's internal resource identifier. Structured as "`project_id`,`region`,`name`". -- `usage` (Attributes) Managed Rule Set usage (see [below for nested schema](#nestedatt--usage)) - `version` (String) Managed Rule Set version. @@ -60,13 +59,3 @@ Read-Only: - `description` (String) A description of what this rule does. - `mode` (String) The current mode of the rule. - `severity` (String) Impact level. - - - - -### Nested Schema for `usage` - -Read-Only: - -- `count` (Number) Number of WAFs using this Managed Rule Set. -- `items` (List of String) List of WAFs that use this Managed Rule Set. diff --git a/examples/data-sources/stackit_alb_waf_custom_rule_group/data-source.tf b/examples/data-sources/stackit_alb_waf_custom_rule_group/data-source.tf new file mode 100644 index 000000000..1182aaac4 --- /dev/null +++ b/examples/data-sources/stackit_alb_waf_custom_rule_group/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_alb_waf_custom_rule_group" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-custom-rule-group" +} diff --git a/examples/resources/stackit_alb_waf_custom_rule_group/resource.tf b/examples/resources/stackit_alb_waf_custom_rule_group/resource.tf new file mode 100644 index 000000000..efe0077d8 --- /dev/null +++ b/examples/resources/stackit_alb_waf_custom_rule_group/resource.tf @@ -0,0 +1,29 @@ +resource "stackit_alb_waf_custom_rule_group" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-custom-rule-group" + rules = [ + { + description = "My custom rule group" + behavior = { + action = "ACTION_DENY" + log = true + log_msg = "Some custom notification message string" + } + conditions = [ + { + operator = { + type = "OPERATOR_BEGINS_WITH" + value = "allowed objects" + } + transformations = [ + "TRANSFORMATION_LOWERCASE" + ] + variable = { + type = "VARIABLE_REQUEST_HEADERS" + value = "Host" + } + } + ] + } + ] +} diff --git a/go.mod b/go.mod index 0645f5ff4..687c9530a 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-testing v1.16.0 github.com/stackitcloud/stackit-sdk-go/core v0.26.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.16.0 - github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.10.0 + github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.19.0 github.com/stackitcloud/stackit-sdk-go/services/certificates v1.9.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.21.0 diff --git a/go.sum b/go.sum index 5f4b12852..0c9fe380b 100644 --- a/go.sum +++ b/go.sum @@ -157,8 +157,8 @@ github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10 github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= github.com/stackitcloud/stackit-sdk-go/services/alb v0.16.0 h1:WoWlHdzISGXPEaJOYt6HP5F9M5nbyCJL6VqRJZIaOQs= github.com/stackitcloud/stackit-sdk-go/services/alb v0.16.0/go.mod h1:eK6oRB5Tmpt6KbXQ4UYBGg2LgW5bPtVoncL9E8JSRww= -github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.10.0 h1:0WsTSSZ0LjNpM3E1d3MgkBXmzMQThVQ7IuXhL2w4EyM= -github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.10.0/go.mod h1:4M9G1I64kZwlXO32ZoIpt0GAN4SpZ1SYerwCVVIBGoE= +github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.0 h1:uuQDV7Q3ndFJhlBbE2SX9ft+AtBJIck8rEcPr1lIqVE= +github.com/stackitcloud/stackit-sdk-go/services/albwaf v0.13.0/go.mod h1:4M9G1I64kZwlXO32ZoIpt0GAN4SpZ1SYerwCVVIBGoE= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2 h1:b7WJ/vwxlVmNNX91kI3obqGcuoPAyaCbDL5aCMQ/sNg= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.15.2/go.mod h1:T/JF25XGJ3GqER/1L2N//DgY8x5tY7gA3N+/0nvmOWY= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.19.0 h1:k+KJ4gp9awhJMY5y55vDqRSr6G/S9+8haTNILGbgH9s= diff --git a/stackit/internal/services/albwaf/albwaf_acc_test.go b/stackit/internal/services/albwaf/albwaf_acc_test.go index b8e539850..0c13bfe38 100644 --- a/stackit/internal/services/albwaf/albwaf_acc_test.go +++ b/stackit/internal/services/albwaf/albwaf_acc_test.go @@ -15,17 +15,67 @@ import ( "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stackitcloud/stackit-sdk-go/core/utils" - albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) var ( + //go:embed testdata/custom-rule-group-min.tf + customRuleGroupMinConfig string + + //go:embed testdata/custom-rule-group-max.tf + customRuleGroupMaxConfig string + //go:embed testdata/managed-rule-set.tf managedRuleSetConfig string ) +var testCustomRuleGroupMin = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "action": config.StringVariable("ACTION_DENY"), + "operator_type": config.StringVariable("OPERATOR_VALIDATE_UTF8_ENCODING"), + "variable_type": config.StringVariable("VARIABLE_RESPONSE_STATUS"), +} + +var testCustomRuleGroupMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testCustomRuleGroupMin) + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) + return updatedConfig +} + +var testCustomRuleGroupMax = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "description": config.StringVariable("foo bar"), + "action": config.StringVariable("ACTION_DENY"), + "log": config.BoolVariable(true), + "log_msg": config.StringVariable("foo-bar"), + "operator_type": config.StringVariable("OPERATOR_CONTAINS"), + "operator_value": config.StringVariable("foo"), + "transformation": config.StringVariable("TRANSFORMATION_LOWERCASE"), + "variable_type": config.StringVariable("VARIABLE_REQUEST_HEADERS"), + "variable_value": config.StringVariable("bar"), +} + +var testCustomRuleGroupMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testCustomRuleGroupMax) + // Name should not be updated, test if the update works in place + updatedConfig["description"] = config.StringVariable("new description") + updatedConfig["action"] = config.StringVariable("ACTION_ALLOW") + updatedConfig["log_msg"] = config.StringVariable("foo-bar:") + updatedConfig["operator_type"] = config.StringVariable("OPERATOR_BEGINS_WITH") + updatedConfig["operator_value"] = config.StringVariable("bar") + updatedConfig["transformation"] = config.StringVariable("TRANSFORMATION_UTF8_TO_UNICODE") + updatedConfig["variable_type"] = config.StringVariable("VARIABLE_ARGS_POST") + updatedConfig["variable_value"] = config.StringVariable("foo") + return updatedConfig +} + var testManagedRuleSet = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), @@ -39,6 +89,263 @@ var testManagedRuleSetUpdated = func() config.Variables { return updatedConfig } +func TestAccCustomRuleGroupMin(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testCustomRuleGroupMin, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), customRuleGroupMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "name", testutil.ConvertConfigVariable(testCustomRuleGroupMin["name"])), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.#", "1"), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.action", testutil.ConvertConfigVariable(testCustomRuleGroupMin["action"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log", "false"), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.type", testutil.ConvertConfigVariable(testCustomRuleGroupMin["operator_type"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.#", "0"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.type", testutil.ConvertConfigVariable(testCustomRuleGroupMin["variable_type"])), + ), + }, + // Data source + { + ConfigVariables: testCustomRuleGroupMin, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_alb_waf_custom_rule_group" "custom_rule_group" { + project_id = stackit_alb_waf_custom_rule_group.custom_rule_group.project_id + name = stackit_alb_waf_custom_rule_group.custom_rule_group.name + } + `, + testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), customRuleGroupMinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_custom_rule_group.custom_rule_group", "id", + "stackit_alb_waf_custom_rule_group.custom_rule_group", "id", + ), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "name", testutil.ConvertConfigVariable(testCustomRuleGroupMin["name"])), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.#", "1"), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id", + "stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id", + ), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.action", testutil.ConvertConfigVariable(testCustomRuleGroupMin["action"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log", "false"), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity", + "stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity", + ), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.#", "1"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.type", testutil.ConvertConfigVariable(testCustomRuleGroupMin["operator_type"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.#", "0"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.type", testutil.ConvertConfigVariable(testCustomRuleGroupMin["variable_type"])), + ), + }, + // Import + { + ConfigVariables: testCustomRuleGroupMin, + ResourceName: "stackit_alb_waf_custom_rule_group.custom_rule_group", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_alb_waf_custom_rule_group.custom_rule_group"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_alb_waf_custom_rule_group.custom_rule_group") + } + policyId, ok := r.Primary.Attributes["name"] + if !ok { + return "", fmt.Errorf("couldn't find attribute name") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, policyId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testCustomRuleGroupMinUpdated(), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), customRuleGroupMinConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_alb_waf_custom_rule_group.custom_rule_group", plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "name", testutil.ConvertConfigVariable(testCustomRuleGroupMinUpdated()["name"])), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.#", "1"), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.action", testutil.ConvertConfigVariable(testCustomRuleGroupMinUpdated()["action"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log", "false"), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.type", testutil.ConvertConfigVariable(testCustomRuleGroupMinUpdated()["operator_type"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.#", "0"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.type", testutil.ConvertConfigVariable(testCustomRuleGroupMinUpdated()["variable_type"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + +func TestAccCustomRuleGroupMax(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testCustomRuleGroupMax, + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), customRuleGroupMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "name", testutil.ConvertConfigVariable(testCustomRuleGroupMax["name"])), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.description", testutil.ConvertConfigVariable(testCustomRuleGroupMax["description"])), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.action", testutil.ConvertConfigVariable(testCustomRuleGroupMax["action"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log", testutil.ConvertConfigVariable(testCustomRuleGroupMax["log"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log_msg", testutil.ConvertConfigVariable(testCustomRuleGroupMax["log_msg"])), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.type", testutil.ConvertConfigVariable(testCustomRuleGroupMax["operator_type"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.value", testutil.ConvertConfigVariable(testCustomRuleGroupMax["operator_value"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.0", testutil.ConvertConfigVariable(testCustomRuleGroupMax["transformation"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.type", testutil.ConvertConfigVariable(testCustomRuleGroupMax["variable_type"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.value", testutil.ConvertConfigVariable(testCustomRuleGroupMax["variable_value"])), + ), + }, + // Data source + { + ConfigVariables: testCustomRuleGroupMax, + Config: fmt.Sprintf(` + %s + %s + + data "stackit_alb_waf_custom_rule_group" "custom_rule_group" { + project_id = stackit_alb_waf_custom_rule_group.custom_rule_group.project_id + name = stackit_alb_waf_custom_rule_group.custom_rule_group.name + } + `, + testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), customRuleGroupMaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "region", testutil.Region), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_custom_rule_group.custom_rule_group", "id", + "stackit_alb_waf_custom_rule_group.custom_rule_group", "id", + ), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "name", testutil.ConvertConfigVariable(testCustomRuleGroupMax["name"])), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.#", "1"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.description", testutil.ConvertConfigVariable(testCustomRuleGroupMax["description"])), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id", + "stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id", + ), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.action", testutil.ConvertConfigVariable(testCustomRuleGroupMax["action"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log", testutil.ConvertConfigVariable(testCustomRuleGroupMax["log"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log_msg", testutil.ConvertConfigVariable(testCustomRuleGroupMax["log_msg"])), + resource.TestCheckResourceAttrPair( + "data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity", + "stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity", + ), + + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.#", "1"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.type", testutil.ConvertConfigVariable(testCustomRuleGroupMax["operator_type"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.value", testutil.ConvertConfigVariable(testCustomRuleGroupMax["operator_value"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.#", "1"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.0", testutil.ConvertConfigVariable(testCustomRuleGroupMax["transformation"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.type", testutil.ConvertConfigVariable(testCustomRuleGroupMax["variable_type"])), + resource.TestCheckResourceAttr("data.stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.value", testutil.ConvertConfigVariable(testCustomRuleGroupMax["variable_value"])), + ), + }, + // Import + { + ConfigVariables: testCustomRuleGroupMax, + ResourceName: "stackit_alb_waf_custom_rule_group.custom_rule_group", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_alb_waf_custom_rule_group.custom_rule_group"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_alb_waf_custom_rule_group.custom_rule_group") + } + policyId, ok := r.Primary.Attributes["name"] + if !ok { + return "", fmt.Errorf("couldn't find attribute name") + } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, policyId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testCustomRuleGroupMaxUpdated(), + Config: fmt.Sprintf("%s\n%s", testutil.NewConfigBuilder().EnableBetaResources(true).BuildProviderConfig(), customRuleGroupMaxConfig), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("stackit_alb_waf_custom_rule_group.custom_rule_group", plancheck.ResourceActionUpdate), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "project_id", testutil.ProjectId), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "region", testutil.Region), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "id"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "name", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["name"])), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.description", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["description"])), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.id"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.action", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["action"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["log"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.log_msg", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["log_msg"])), + resource.TestCheckResourceAttrSet("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.behavior.severity"), + + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.type", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["operator_type"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.operator.value", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["operator_value"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.#", "1"), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.transformations.0", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["transformation"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.type", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["variable_type"])), + resource.TestCheckResourceAttr("stackit_alb_waf_custom_rule_group.custom_rule_group", "rules.0.conditions.0.variable.value", testutil.ConvertConfigVariable(testCustomRuleGroupMaxUpdated()["variable_value"])), + ), + }, + // Deletion is done by the framework implicitly + }, + }) +} + func TestAccManagedRuleSet(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, @@ -53,8 +360,7 @@ func TestAccManagedRuleSet(t *testing.T) { resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "region", testutil.Region), resource.TestCheckResourceAttrSet("stackit_alb_waf_managed_rule_set.managed_rule_set", "id"), resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testManagedRuleSet["name"])), - - resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "usage.count", "0"), + resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "type", testutil.ConvertConfigVariable(testManagedRuleSet["type"])), ), }, // Data source @@ -79,8 +385,7 @@ func TestAccManagedRuleSet(t *testing.T) { "stackit_alb_waf_managed_rule_set.managed_rule_set", "id", ), resource.TestCheckResourceAttr("data.stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testManagedRuleSet["name"])), - - resource.TestCheckResourceAttr("data.stackit_alb_waf_managed_rule_set.managed_rule_set", "usage.count", "0"), + resource.TestCheckResourceAttr("data.stackit_alb_waf_managed_rule_set.managed_rule_set", "type", testutil.ConvertConfigVariable(testManagedRuleSet["type"])), ), }, // Import @@ -115,8 +420,7 @@ func TestAccManagedRuleSet(t *testing.T) { resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "region", testutil.Region), resource.TestCheckResourceAttrSet("stackit_alb_waf_managed_rule_set.managed_rule_set", "id"), resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "name", testutil.ConvertConfigVariable(testManagedRuleSetUpdated()["name"])), - - resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "usage.count", "0"), + resource.TestCheckResourceAttr("stackit_alb_waf_managed_rule_set.managed_rule_set", "type", testutil.ConvertConfigVariable(testManagedRuleSetUpdated()["type"])), ), }, // Deletion is done by the framework implicitly @@ -124,8 +428,8 @@ func TestAccManagedRuleSet(t *testing.T) { }) } -func createClient() (*albwaf.APIClient, error) { - client, err := albwaf.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.AlbWafCustomEndpoint, false)...) +func createClient() (*albWaf.APIClient, error) { + client, err := albWaf.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.AlbWafCustomEndpoint, false)...) if err != nil { return nil, fmt.Errorf("creating client: %w", err) } @@ -135,6 +439,7 @@ func createClient() (*albwaf.APIClient, error) { func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ + testAlbWafCustomRuleGroupDestroy, testAlbWafManagedRuleSetDestroy, } var errs []error @@ -150,6 +455,39 @@ func testAccCheckDestroy(s *terraform.State) error { return errors.Join(errs...) } +func testAlbWafCustomRuleGroupDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := createClient() + if err != nil { + return err + } + + customRuleGroupsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_alb_waf_custom_rule_group" { + continue + } + // custom rule group transform id: "[projectId],[region],[name]" + name := strings.Split(rs.Primary.ID, core.Separator)[2] + customRuleGroupsToDestroy = append(customRuleGroupsToDestroy, name) + } + + resp, err := client.DefaultAPI.ListCustomRuleGroup(ctx, testutil.ProjectId, testutil.Region).Execute() + if err != nil { + return fmt.Errorf("getting resp: %w", err) + } + + for _, item := range resp.Items { + if utils.Contains(customRuleGroupsToDestroy, item.GetName()) { + _, err := client.DefaultAPI.DeleteCustomRuleGroup(ctx, testutil.ProjectId, testutil.Region, item.GetName()).Execute() + if err != nil { + return fmt.Errorf("deleting policy %s during CheckDestroy: %w", item.GetName(), err) + } + } + } + return nil +} + func testAlbWafManagedRuleSetDestroy(s *terraform.State) error { ctx := context.Background() client, err := createClient() diff --git a/stackit/internal/services/albwaf/custom_rule_group/bool.go b/stackit/internal/services/albwaf/custom_rule_group/bool.go new file mode 100644 index 000000000..1984c47bd --- /dev/null +++ b/stackit/internal/services/albwaf/custom_rule_group/bool.go @@ -0,0 +1,61 @@ +package custom_rule_group + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// TODO: will be moved to validators within STACKITTPR-786 + +// OnlyAllowedIfBoolEqualsValidator prevents that this string attribute is set if a target bool does not equal the specified value. +type OnlyAllowedIfBoolEqualsValidator struct { + Target path.Expression + Value bool +} + +// Ensure the validator implements the String validator interface +var _ validator.String = OnlyAllowedIfBoolEqualsValidator{} + +func (v OnlyAllowedIfBoolEqualsValidator) Description(_ context.Context) string { + return "The attribute can only be set if the boolean is set to the provided value." +} + +func (v OnlyAllowedIfBoolEqualsValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v OnlyAllowedIfBoolEqualsValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { // nolint:gocritic // function signature required by Terraform + expression := req.PathExpression.Merge(v.Target) + + matchedPaths, diags := req.Config.PathMatches(ctx, expression) + resp.Diagnostics.Append(diags...) + + for _, target := range matchedPaths { + var targetBool types.Bool + diags := req.Config.GetAttribute(ctx, target, &targetBool) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() || targetBool.IsUnknown() { + return + } + + if targetBool.ValueBool() != v.Value && !req.ConfigValue.IsNull() { + resp.Diagnostics.AddAttributeError( + req.Path, + "Attribute can not be set", + fmt.Sprintf("This attribute can only be configured when %q is set to %t.", target.String(), v.Value), + ) + } + } +} + +func OnlyAllowedIfBoolEquals(target path.Expression, value bool) validator.String { + return OnlyAllowedIfBoolEqualsValidator{ + Target: target, + Value: value, + } +} diff --git a/stackit/internal/services/albwaf/custom_rule_group/bool_test.go b/stackit/internal/services/albwaf/custom_rule_group/bool_test.go new file mode 100644 index 000000000..a9d2910c7 --- /dev/null +++ b/stackit/internal/services/albwaf/custom_rule_group/bool_test.go @@ -0,0 +1,117 @@ +package custom_rule_group + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestOnlyIfBoolValidator(t *testing.T) { + tests := []struct { + description string + target types.Bool + expectedValue bool + isValid bool + }{ + { + description: "target true, expect true", + target: types.BoolValue(true), + expectedValue: true, + isValid: true, + }, + { + description: "target false, expect true", + target: types.BoolValue(false), + expectedValue: true, + isValid: false, + }, + { + description: "target false, expect false", + target: types.BoolValue(false), + expectedValue: false, + isValid: true, + }, + { + description: "target true, expect false", + target: types.BoolValue(true), + expectedValue: false, + isValid: false, + }, + { + description: "target unknown, expect true", + target: types.BoolUnknown(), + expectedValue: true, + isValid: true, + }, + { + description: "target unknown, expect false", + target: types.BoolUnknown(), + expectedValue: false, + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + ctx := context.Background() + + boolVal, err := tt.target.ToTerraformValue(ctx) + if err != nil { + t.Fatalf("Failed to convert bool to tftypes.Value: %s", err) + } + + objType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "target_bool": tftypes.Bool, + }, + } + rawConfig := tftypes.NewValue(objType, map[string]tftypes.Value{ + "target_bool": boolVal, + }) + + req := validator.StringRequest{ + Path: path.Root("my_string"), + PathExpression: path.MatchRoot("my_string"), + ConfigValue: types.StringValue("example_string"), + Config: tfsdk.Config{ + Raw: rawConfig, + Schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "target_bool": schema.BoolAttribute{}, + }, + }, + }, + } + + resp := &validator.StringResponse{} + + OnlyAllowedIfBoolEquals(path.MatchRoot("target_bool"), tt.expectedValue).ValidateString(ctx, req, resp) + + if tt.isValid { + if resp.Diagnostics.HasError() { + t.Fatalf("did not expect validation error, got: %v", resp.Diagnostics) + } + } else { + hasExpectedError := false + + for _, diag := range resp.Diagnostics { + if diag.Summary() == "Attribute can not be set" { + hasExpectedError = true + } else { + t.Fatalf("expected validation error, got %q", diag.Summary()) + } + } + + if !hasExpectedError { + t.Fatalf("expected 'Attribute can not be set' error, got: %v", resp.Diagnostics) + } + } + }) + } +} diff --git a/stackit/internal/services/albwaf/custom_rule_group/datasource.go b/stackit/internal/services/albwaf/custom_rule_group/datasource.go new file mode 100644 index 000000000..51649be50 --- /dev/null +++ b/stackit/internal/services/albwaf/custom_rule_group/datasource.go @@ -0,0 +1,221 @@ +package custom_rule_group + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = &customRuleGroupDataSource{} + _ datasource.DataSourceWithConfigure = &customRuleGroupDataSource{} +) + +type customRuleGroupDataSource struct { + client *albWaf.APIClient + providerData core.ProviderData +} + +func NewCustomRuleGroupDataSource() datasource.DataSource { + return &customRuleGroupDataSource{} +} + +func (r *customRuleGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_alb_waf_custom_rule_group", core.Resource) + if resp.Diagnostics.HasError() { + return + } + + apiClient := utils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "ALB WAF client configured") +} + +func (r *customRuleGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_alb_waf_custom_rule_group" +} + +func (r *customRuleGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: features.AddBetaDescription(fmt.Sprintf("ALB WAF Custom Rule Group resource schema. %s", core.ResourceRegionFallbackDocstring), core.Resource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Optional: true, + Computed: true, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[0-9a-z](?:(?:[0-9a-z]|-){0,61}[0-9a-z])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + "rules": schema.ListNestedAttribute{ + Description: descriptions["rules"], + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "behavior": schema.SingleNestedAttribute{ + Description: descriptions["behavior"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "action": schema.StringAttribute{ + Description: descriptions["behavior_action"], + Computed: true, + }, + "log": schema.BoolAttribute{ + Description: descriptions["behavior_log"], + Computed: true, + }, + "log_msg": schema.StringAttribute{ + Description: descriptions["behavior_log_msg"], + Computed: true, + }, + "severity": schema.StringAttribute{ + Description: descriptions["behavior_severity"], + Computed: true, + }, + }, + }, + "conditions": schema.ListNestedAttribute{ + Description: descriptions["rule_conditions"], + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "operator": schema.SingleNestedAttribute{ + Description: descriptions["operator"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: descriptions["operator_type"], + Computed: true, + }, + "value": schema.StringAttribute{ + Description: descriptions["operator_value"], + Computed: true, + }, + }, + }, + "transformations": schema.ListAttribute{ + Description: descriptions["transformations"], + Computed: true, + ElementType: types.StringType, + }, + "variable": schema.SingleNestedAttribute{ + Description: descriptions["variable"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: descriptions["variable_type"], + Computed: true, + }, + "value": schema.StringAttribute{ + Description: descriptions["variable_value"], + Computed: true, + }, + }, + }, + }, + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["rule_description"], + Computed: true, + }, + "id": schema.Int32Attribute{ + Description: descriptions["rule_id"], + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (r *customRuleGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + name := model.Name.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + customRuleGroupResp, err := r.client.DefaultAPI.GetCustomRuleGroup(ctx, projectId, region, name).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("ALB WAF Custom Rule Group with name %q not found in project %q and region %q", name, projectId, region), err.Error()) + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Custom Rule Group", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, customRuleGroupResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Custom Rule Group", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Custom Rule Group read") +} diff --git a/stackit/internal/services/albwaf/custom_rule_group/resource.go b/stackit/internal/services/albwaf/custom_rule_group/resource.go new file mode 100644 index 000000000..540c0411d --- /dev/null +++ b/stackit/internal/services/albwaf/custom_rule_group/resource.go @@ -0,0 +1,846 @@ +package custom_rule_group + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" + + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/utils" + tfutils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ resource.Resource = &customRuleGroupResource{} + _ resource.ResourceWithConfigure = &customRuleGroupResource{} + _ resource.ResourceWithImportState = &customRuleGroupResource{} + _ resource.ResourceWithModifyPlan = &customRuleGroupResource{} + + variableTypeOptions = sdkUtils.EnumSliceToStringSlice(albWaf.AllowedVariableEnumValues) + transformationOptions = sdkUtils.EnumSliceToStringSlice(albWaf.AllowedTransformationEnumValues) + operatorTypeOptions = sdkUtils.EnumSliceToStringSlice(albWaf.AllowedOperatorEnumValues) + actionOptions = sdkUtils.EnumSliceToStringSlice(albWaf.AllowedActionEnumValues) +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Name types.String `tfsdk:"name"` + Rules types.List `tfsdk:"rules"` +} + +type RuleModel struct { + Behavior types.Object `tfsdk:"behavior"` + Conditions types.List `tfsdk:"conditions"` + Description types.String `tfsdk:"description"` + Id types.Int32 `tfsdk:"id"` +} + +var ruleType = map[string]attr.Type{ + "behavior": types.ObjectType{AttrTypes: behaviorType}, + "conditions": types.ListType{ + ElemType: types.ObjectType{AttrTypes: conditionType}, + }, + "description": types.StringType, + "id": types.Int32Type, +} + +type BehaviorModel struct { + Action types.String `tfsdk:"action"` + Log types.Bool `tfsdk:"log"` + LogMsg types.String `tfsdk:"log_msg"` + Severity types.String `tfsdk:"severity"` +} + +var behaviorType = map[string]attr.Type{ + "action": types.StringType, + "log": types.BoolType, + "log_msg": types.StringType, + "severity": types.StringType, +} + +type ConditionModel struct { + Operator types.Object `tfsdk:"operator"` + Transformations types.List `tfsdk:"transformations"` + Variable types.Object `tfsdk:"variable"` +} + +var conditionType = map[string]attr.Type{ + "operator": types.ObjectType{AttrTypes: operatorType}, + "transformations": types.ListType{ElemType: types.StringType}, + "variable": types.ObjectType{AttrTypes: variableType}, +} + +type OperatorModel struct { + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` +} + +var operatorType = map[string]attr.Type{ + "type": types.StringType, + "value": types.StringType, +} + +type VariableModel struct { + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` +} + +var variableType = map[string]attr.Type{ + "type": types.StringType, + "value": types.StringType, +} + +type customRuleGroupResource struct { + client *albWaf.APIClient + providerData core.ProviderData +} + +func NewCustomRuleGroupResource() resource.Resource { + return &customRuleGroupResource{} +} + +func (r *customRuleGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_alb_waf_custom_rule_group", core.Resource) + if resp.Diagnostics.HasError() { + return + } + + apiClient := utils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "ALB WAF client configured") +} + +func (r *customRuleGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_alb_waf_custom_rule_group" +} + +// descriptions for the attributes in the Schema. +var descriptions = map[string]string{ + "id": "Terraform's internal resource identifier. Structured as \"`project_id`,`region`,`name`\".", + "project_id": "STACKIT project ID associated with the ALB WAF Custom Rule Group.", + "region": "STACKIT region name the resource is located in. If not defined, the provider region is used.", + "name": "Custom rule group configuration name.", + "rules": "Enriched rules containing auto-generated IDs and computed severity values.", + "rule_behavior": "Behavior of the rule.", + "rule_condition": "Conditions for this rule (order matters, first condition match triggers execution).", + "rule_description": "A clear description explaining the threat vector or criteria addressed by this rule.", + "rule_id": "Backend auto-allocated unique rule ID within the valid 1-99999 threshold.", + "behavior_action": "The protective stance action. ACTION_DENY forces a 403 status response code.", + "behavior_log": "Determines whether an entry should be generated in the security ledger upon a rule hit.", + "behavior_log_msg": "Custom notification message string mapped to underlying logdata contexts. Required if log is true.", + "behavior_severity": "Severity classification metric used by internal analytics graphs.", + "operator": "The comparison logic executed against the transformed variable.", + "operator_type": "The operational evaluation type definition macro.", + "operator_value": "The text or rule regex pattern arguments applied inside the operator execution loop.", + "transformations": "Ordered normalization steps applied before the operator runs.", + "variable": "The part of the HTTP transaction to inspect.", + "variable_type": "The targeted validation engine variable macro.", + "variable_value": "Optional key element context for map variables (e.g., matching a 'Host' header key).", +} + +func (r *customRuleGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: features.AddBetaDescription(fmt.Sprintf("ALB WAF Custom Rule Group resource schema. %s", core.ResourceRegionFallbackDocstring), core.Resource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: descriptions["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: descriptions["project_id"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: descriptions["region"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "name": schema.StringAttribute{ + Description: descriptions["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[0-9a-z](?:(?:[0-9a-z]|-){0,61}[0-9a-z])?$`), + "must start and end with an alphanumeric character, may contain hyphens, and be 1-63 characters long", + ), + }, + }, + "rules": schema.ListNestedAttribute{ + Description: descriptions["rules"], + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "behavior": schema.SingleNestedAttribute{ + Description: descriptions["behavior"], + Required: true, + Attributes: map[string]schema.Attribute{ + "action": schema.StringAttribute{ + Description: descriptions["behavior_action"], + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(actionOptions...), + }, + }, + "log": schema.BoolAttribute{ + Description: descriptions["behavior_log"], + Optional: true, + Computed: true, + }, + "log_msg": schema.StringAttribute{ + Description: descriptions["behavior_log_msg"], + Optional: true, + Computed: true, + Validators: []validator.String{ + OnlyAllowedIfBoolEquals(path.MatchRelative().AtParent().AtName("log"), true), + }, + }, + "severity": schema.StringAttribute{ + Description: descriptions["behavior_severity"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + "conditions": schema.ListNestedAttribute{ + Description: descriptions["rule_conditions"], + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "operator": schema.SingleNestedAttribute{ + Description: descriptions["operator"], + Required: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: descriptions["operator_type"], + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(operatorTypeOptions...), + }, + }, + "value": schema.StringAttribute{ + Description: descriptions["operator_value"], + Optional: true, + }, + }, + }, + "transformations": schema.ListAttribute{ + Description: descriptions["transformations"], + Optional: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre( + stringvalidator.OneOf(transformationOptions...), + ), + }, + Computed: true, + }, + "variable": schema.SingleNestedAttribute{ + Description: descriptions["variable"], + Required: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: descriptions["variable_type"], + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(variableTypeOptions...), + }, + }, + "value": schema.StringAttribute{ + Description: descriptions["variable_value"], + Optional: true, + }, + }, + }, + }, + }, + }, + "description": schema.StringAttribute{ + Description: descriptions["rule_description"], + Optional: true, + }, + "id": schema.Int32Attribute{ + Description: descriptions["rule_id"], + Computed: true, + PlanModifiers: []planmodifier.Int32{ + int32planmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + } +} + +func (r *customRuleGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform + var configModel Model + if req.Config.Raw.IsNull() { + return + } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { + return + } + + var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { + return + } + + tfutils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *customRuleGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing ALB WAF Custom Rule Group", + fmt.Sprintf("Expected import identifier with format: [project_id],[region],[name] Got: %q", req.ID), + ) + return + } + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": idParts[0], + "region": idParts[1], + "name": idParts[2], + }) + tflog.Info(ctx, "ALB WAF Custom Rule Group state imported") +} + +func (r *customRuleGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", model.Name) + + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Custom Rule Group", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + createResp, err := r.client.DefaultAPI.CreateCustomRuleGroup(ctx, projectId, region).CreateCustomRuleGroupPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Custom Rule Group", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + "name": createResp.Name, + }) + if resp.Diagnostics.HasError() { + return + } + + err = mapFields(ctx, createResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Custom Rule Group", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Custom Rule Group created") +} + +func (r *customRuleGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + customRuleGroupName := model.Name.ValueString() + region := model.Region.ValueString() + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "name", customRuleGroupName) + ctx = tflog.SetField(ctx, "region", region) + + payload, err := toUpdatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Custom Rule Group", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + updateResp, err := r.client.DefaultAPI.UpdateCustomRuleGroup(ctx, projectId, region, customRuleGroupName).UpdateCustomRuleGroupPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Custom Rule Group", fmt.Sprintf("Calling API update endpoint: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, updateResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating ALB WAF Custom Rule Group", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "ALB WAF Custom Rule Group update") +} + +func (r *customRuleGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + name := model.Name.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + customRuleGroupResp, err := r.client.DefaultAPI.GetCustomRuleGroup(ctx, projectId, region, name).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Custom Rule Group", err.Error()) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(ctx, customRuleGroupResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading ALB WAF Custom Rule Group", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "ALB WAF Custom Rule Group read") +} + +func (r *customRuleGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + projectId := model.ProjectId.ValueString() + name := model.Name.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "name", name) + + _, err := r.client.DefaultAPI.DeleteCustomRuleGroup(ctx, projectId, region, name).Execute() + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) && oapiErr.StatusCode == http.StatusNotFound { + tflog.Info(ctx, "ALB WAF Custom Rule Group was already deleted") + return + } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting ALB WAF Custom Rule Group", fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + tflog.Info(ctx, "ALB WAF Custom Rule Group deleted") +} + +func toCreatePayload(ctx context.Context, model *Model) (*albWaf.CreateCustomRuleGroupPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + payloadRules, err := toRulesPayload(ctx, model.Rules) + if err != nil { + return nil, fmt.Errorf("generating rules payload: %w", err) + } else if payloadRules == nil { + return nil, fmt.Errorf("rules can not be empty") + } + + payload := &albWaf.CreateCustomRuleGroupPayload{ + Name: model.Name.ValueString(), + Rules: *payloadRules, + } + + return payload, nil +} + +func toUpdatePayload(ctx context.Context, model *Model) (*albWaf.UpdateCustomRuleGroupPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + payloadRules, err := toRulesPayload(ctx, model.Rules) + if err != nil { + return nil, fmt.Errorf("generating rules payload: %w", err) + } else if payloadRules == nil { + return nil, fmt.Errorf("rules can not be empty") + } + + payload := &albWaf.UpdateCustomRuleGroupPayload{ + Name: model.Name.ValueString(), + Rules: *payloadRules, + } + + return payload, nil +} + +func toRulesPayload(ctx context.Context, modelRules basetypes.ListValue) (*[]albWaf.CreateCustomRule, error) { + payloadRules := []albWaf.CreateCustomRule{} + if !tfutils.IsUndefined(modelRules) { + rules := []RuleModel{} + diags := modelRules.ElementsAs(ctx, &rules, true) + if diags.HasError() { + return nil, fmt.Errorf("converting to rule map: %w", core.DiagsToError(diags)) + } + + for _, rule := range rules { + behavior := BehaviorModel{} + if !tfutils.IsUndefined(rule.Behavior) { + diags := rule.Behavior.As(ctx, &behavior, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting to rule behavior: %w", core.DiagsToError(diags)) + } + } + + conditions, err := toConditionsPayload(ctx, rule.Conditions) + if err != nil { + return nil, fmt.Errorf("converting conditions: %w", err) + } else if conditions == nil { + return nil, fmt.Errorf("conditions can not be empty") + } + + var log *bool + if !tfutils.IsUndefined(behavior.Log) { + log = behavior.Log.ValueBoolPointer() + } + + var logMsg *string + if !tfutils.IsUndefined(behavior.LogMsg) { + logMsg = behavior.LogMsg.ValueStringPointer() + } + + var description *string + if !tfutils.IsUndefined(rule.Description) { + description = rule.Description.ValueStringPointer() + } + + payloadRules = append(payloadRules, albWaf.CreateCustomRule{ + Behavior: albWaf.Behavior{ + Action: albWaf.Action(behavior.Action.ValueString()), + Log: log, + LogMsg: logMsg, + }, + Conditions: *conditions, + Description: description, + }) + } + } + + return &payloadRules, nil +} + +func toConditionsPayload(ctx context.Context, conditions basetypes.ListValue) (*[]albWaf.Condition, error) { + result := []albWaf.Condition{} + + if !tfutils.IsUndefined(conditions) { + conditionModels := []ConditionModel{} + diags := conditions.ElementsAs(ctx, &conditionModels, true) + if diags.HasError() { + return nil, fmt.Errorf("converting to rule map: %w", core.DiagsToError(diags)) + } + + for _, condition := range conditionModels { + transformations := []albWaf.Transformation{} + if !tfutils.IsUndefined(condition.Transformations) { + diags := condition.Transformations.ElementsAs(ctx, &transformations, true) + if diags.HasError() { + return nil, fmt.Errorf("converting transformations: %w", core.DiagsToError(diags)) + } + } + + var operatorModel = OperatorModel{} + diags = condition.Operator.As(ctx, &operatorModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting operator: %w", core.DiagsToError(diags)) + } + + var variableModel = VariableModel{} + diags = condition.Variable.As(ctx, &variableModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting variable: %w", core.DiagsToError(diags)) + } + + result = append(result, albWaf.Condition{ + Operator: albWaf.ConditionOperator{ + Type: albWaf.Operator(operatorModel.Type.ValueString()), + Value: operatorModel.Value.ValueStringPointer(), + }, + Transformations: transformations, + Variable: albWaf.ConditionVariable{ + Type: albWaf.Variable(variableModel.Type.ValueString()), + Value: variableModel.Value.ValueStringPointer(), + }, + }) + } + } + + return &result, nil +} + +func mapFields(ctx context.Context, customRuleGroup *albWaf.GetCustomRuleGroupResponse, model *Model, region string) error { + if customRuleGroup == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + model.Id = tfutils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.Name.ValueString()) + model.Name = types.StringValue(customRuleGroup.GetName()) + model.Region = types.StringValue(region) + + rules, err := mapRules(ctx, &customRuleGroup.Rules) + if err != nil { + return fmt.Errorf("map rules: %w", err) + } else if rules == nil { + return fmt.Errorf("rules can not be empty") + } + model.Rules = *rules + + return nil +} + +func mapRules(ctx context.Context, rules *[]albWaf.GetCustomRule) (*basetypes.ListValue, error) { + var diags diag.Diagnostics + var result basetypes.ListValue + + if rules != nil { + rulesList := []attr.Value{} + for _, rule := range *rules { + ruleTF := RuleModel{ + Id: types.Int32Value(rule.Id), + Description: types.StringPointerValue(rule.Description), + } + + behavior, err := mapBehavior(ctx, rule.Behavior) + if err != nil { + return nil, fmt.Errorf("map behavior: %w", err) + } else if behavior == nil { + return nil, fmt.Errorf("behavior can not be empty") + } + ruleTF.Behavior = *behavior + + conditions, err := mapConditions(ctx, &rule) + if err != nil { + return nil, fmt.Errorf("map conditions: %w", err) + } else if conditions == nil { + return nil, fmt.Errorf("conditions can not be empty") + } + ruleTF.Conditions = *conditions + + rule, diags := types.ObjectValueFrom(ctx, ruleType, ruleTF) + if diags.HasError() { + return nil, fmt.Errorf("mapping rule: %w", core.DiagsToError(diags)) + } + rulesList = append(rulesList, rule) + } + result, diags = types.ListValue(types.ObjectType{AttrTypes: ruleType}, rulesList) + if diags.HasError() { + return nil, fmt.Errorf("creating rule object: %w", core.DiagsToError(diags)) + } + } else { + result = types.ListNull(types.ObjectType{AttrTypes: ruleType}) + } + + return &result, nil +} + +func mapBehavior(ctx context.Context, behavior albWaf.GetBehavior) (*basetypes.ObjectValue, error) { + var diags diag.Diagnostics + var result basetypes.ObjectValue + + behaviorModel := BehaviorModel{ + Action: types.StringValue(string(behavior.Action)), + Log: types.BoolValue(behavior.Log), + LogMsg: types.StringPointerValue(behavior.LogMsg), + Severity: types.StringValue(string(behavior.Severity)), + } + + result, diags = types.ObjectValueFrom(ctx, behaviorType, behaviorModel) + if diags.HasError() { + return nil, fmt.Errorf("creating behavior object: %w", core.DiagsToError(diags)) + } + + return &result, nil +} + +func mapConditions(ctx context.Context, rule *albWaf.GetCustomRule) (*basetypes.ListValue, error) { + var diags diag.Diagnostics + var result basetypes.ListValue + + if rule != nil { + if conditions, ok := rule.GetConditionsOk(); ok { + conditionsList := []attr.Value{} + for _, condition := range conditions { + conditionTF := ConditionModel{} + + if operator, ok := condition.GetOperatorOk(); ok { + operatorModel := OperatorModel{ + Type: types.StringValue(string(operator.Type)), + Value: types.StringPointerValue(operator.Value), + } + + conditionTF.Operator, diags = types.ObjectValueFrom(ctx, operatorType, operatorModel) + if diags.HasError() { + return nil, fmt.Errorf("creating operator object: %w", core.DiagsToError(diags)) + } + } else { + conditionTF.Operator = types.ObjectNull(operatorType) + } + + conditionTF.Transformations, diags = types.ListValueFrom(ctx, types.StringType, condition.Transformations) + if diags.HasError() { + return nil, fmt.Errorf("mapping transformations: %w", core.DiagsToError(diags)) + } + + if variable, ok := condition.GetVariableOk(); ok { + variableModel := VariableModel{ + Type: types.StringValue(string(variable.Type)), + Value: types.StringPointerValue(variable.Value), + } + + conditionTF.Variable, diags = types.ObjectValueFrom(ctx, variableType, variableModel) + if diags.HasError() { + return nil, fmt.Errorf("creating variable object: %w", core.DiagsToError(diags)) + } + } else { + conditionTF.Variable = types.ObjectNull(variableType) + } + + condition, diags := types.ObjectValueFrom(ctx, conditionType, conditionTF) + if diags.HasError() { + return nil, fmt.Errorf("mapping condition: %w", core.DiagsToError(diags)) + } + conditionsList = append(conditionsList, condition) + } + result, diags = types.ListValue(types.ObjectType{AttrTypes: conditionType}, conditionsList) + if diags.HasError() { + return nil, fmt.Errorf("mapping conditions: %w", core.DiagsToError(diags)) + } + } else { + result = types.ListNull(types.ObjectType{AttrTypes: conditionType}) + } + } else { + result = types.ListNull(types.ObjectType{AttrTypes: conditionType}) + } + + return &result, nil +} diff --git a/stackit/internal/services/albwaf/custom_rule_group/resource_test.go b/stackit/internal/services/albwaf/custom_rule_group/resource_test.go new file mode 100644 index 000000000..d1b353049 --- /dev/null +++ b/stackit/internal/services/albwaf/custom_rule_group/resource_test.go @@ -0,0 +1,336 @@ +package custom_rule_group + +import ( + "context" + _ "embed" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +var ( + testProjectId = types.StringValue(uuid.NewString()) + testRegion = types.StringValue("eu01") + testName = types.StringValue("test-custom-rule-group") + testId = types.StringValue(testProjectId.ValueString() + "," + testRegion.ValueString() + "," + testName.ValueString()) +) + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + name string + model *Model + expected *albWaf.CreateCustomRuleGroupPayload + isValid bool + }{ + { + name: "default", + model: &Model{ + Name: testName, + Id: testId, + ProjectId: testProjectId, + Region: testRegion, + Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleType}, []attr.Value{ + types.ObjectValueMust(ruleType, map[string]attr.Value{ + "behavior": types.ObjectValueMust(behaviorType, map[string]attr.Value{ + "action": types.StringValue("some-action"), + "log": types.BoolValue(true), + "log_msg": types.StringValue("Log: something happened"), + "severity": types.StringNull(), + }), + "conditions": types.ListValueMust(types.ObjectType{AttrTypes: conditionType}, []attr.Value{ + types.ObjectValueMust(conditionType, map[string]attr.Value{ + "operator": types.ObjectValueMust(operatorType, map[string]attr.Value{ + "type": types.StringValue("operator-type"), + "value": types.StringValue("operator-value"), + }), + "transformations": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("foo"), + types.StringValue("bar"), + }), + "variable": types.ObjectValueMust(variableType, map[string]attr.Value{ + "type": types.StringValue("variable-type"), + "value": types.StringValue("variable-value"), + }), + }), + }), + "description": types.StringValue("foo-bar"), + "id": types.Int32Null(), + }), + }), + }, + expected: &albWaf.CreateCustomRuleGroupPayload{ + Name: testName.ValueString(), + Rules: []albWaf.CreateCustomRule{ + { + Behavior: albWaf.Behavior{ + Action: albWaf.Action("some-action"), + Log: new(true), + LogMsg: new("Log: something happened"), + }, + Conditions: []albWaf.Condition{ + { + Operator: albWaf.ConditionOperator{ + Type: albWaf.Operator("operator-type"), + Value: new("operator-value"), + }, + Transformations: []albWaf.Transformation{ + "foo", + "bar", + }, + Variable: albWaf.ConditionVariable{ + Type: albWaf.Variable("variable-type"), + Value: new("variable-value"), + }, + }, + }, + Description: new("foo-bar"), + }, + }, + }, + isValid: true, + }, + { + name: "null values", + model: &Model{ + Name: testName, + Id: testId, + ProjectId: testProjectId, + Region: testRegion, + Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleType}, []attr.Value{ + types.ObjectValueMust(ruleType, map[string]attr.Value{ + "behavior": types.ObjectValueMust(behaviorType, map[string]attr.Value{ + "action": types.StringNull(), + "log": types.BoolNull(), + "log_msg": types.StringNull(), + "severity": types.StringNull(), + }), + "conditions": types.ListValueMust(types.ObjectType{AttrTypes: conditionType}, []attr.Value{ + types.ObjectValueMust(conditionType, map[string]attr.Value{ + "operator": types.ObjectValueMust(operatorType, map[string]attr.Value{ + "type": types.StringNull(), + "value": types.StringNull(), + }), + "transformations": types.ListValueMust(types.StringType, []attr.Value{}), + "variable": types.ObjectValueMust(variableType, map[string]attr.Value{ + "type": types.StringNull(), + "value": types.StringNull(), + }), + }), + }), + "description": types.StringNull(), + "id": types.Int32Null(), + }), + }), + }, + expected: &albWaf.CreateCustomRuleGroupPayload{ + Name: testName.ValueString(), + Rules: []albWaf.CreateCustomRule{ + { + Behavior: albWaf.Behavior{}, + Conditions: []albWaf.Condition{ + { + Operator: albWaf.ConditionOperator{}, + Transformations: []albWaf.Transformation{}, + Variable: albWaf.ConditionVariable{}, + }, + }, + }, + }, + }, + isValid: true, + }, + { + name: "no rules", + model: &Model{ + Name: testName, + Id: testId, + ProjectId: testProjectId, + Region: testRegion, + }, + expected: &albWaf.CreateCustomRuleGroupPayload{ + Name: testName.ValueString(), + Rules: []albWaf.CreateCustomRule{}, + }, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toCreatePayload(context.Background(), tt.model) + if (err != nil) == tt.isValid { + t.Errorf("toCreatePayload() error = %v, isValid %v", err, tt.isValid) + return + } + + if tt.isValid { + if diff := cmp.Diff(got, tt.expected); diff != "" { + t.Errorf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestMapFields(t *testing.T) { + tests := []struct { + name string + state *Model + region string + input *albWaf.GetCustomRuleGroupResponse + expected *Model + isValid bool + }{ + { + name: "default", + state: &Model{ + ProjectId: testProjectId, + Region: testRegion, + Name: testName, + Id: testId, + Rules: types.ListNull(types.ObjectType{AttrTypes: ruleType}), + }, + region: testRegion.ValueString(), + input: &albWaf.GetCustomRuleGroupResponse{ + Name: testName.ValueString(), + Rules: []albWaf.GetCustomRule{ + { + Behavior: albWaf.GetBehavior{ + Action: albWaf.Action("some-action"), + Log: true, + LogMsg: new("Log: something happened"), + Severity: albWaf.Severity("critical"), + }, + Conditions: []albWaf.Condition{ + { + Operator: albWaf.ConditionOperator{ + Type: albWaf.Operator("operator-type"), + Value: new("operator-value"), + }, + Transformations: []albWaf.Transformation{ + "foo", + "bar", + }, + Variable: albWaf.ConditionVariable{ + Type: albWaf.Variable("variable-type"), + Value: new("variable-value"), + }, + }, + }, + Description: new("foo-bar"), + Id: int32(42), + }, + }, + }, + expected: &Model{ + ProjectId: testProjectId, + Region: testRegion, + Name: testName, + Id: testId, + Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleType}, []attr.Value{ + types.ObjectValueMust(ruleType, map[string]attr.Value{ + "behavior": types.ObjectValueMust(behaviorType, map[string]attr.Value{ + "action": types.StringValue("some-action"), + "log": types.BoolValue(true), + "log_msg": types.StringValue("Log: something happened"), + "severity": types.StringValue("critical"), + }), + "conditions": types.ListValueMust(types.ObjectType{AttrTypes: conditionType}, []attr.Value{ + types.ObjectValueMust(conditionType, map[string]attr.Value{ + "operator": types.ObjectValueMust(operatorType, map[string]attr.Value{ + "type": types.StringValue("operator-type"), + "value": types.StringValue("operator-value"), + }), + "transformations": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("foo"), + types.StringValue("bar"), + }), + "variable": types.ObjectValueMust(variableType, map[string]attr.Value{ + "type": types.StringValue("variable-type"), + "value": types.StringValue("variable-value"), + }), + }), + }), + "description": types.StringValue("foo-bar"), + "id": types.Int32Value(42), + }), + }), + }, + isValid: true, + }, + { + name: "empty rule", + state: &Model{ + ProjectId: testProjectId, + Region: testRegion, + Name: testName, + Id: testId, + Rules: types.ListNull(types.ObjectType{AttrTypes: ruleType}), + }, + region: testRegion.ValueString(), + input: &albWaf.GetCustomRuleGroupResponse{ + Name: testName.ValueString(), + Rules: []albWaf.GetCustomRule{ + {}, + }, + }, + expected: &Model{ + Name: testName, + Id: testId, + ProjectId: testProjectId, + Region: testRegion, + Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleType}, []attr.Value{ + types.ObjectValueMust(ruleType, map[string]attr.Value{ + "behavior": types.ObjectValueMust(behaviorType, map[string]attr.Value{ + "action": types.StringValue(""), + "log": types.BoolValue(false), + "log_msg": types.StringNull(), + "severity": types.StringValue(""), + }), + "conditions": types.ListValueMust(types.ObjectType{AttrTypes: conditionType}, []attr.Value{}), + "description": types.StringNull(), + "id": types.Int32Value(0), + }), + }), + }, + isValid: true, + }, + { + name: "no rules", + state: &Model{ + ProjectId: testProjectId, + Region: testRegion, + Name: testName, + Id: testId, + }, + region: testRegion.ValueString(), + input: &albWaf.GetCustomRuleGroupResponse{ + Name: testName.ValueString(), + }, + expected: &Model{ + Name: testName, + Id: testId, + ProjectId: testProjectId, + Region: testRegion, + Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleType}, []attr.Value{}), + }, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := mapFields(ctx, tt.input, tt.state, tt.region); (err == nil) != tt.isValid { + t.Errorf("unexpected error") + } + if tt.isValid { + if diff := cmp.Diff(tt.state, tt.expected); diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/albwaf/managed_rule_set/datasource.go b/stackit/internal/services/albwaf/managed_rule_set/datasource.go index b802907a4..66956272b 100644 --- a/stackit/internal/services/albwaf/managed_rule_set/datasource.go +++ b/stackit/internal/services/albwaf/managed_rule_set/datasource.go @@ -11,10 +11,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -100,21 +99,6 @@ func (d *managedRuleSetDataSource) Schema(_ context.Context, _ datasource.Schema Description: descriptions["version"], Computed: true, }, - "usage": schema.SingleNestedAttribute{ - Description: descriptions["usage"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "count": schema.Int32Attribute{ - Description: descriptions["usage_count"], - Computed: true, - }, - "items": schema.ListAttribute{ - Description: descriptions["usage_items"], - Computed: true, - ElementType: types.StringType, - }, - }, - }, "groups": schema.MapNestedAttribute{ Description: descriptions["groups"], Computed: true, diff --git a/stackit/internal/services/albwaf/managed_rule_set/resource.go b/stackit/internal/services/albwaf/managed_rule_set/resource.go index 0409ec6e8..8627854ba 100644 --- a/stackit/internal/services/albwaf/managed_rule_set/resource.go +++ b/stackit/internal/services/albwaf/managed_rule_set/resource.go @@ -19,7 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -43,7 +43,6 @@ type Model struct { Name types.String `tfsdk:"name"` Groups types.Map `tfsdk:"groups"` Type types.String `tfsdk:"type"` - Usage types.Object `tfsdk:"usage"` Version types.String `tfsdk:"version"` } @@ -73,16 +72,6 @@ var ruleType = map[string]attr.Type{ "severity": types.StringType, } -type UsageModel struct { - Count types.Int32 `tfsdk:"count"` - Items types.List `tfsdk:"items"` -} - -var usageType = map[string]attr.Type{ - "count": types.Int32Type, - "items": types.ListType{ElemType: types.StringType}, -} - type managedRuleSetResource struct { client *albWaf.APIClient providerData core.ProviderData @@ -124,9 +113,6 @@ var descriptions = map[string]string{ "name": "Managed Rule Set configuration name.", "type": "Type of the Managed Rule Set.", "version": "Managed Rule Set version.", - "usage": "Managed Rule Set usage", - "usage_count": "Number of WAFs using this Managed Rule Set.", - "usage_items": "List of WAFs that use this Managed Rule Set.", "groups": "Inventory of all available Managed Rule Set groups and their current configuration.", "group_description": "A description of what this group covers.", "group_name": "The name for the rule group.", @@ -190,21 +176,6 @@ func (r *managedRuleSetResource) Schema(_ context.Context, _ resource.SchemaRequ Description: descriptions["version"], Computed: true, }, - "usage": schema.SingleNestedAttribute{ - Description: descriptions["usage"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "count": schema.Int32Attribute{ - Description: descriptions["usage_count"], - Computed: true, - }, - "items": schema.ListAttribute{ - Description: descriptions["usage_items"], - Computed: true, - ElementType: types.StringType, - }, - }, - }, "groups": schema.MapNestedAttribute{ Description: descriptions["groups"], Computed: true, @@ -321,16 +292,10 @@ func (r *managedRuleSetResource) Create(ctx context.Context, req resource.Create ctx = core.LogResponse(ctx) - if createResp.Name == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating ALB WAF Managed Rule Set", "Got empty Managed Rule Set name") - return - } - managedRuleSetName := *createResp.Name - ctx = tfutils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ "project_id": projectId, "region": region, - "name": managedRuleSetName, + "name": createResp.Name, }) if resp.Diagnostics.HasError() { return @@ -432,8 +397,8 @@ func toCreatePayload(_ context.Context, model *Model) (*albWaf.CreateManagedRule } payload := &albWaf.CreateManagedRuleSetPayload{ - Name: model.Name.ValueStringPointer(), - Type: new(albWaf.MRSType(model.Type.ValueString())), + Name: model.Name.ValueString(), + Type: albWaf.Type(model.Type.ValueString()), } return payload, nil @@ -453,35 +418,35 @@ func mapFields(ctx context.Context, managedRuleSet *albWaf.GetManagedRuleSetResp model.Name = types.StringValue(model.Name.ValueString()) model.Region = types.StringValue(region) - model.Type = types.StringPointerValue((*string)(managedRuleSet.Type)) - model.Version = types.StringPointerValue(managedRuleSet.Version) + model.Type = types.StringValue(string(managedRuleSet.Type)) + model.Version = types.StringValue(managedRuleSet.Version) groupsMap := map[string]attr.Value{} if groups, ok := managedRuleSet.GetGroupsOk(); ok { for groupKey, group := range *groups { groupTF := RuleGroupModel{ - Description: types.StringPointerValue(group.Description), - GroupName: types.StringPointerValue(group.GroupName), + Description: types.StringValue(group.Description), + GroupName: types.StringValue(group.GroupName), } ruleMap := map[string]attr.Value{} if rules, ok := group.GetRulesOk(); ok { for ruleKey, rule := range *rules { ruleTF := RuleModel{ - Description: types.StringPointerValue(rule.Description), - Mode: types.StringPointerValue((*string)(rule.Mode)), - Severity: types.StringPointerValue(rule.Severity), + Description: types.StringValue(rule.Description), + Mode: types.StringValue(string(rule.Mode)), + Severity: types.StringValue(rule.Severity), } ruleMap[ruleKey], diags = types.ObjectValueFrom(ctx, ruleType, ruleTF) if diags.HasError() { - return fmt.Errorf("mapping role: %w", core.DiagsToError(diags)) + return fmt.Errorf("mapping rule: %w", core.DiagsToError(diags)) } } } groupTF.Rules, diags = types.MapValue(types.ObjectType{AttrTypes: ruleType}, ruleMap) if diags.HasError() { - return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) + return fmt.Errorf("mapping rules: %w", core.DiagsToError(diags)) } groupsMap[groupKey], diags = types.ObjectValueFrom(ctx, ruleGroupType, groupTF) @@ -498,23 +463,5 @@ func mapFields(ctx context.Context, managedRuleSet *albWaf.GetManagedRuleSetResp return fmt.Errorf("mapping groups: %w", core.DiagsToError(diags)) } - if usage, ok := managedRuleSet.GetUsageOk(); ok { - usageModel := UsageModel{ - Count: types.Int32PointerValue(usage.Count), - } - - usageModel.Items, diags = types.ListValueFrom(ctx, types.StringType, usage.GetItems()) - if diags.HasError() { - return fmt.Errorf("creating usage object: %w", core.DiagsToError(diags)) - } - - model.Usage, diags = types.ObjectValueFrom(ctx, usageType, usageModel) - if diags.HasError() { - return fmt.Errorf("creating usage object: %w", core.DiagsToError(diags)) - } - } else { - model.Usage = types.ObjectNull(usageType) - } - return nil } diff --git a/stackit/internal/services/albwaf/managed_rule_set/resource_test.go b/stackit/internal/services/albwaf/managed_rule_set/resource_test.go index 9b7bc9548..5271a5381 100644 --- a/stackit/internal/services/albwaf/managed_rule_set/resource_test.go +++ b/stackit/internal/services/albwaf/managed_rule_set/resource_test.go @@ -9,7 +9,7 @@ import ( "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" - albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" ) var ( @@ -33,11 +33,11 @@ func TestToCreatePayload(t *testing.T) { Id: testId, ProjectId: testProjectId, Region: testRegion, - Type: types.StringValue(string(albWaf.MRSTYPE_TYPE_OWASP_CRS)), + Type: types.StringValue(string(albWaf.TYPE_TYPE_OWASP_CRS)), }, expected: &albWaf.CreateManagedRuleSetPayload{ - Name: testName.ValueStringPointer(), - Type: new(albWaf.MRSTYPE_TYPE_OWASP_CRS), + Name: testName.ValueString(), + Type: albWaf.TYPE_TYPE_OWASP_CRS, }, isValid: true, }, @@ -74,23 +74,24 @@ func TestMapFields(t *testing.T) { ProjectId: testProjectId, Region: testRegion, Name: testName, - Type: types.StringValue(string(albWaf.MRSTYPE_TYPE_OWASP_CRS)), + Type: types.StringValue(string(albWaf.TYPE_TYPE_OWASP_CRS)), Id: testId, Groups: types.MapValueMust(types.ObjectType{AttrTypes: ruleGroupType}, map[string]attr.Value{}), }, region: testRegion.ValueString(), input: &albWaf.GetManagedRuleSetResponse{ Groups: &map[string]albWaf.MRSRuleGroup{}, - Name: testName.ValueStringPointer(), - Type: new(albWaf.MRSTYPE2_TYPE_OWASP_CRS), + Name: testName.ValueString(), + Type: albWaf.TYPE_TYPE_OWASP_CRS, }, expected: &Model{ ProjectId: testProjectId, Region: testRegion, Name: testName, - Type: types.StringValue(string(albWaf.MRSTYPE_TYPE_OWASP_CRS)), + Type: types.StringValue(string(albWaf.TYPE_TYPE_OWASP_CRS)), Id: testId, Groups: types.MapValueMust(types.ObjectType{AttrTypes: ruleGroupType}, map[string]attr.Value{}), + Version: types.StringValue(""), }, isValid: true, }, diff --git a/stackit/internal/services/albwaf/testdata/custom-rule-group-max.tf b/stackit/internal/services/albwaf/testdata/custom-rule-group-max.tf new file mode 100644 index 000000000..095df35cf --- /dev/null +++ b/stackit/internal/services/albwaf/testdata/custom-rule-group-max.tf @@ -0,0 +1,42 @@ + +variable "project_id" {} +variable "name" {} +variable "description" {} +variable "action" {} +variable "log" {} +variable "log_msg" {} +variable "operator_type" {} +variable "operator_value" {} +variable "transformation" {} +variable "variable_type" {} +variable "variable_value" {} + +resource "stackit_alb_waf_custom_rule_group" "custom_rule_group" { + project_id = var.project_id + name = var.name + rules = [ + { + description = var.description + behavior = { + action = var.action + log = var.log + log_msg = var.log_msg + } + conditions = [ + { + operator = { + type = var.operator_type + value = var.operator_value + } + transformations = [ + var.transformation + ] + variable = { + type = var.variable_type + value = var.variable_value + } + } + ] + } + ] +} diff --git a/stackit/internal/services/albwaf/testdata/custom-rule-group-min.tf b/stackit/internal/services/albwaf/testdata/custom-rule-group-min.tf new file mode 100644 index 000000000..139e55b45 --- /dev/null +++ b/stackit/internal/services/albwaf/testdata/custom-rule-group-min.tf @@ -0,0 +1,28 @@ + +variable "project_id" {} +variable "name" {} +variable "action" {} +variable "operator_type" {} +variable "variable_type" {} + +resource "stackit_alb_waf_custom_rule_group" "custom_rule_group" { + project_id = var.project_id + name = var.name + rules = [ + { + behavior = { + action = var.action + } + conditions = [ + { + operator = { + type = var.operator_type + } + variable = { + type = var.variable_type + } + } + ] + } + ] +} diff --git a/stackit/internal/services/albwaf/utils/util.go b/stackit/internal/services/albwaf/utils/util.go index 9ed484a50..fc02f8d89 100644 --- a/stackit/internal/services/albwaf/utils/util.go +++ b/stackit/internal/services/albwaf/utils/util.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" - albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" diff --git a/stackit/internal/services/albwaf/utils/util_test.go b/stackit/internal/services/albwaf/utils/util_test.go index ccd9f606c..171ab8ebe 100644 --- a/stackit/internal/services/albwaf/utils/util_test.go +++ b/stackit/internal/services/albwaf/utils/util_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" - albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1betaapi" + albWaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" diff --git a/stackit/provider.go b/stackit/provider.go index 5c99aaa50..41008bc20 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -21,6 +21,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/access_token" alb "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/alb/applicationloadbalancer" cert "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albcertificates/certificate" + albWafCustomRuleGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/custom_rule_group" albWafManagedRuleSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/albwaf/managed_rule_set" customRole "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/customrole" roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments" @@ -664,6 +665,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource { dataSources := []func() datasource.DataSource{ alb.NewApplicationLoadBalancerDataSource, + albWafCustomRuleGroup.NewCustomRuleGroupDataSource, albWafManagedRuleSet.NewManagedRuleSetDataSource, alertGroup.NewAlertGroupDataSource, cdn.NewDistributionDataSource, @@ -779,6 +781,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource func (p *Provider) Resources(_ context.Context) []func() resource.Resource { resources := []func() resource.Resource{ alb.NewApplicationLoadBalancerResource, + albWafCustomRuleGroup.NewCustomRuleGroupResource, albWafManagedRuleSet.NewManagedRuleSetResource, alertGroup.NewAlertGroupResource, cdn.NewDistributionResource,