diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 099a24799..c81d09ad3 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -27,7 +27,7 @@ data "stackit_cdn_distribution" "example" { ### Required -- `distribution_id` (String) STACKIT project ID associated with the distribution +- `distribution_id` (String) CDN distribution ID - `project_id` (String) STACKIT project ID associated with the distribution ### Read-Only @@ -53,6 +53,7 @@ Read-Only: - `optimizer` (Attributes) Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience. (see [below for nested schema](#nestedatt--config--optimizer)) - `redirects` (Attributes) A wrapper for a list of redirect rules that allows for redirect settings on a distribution (see [below for nested schema](#nestedatt--config--redirects)) - `regions` (List of String) The configured regions where content will be hosted +- `waf` (Attributes) Configures the Web Application Firewall (WAF) for the distribution. If this block is undefined or removed from your configuration, the WAF mode will default to DISABLED and the type to FREE. All other WAF properties will retain their last known state in the API; if they were never defined, the API will apply its default settings. (see [below for nested schema](#nestedatt--config--waf)) ### Nested Schema for `config.backend` @@ -105,6 +106,28 @@ Read-Only: + +### Nested Schema for `config.waf` + +Read-Only: + +- `allowed_http_methods` (Set of String) Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. Case you removed waf will retain the last known state and if omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. +- `allowed_http_versions` (Set of String) Restricts which HTTP protocol versions are accepted. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. +- `allowed_request_content_types` (Set of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the set must contain at least one item. Case you removed waf will retain the last known state and if omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. +- `disabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (Set of String) Set of WAF rule IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. +- `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Case you removed waf will retain the last known state and if omitted, The API applies the following default 'L1'. +- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. + + ### Nested Schema for `domains` diff --git a/docs/resources/cdn_distribution.md b/docs/resources/cdn_distribution.md index f60345443..046eeffae 100644 --- a/docs/resources/cdn_distribution.md +++ b/docs/resources/cdn_distribution.md @@ -74,6 +74,39 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { } ] } + + # WAF Configuration + # + # Precedence Hierarchy: Specific Rules > Groups > Collections + # In this example, the entire "@builtin/crs/request" collection is ENABLED. + # However, because specific Rule IDs have a higher precedence, the rule + # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. + # + # To view all available collections, groups, and rules, consult the API documentation: + # https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections + waf = { + mode = "ENABLED" + type = "PREMIUM" + paranoia_level = "L1" + allowed_http_versions = ["HTTP/1.0", "HTTP/1.1"] + allowed_http_methods = ["GET"] + allowed_request_content_types = ["text/plain"] + + # Collections + enabled_rule_collection_ids = ["@builtin/crs/request"] + disabled_rule_collection_ids = [] + log_only_rule_collection_ids = ["@builtin/crs/response"] + + # Groups + enabled_rule_group_ids = [] + disabled_rule_group_ids = [] + log_only_rule_group_ids = [] + + # Specific Rules (Highest Precedence) + enabled_rule_ids = ["@builtin/crs/request/913100"] + disabled_rule_ids = ["@builtin/crs/request/942151"] + log_only_rule_ids = ["@builtin/crs/response/954120"] + } } } @@ -115,6 +148,7 @@ Optional: - `blocked_countries` (List of String) The configured countries where distribution of content is blocked - `optimizer` (Attributes) Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience. (see [below for nested schema](#nestedatt--config--optimizer)) - `redirects` (Attributes) A wrapper for a list of redirect rules that allows for redirect settings on a distribution (see [below for nested schema](#nestedatt--config--redirects)) +- `waf` (Attributes) Configures the Web Application Firewall (WAF) for the distribution. If this block is undefined or removed from your configuration, the WAF mode will default to DISABLED and the type to FREE. All other WAF properties will retain their last known state in the API; if they were never defined, the API will apply its default settings. (see [below for nested schema](#nestedatt--config--waf)) ### Nested Schema for `config.backend` @@ -186,6 +220,28 @@ Optional: + +### Nested Schema for `config.waf` + +Optional: + +- `allowed_http_methods` (Set of String) Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. Case you removed waf will retain the last known state and if omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`. +- `allowed_http_versions` (Set of String) Restricts which HTTP protocol versions are accepted. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`. +- `allowed_request_content_types` (Set of String) Restricts which Content-Type headers are accepted in request bodies. If provided, the set must contain at least one item. Case you removed waf will retain the last known state and if omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`. +- `disabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `disabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `enabled_rule_ids` (Set of String) Set of WAF rule IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_collection_ids` (Set of String) Set of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_group_ids` (Set of String) Set of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `log_only_rule_ids` (Set of String) Set of WAF rule IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections +- `mode` (String) The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'. +- `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Case you removed waf will retain the last known state and if omitted, The API applies the following default 'L1'. +- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. + + ### Nested Schema for `domains` diff --git a/examples/resources/stackit_cdn_distribution/resource.tf b/examples/resources/stackit_cdn_distribution/resource.tf index 4c37818bf..afeabecc6 100644 --- a/examples/resources/stackit_cdn_distribution/resource.tf +++ b/examples/resources/stackit_cdn_distribution/resource.tf @@ -56,6 +56,39 @@ resource "stackit_cdn_distribution" "example_bucket_distribution" { } ] } + + # WAF Configuration + # + # Precedence Hierarchy: Specific Rules > Groups > Collections + # In this example, the entire "@builtin/crs/request" collection is ENABLED. + # However, because specific Rule IDs have a higher precedence, the rule + # "@builtin/crs/request/942151" is explicitly DISABLED, overriding the collection setting. + # + # To view all available collections, groups, and rules, consult the API documentation: + # https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections + waf = { + mode = "ENABLED" + type = "PREMIUM" + paranoia_level = "L1" + allowed_http_versions = ["HTTP/1.0", "HTTP/1.1"] + allowed_http_methods = ["GET"] + allowed_request_content_types = ["text/plain"] + + # Collections + enabled_rule_collection_ids = ["@builtin/crs/request"] + disabled_rule_collection_ids = [] + log_only_rule_collection_ids = ["@builtin/crs/response"] + + # Groups + enabled_rule_group_ids = [] + disabled_rule_group_ids = [] + log_only_rule_group_ids = [] + + # Specific Rules (Highest Precedence) + enabled_rule_ids = ["@builtin/crs/request/913100"] + disabled_rule_ids = ["@builtin/crs/request/942151"] + log_only_rule_ids = ["@builtin/crs/response/954120"] + } } } diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index 18cb69012..712da0d36 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) func ToString(ctx context.Context, v attr.Value) (string, error) { @@ -263,3 +264,55 @@ func ParseEphemeralProviderData(ctx context.Context, providerData any, diags *di } return stackitProviderData, true } + +// StringListToSet safely converts a Go slice of strings into a Terraform framework types.Set. +// +// By accepting a pointer to diag.Diagnostics, it enables clean, inline assignments within +// map literals (avoiding verbose error checking per field) while correctly accumulating +// framework diagnostics. +// +// State mapping behavior: +// - Nil inputs map directly to types.SetNull to accurately reflect unconfigured states. +// - On conversion failure, it mutates the provided diags and returns an Unknown set +// (basetypes.NewSetUnknown) to prevent state corruption and allow Terraform to handle +// the error gracefully. +func StringListToSet(ctx context.Context, stringList []string, diags *diag.Diagnostics) types.Set { + if stringList == nil { + return types.SetNull(types.StringType) + } + + setVal, d := types.SetValueFrom(ctx, types.StringType, stringList) + diags.Append(d...) + + if d.HasError() { + return basetypes.NewSetUnknown(types.StringType) + } + + return setVal +} + +// TerraformStringSetToList safely converts a Go slice of strings into a Terraform framework types.Set. +// +// By accepting a pointer to diag.Diagnostics, it enables clean, inline assignments within +// map literals (avoiding verbose error checking per field) while correctly accumulating +// framework diagnostics. +// +// State mapping behavior: +// - Nil inputs or IsUnknown return nil. +// - On conversion failure, it mutates the provided diags and returns an nil +func TerraformStringSetToList(ctx context.Context, tfSet basetypes.SetValue, diags *diag.Diagnostics) []string { + if utils.IsUndefined(tfSet) { + return nil + } + + var elements []string + d := tfSet.ElementsAs(ctx, &elements, true) + + diags.Append(d...) + + if d.HasError() { + return nil + } + + return elements +} diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index 0ebfccf86..2f0031226 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -511,3 +511,127 @@ func TestStringListToSlice(t *testing.T) { }) } } + +func TestStringListToSet(t *testing.T) { + t.Parallel() + tests := []struct { + name string + stringList []string + want types.Set + wantErr bool + }{ + { + name: "nil list", + stringList: nil, + want: basetypes.NewSetNull(types.StringType), + wantErr: false, + }, + { + name: "empty list", + stringList: []string{}, + want: basetypes.NewSetValueMust(types.StringType, []attr.Value{}), + wantErr: false, + }, + { + name: "valid list", + stringList: []string{"value1", "value2"}, + want: basetypes.NewSetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("value1"), + types.StringValue("value2"), + }, + ), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + var diags diag.Diagnostics + + got := StringListToSet(ctx, tt.stringList, &diags) + + if diags.HasError() != tt.wantErr { + t.Fatalf("expected error presence: %v, got diagnostics: %v", tt.wantErr, diags) + } + + if !got.Equal(tt.want) { + t.Fatalf("expected set: %v, got set: %v", tt.want, got) + } + }) + } +} + +func TestTerraformStringSetToList(t *testing.T) { + t.Parallel() + tests := []struct { + name string + tfSet basetypes.SetValue + want []string + wantErr bool + }{ + { + name: "unknown", + tfSet: basetypes.NewSetUnknown(types.StringType), + want: nil, + wantErr: false, + }, + { + name: "null", + tfSet: basetypes.NewSetNull(types.StringType), + want: nil, + wantErr: false, + }, + { + name: "invalid type", + tfSet: basetypes.NewSetValueMust(types.Int64Type, []attr.Value{types.Int64Value(123)}), + want: nil, + wantErr: true, + }, + { + name: "valid string set", + tfSet: basetypes.NewSetValueMust( + types.StringType, + []attr.Value{ + types.StringValue("value1"), + types.StringValue("value2"), + }, + ), + want: []string{ + "value1", + "value2", + }, + wantErr: false, + }, + { + name: "empty string set", + tfSet: basetypes.NewSetValueMust( + types.StringType, + []attr.Value{}, + ), + want: []string{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + var diags diag.Diagnostics + + got := TerraformStringSetToList(ctx, tt.tfSet, &diags) + + if diags.HasError() != tt.wantErr { + t.Fatalf("expected error presence: %v, got diagnostics: %v", tt.wantErr, diags) + } + + if d := cmp.Diff(got, tt.want); d != "" { + t.Fatalf("no match, diff: %s", d) + } + }) + } +} diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index f7be85668..369f23c5e 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -46,6 +46,13 @@ var ( cert, key = makeCertAndKey(testutil.OrganizationId, fullDomainNameHttp) ) +// WAF Rules for Base and Updated states +var ( + wafRule1 = "@builtin/crs/response/955100" + wafRule2 = "@builtin/crs/response/955320" + wafRule3 = "@builtin/crs/response/955102" +) + var ( //go:embed testdata/resource-bucket.tf resourceBucket string @@ -78,34 +85,70 @@ func configVarsBucketUpdated() config.Variables { } var testConfigVarsHttp = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(httpTestName), - "regions": config.ListVariable(config.StringVariable("EU"), config.StringVariable("US")), - "dns_zone_name": config.StringVariable("acc_cdn_test_zone"), - "dns_name": config.StringVariable(dnsNameHttp), - "dns_record_name": config.StringVariable(dnsRecordNameHttp), - "optimizer": config.BoolVariable(true), - "backend_http_type": config.StringVariable("http"), - "blocked_countries": config.ListVariable(config.StringVariable("CU")), - "backend_origin_url": config.StringVariable("https://test-backend-1.cdn-dev.runs.onstackit.cloud"), - "geofencing_list": config.ListVariable(config.StringVariable("DE")), - "origin_request_headers_name": config.StringVariable("X-Custom-Header"), - "origin_request_headers_value": config.StringVariable("x-custom-value"), - "certificate": config.StringVariable(string(cert)), - "private_key": config.StringVariable(string(key)), - "redirect_target_url": config.StringVariable("https://example.com"), - "redirect_status_code": config.IntegerVariable(301), - "redirect_matcher_value": config.StringVariable("/shop/*"), - "redirect_rule_description": config.StringVariable("Acc test redirect"), - "redirect_rule_enabled": config.BoolVariable(true), - "redirect_rule_match_condition": config.StringVariable("ANY"), - "redirect_matcher_condition": config.StringVariable("ANY"), + "project_id": config.StringVariable(testutil.ProjectId), + "name": config.StringVariable(httpTestName), + "regions": config.ListVariable(config.StringVariable("EU"), config.StringVariable("US")), + "dns_zone_name": config.StringVariable("acc_cdn_test_zone"), + "dns_name": config.StringVariable(dnsNameHttp), + "dns_record_name": config.StringVariable(dnsRecordNameHttp), + "optimizer": config.BoolVariable(true), + "backend_http_type": config.StringVariable("http"), + "blocked_countries": config.ListVariable(config.StringVariable("CU")), + "backend_origin_url": config.StringVariable("https://test-backend-1.cdn-dev.runs.onstackit.cloud"), + "geofencing_list": config.ListVariable(config.StringVariable("DE")), + "origin_request_headers_name": config.StringVariable("X-Custom-Header"), + "origin_request_headers_value": config.StringVariable("x-custom-value"), + "certificate": config.StringVariable(string(cert)), + "private_key": config.StringVariable(string(key)), + "redirect_target_url": config.StringVariable("https://example.com"), + "redirect_status_code": config.IntegerVariable(301), + "redirect_matcher_value": config.StringVariable("/shop/*"), + "redirect_rule_description": config.StringVariable("Acc test redirect"), + "redirect_rule_enabled": config.BoolVariable(true), + "redirect_rule_match_condition": config.StringVariable("ANY"), + "redirect_matcher_condition": config.StringVariable("ANY"), + "waf_mode": config.StringVariable("ENABLED"), + "waf_type": config.StringVariable("FREE"), + "waf_allowed_http_methods_0": config.StringVariable("GET"), + "waf_allowed_request_content_types_0": config.StringVariable("application/json"), + "waf_allowed_http_versions_0": config.StringVariable("HTTP/1.0"), + "waf_paranoia_level": config.StringVariable("L2"), + "waf_disabled_rule_ids_0": config.StringVariable(wafRule1), + "waf_disabled_rule_group_ids_0": config.StringVariable(wafRule1), + "waf_disabled_rule_collection_ids_0": config.StringVariable(wafRule1), + "waf_enabled_rule_ids_0": config.StringVariable(wafRule2), + "waf_enabled_rule_group_ids_0": config.StringVariable(wafRule2), + "waf_enabled_rule_collection_ids_0": config.StringVariable(wafRule2), + "waf_log_only_rule_ids_0": config.StringVariable(wafRule3), + "waf_log_only_rule_group_ids_0": config.StringVariable(wafRule3), + "waf_log_only_rule_collection_ids_0": config.StringVariable(wafRule3), } func configVarsHttpUpdated() config.Variables { updatedConfig := maps.Clone(testConfigVarsHttp) updatedConfig["regions"] = config.ListVariable(config.StringVariable("EU"), config.StringVariable("US"), config.StringVariable("ASIA")) updatedConfig["redirect_target_url"] = config.StringVariable("https://example.com/updated") + + // Update WAF configuration to test mutation + updatedConfig["waf_mode"] = config.StringVariable("LOG_ONLY") + updatedConfig["waf_type"] = config.StringVariable("PREMIUM") + updatedConfig["waf_allowed_http_methods_0"] = config.StringVariable("POST") + updatedConfig["waf_allowed_request_content_types_0"] = config.StringVariable("text/plain") + updatedConfig["waf_allowed_http_versions_0"] = config.StringVariable("HTTP/1.1") + updatedConfig["waf_paranoia_level"] = config.StringVariable("L3") + + updatedConfig["waf_enabled_rule_ids_0"] = config.StringVariable(wafRule2) + updatedConfig["waf_enabled_rule_group_ids_0"] = config.StringVariable(wafRule2) + updatedConfig["waf_enabled_rule_collection_ids_0"] = config.StringVariable(wafRule2) + + updatedConfig["waf_log_only_rule_ids_0"] = config.StringVariable(wafRule1) + updatedConfig["waf_log_only_rule_group_ids_0"] = config.StringVariable(wafRule1) + updatedConfig["waf_log_only_rule_collection_ids_0"] = config.StringVariable(wafRule1) + + updatedConfig["waf_disabled_rule_ids_0"] = config.StringVariable(wafRule3) + updatedConfig["waf_disabled_rule_group_ids_0"] = config.StringVariable(wafRule3) + updatedConfig["waf_disabled_rule_collection_ids_0"] = config.StringVariable(wafRule3) + return updatedConfig } @@ -190,6 +233,41 @@ func TestAccCDNDistributionHttp(t *testing.T) { "DE", ), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), + + // WAF Checks + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.mode", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_mode"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_type"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_methods.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_methods.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_allowed_http_methods_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_request_content_types.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_request_content_types.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_allowed_request_content_types_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_versions.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_versions.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_allowed_http_versions_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.paranoia_level", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_paranoia_level"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_disabled_rule_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_group_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_disabled_rule_group_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_collection_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_disabled_rule_collection_ids_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_enabled_rule_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_group_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_enabled_rule_group_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_collection_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_enabled_rule_collection_ids_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_log_only_rule_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_group_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_log_only_rule_group_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_collection_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_log_only_rule_collection_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), ), @@ -233,7 +311,7 @@ func TestAccCDNDistributionHttp(t *testing.T) { }, ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domains"}, // we added a domain in the meantime... + ImportStateVerifyIgnore: []string{"domains"}, }, { ResourceName: "stackit_cdn_custom_domain.custom_domain", @@ -292,6 +370,41 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.blocked_countries.#", "1"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), + + // WAF Checks inside Data Source + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.mode", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_mode"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_type"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.allowed_http_methods.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.allowed_http_methods.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_allowed_http_methods_0"])), + + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.allowed_request_content_types.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.allowed_request_content_types.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_allowed_request_content_types_0"])), + + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.allowed_http_versions.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.allowed_http_versions.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_allowed_http_versions_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.paranoia_level", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_paranoia_level"])), + + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.disabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.disabled_rule_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_disabled_rule_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.disabled_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.disabled_rule_group_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_disabled_rule_group_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.disabled_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.disabled_rule_collection_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_disabled_rule_collection_ids_0"])), + + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_enabled_rule_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_group_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_enabled_rule_group_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_collection_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_enabled_rule_collection_ids_0"])), + + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.log_only_rule_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.log_only_rule_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_log_only_rule_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.log_only_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.log_only_rule_group_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_log_only_rule_group_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.log_only_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.log_only_rule_collection_ids.0", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_log_only_rule_collection_ids_0"])), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.redirects.rules.#", "1"), @@ -331,6 +444,41 @@ func TestAccCDNDistributionHttp(t *testing.T) { resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "1"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", testutil.ConvertConfigVariable(testConfigVarsHttp["optimizer"])), + + // Checking WAF Mutated Configurations + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.mode", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_mode"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_type"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_methods.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_methods.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_allowed_http_methods_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_request_content_types.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_request_content_types.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_allowed_request_content_types_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_versions.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.allowed_http_versions.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_allowed_http_versions_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.paranoia_level", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_paranoia_level"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_disabled_rule_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_group_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_disabled_rule_group_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.disabled_rule_collection_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_disabled_rule_collection_ids_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_enabled_rule_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_group_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_enabled_rule_group_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_collection_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_enabled_rule_collection_ids_0"])), + + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_log_only_rule_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_group_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_group_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_log_only_rule_group_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_collection_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.log_only_rule_collection_ids.0", testutil.ConvertConfigVariable(configVarsHttpUpdated()["waf_log_only_rule_collection_ids_0"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr( diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index d78702686..69ac63f4a 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -43,6 +43,9 @@ var dataSourceConfigTypes = map[string]attr.Type{ "redirects": types.ObjectType{ AttrTypes: redirectsTypes, // Shared from resource.go }, + "waf": types.ObjectType{ + AttrTypes: wafTypes, // Shared from resource.go + }, } type distributionDataSource struct { @@ -92,7 +95,7 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe Computed: true, }, "distribution_id": schema.StringAttribute{ - Description: schemaDescriptions["project_id"], + Description: schemaDescriptions["distribution_id"], Required: true, Validators: []validator.String{ validate.UUID(), @@ -255,6 +258,84 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe }, }, }, + "waf": schema.SingleNestedAttribute{ + Description: schemaDescriptions["config_waf"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "mode": schema.StringAttribute{ + Computed: true, + Description: schemaDescriptions["waf_mode"], + }, + "type": schema.StringAttribute{ + Computed: true, + Description: schemaDescriptions["waf_type"], + }, + "paranoia_level": schema.StringAttribute{ + Computed: true, + Description: schemaDescriptions["waf_paranoia_level"], + }, + "allowed_http_versions": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_versions"], + }, + "allowed_request_content_types": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_request_content_types"], + }, + "allowed_http_methods": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_methods"], + }, + "enabled_rule_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_ids"], + }, + "disabled_rule_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_ids"], + }, + "log_only_rule_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_ids"], + }, + "enabled_rule_group_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_group_ids"], + }, + "disabled_rule_group_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_group_ids"], + }, + "log_only_rule_group_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_group_ids"], + }, + "enabled_rule_collection_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_collection_ids"], + }, + "disabled_rule_collection_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_collection_ids"], + }, + "log_only_rule_collection_ids": schema.SetAttribute{ + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_collection_ids"], + }, + }, + }, }, }, }, @@ -512,6 +593,40 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, return core.DiagsToError(diags) } + // Map Waf + var pl *string + if distribution.Config.Waf.ParanoiaLevel != nil { + pl = new(string(*distribution.Config.Waf.ParanoiaLevel)) + } + wafObjAttrs := map[string]attr.Value{ + "mode": types.StringValue(string(distribution.Config.Waf.Mode)), + "type": types.StringValue(string(distribution.Config.Waf.Type)), + "paranoia_level": types.StringPointerValue(pl), + "allowed_http_versions": conversion.StringListToSet(ctx, distribution.Config.Waf.AllowedHttpVersions, &diags), + "allowed_request_content_types": conversion.StringListToSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes, &diags), + "allowed_http_methods": conversion.StringListToSet(ctx, distribution.Config.Waf.AllowedHttpMethods, &diags), + "enabled_rule_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.EnabledRuleIds, &diags), + "disabled_rule_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.DisabledRuleIds, &diags), + "log_only_rule_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.LogOnlyRuleIds, &diags), + "enabled_rule_group_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds, &diags), + "disabled_rule_group_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds, &diags), + "log_only_rule_group_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds, &diags), + "enabled_rule_collection_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds, &diags), + "disabled_rule_collection_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds, &diags), + "log_only_rule_collection_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds, &diags), + } + + if diags.HasError() { + return core.DiagsToError(diags) + } + var wafVal attr.Value + + var diagWaf diag.Diagnostics + wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) + if diagWaf.HasError() { + return core.DiagsToError(diagWaf) + } + // Optimizer optimizerVal := types.ObjectNull(optimizerTypes) if o := distribution.Config.Optimizer; o != nil { @@ -533,6 +648,7 @@ func mapDataSourceFields(ctx context.Context, distribution *cdnSdk.Distribution, "blocked_countries": modelBlockedCountries, "optimizer": optimizerVal, "redirects": redirectsVal, + "waf": wafVal, }) if diags.HasError() { return core.DiagsToError(diags) diff --git a/stackit/internal/services/cdn/distribution/datasource_test.go b/stackit/internal/services/cdn/distribution/datasource_test.go index 208383d4d..5124f4e29 100644 --- a/stackit/internal/services/cdn/distribution/datasource_test.go +++ b/stackit/internal/services/cdn/distribution/datasource_test.go @@ -45,6 +45,7 @@ func TestMapDataSourceFields(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) redirectsInput := cdnSdk.RedirectConfig{ Rules: []cdnSdk.RedirectRule{ @@ -93,6 +94,49 @@ func TestMapDataSourceFields(t *testing.T) { "errors": types.ListValueMust(types.StringType, []attr.Value{}), }) domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain}) + + // WAF Fixtures + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, + }) + + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } + expectedModel := func(mods ...func(*Model)) *Model { model := &Model{ ID: types.StringValue("test-project-id,test-distribution-id"), @@ -110,6 +154,7 @@ func TestMapDataSourceFields(t *testing.T) { } return model } + distributionFixture := func(mods ...func(*cdnSdk.Distribution)) *cdnSdk.Distribution { distribution := &cdnSdk.Distribution{ Config: cdnSdk.Config{ @@ -154,6 +199,7 @@ func TestMapDataSourceFields(t *testing.T) { "origin_request_headers": types.MapNull(types.StringType), "geofencing": types.MapNull(geofencingTypes.ElemType), }) + tests := map[string]struct { Input *cdnSdk.Distribution Expected *Model @@ -172,6 +218,7 @@ func TestMapDataSourceFields(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -198,6 +245,7 @@ func TestMapDataSourceFields(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) }), IsValid: true, @@ -218,6 +266,7 @@ func TestMapDataSourceFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -242,6 +291,7 @@ func TestMapDataSourceFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigExpected, + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -249,6 +299,22 @@ func TestMapDataSourceFields(t *testing.T) { }), IsValid: true, }, + "happy_path_with_waf": { + Expected: expectedModel(func(m *Model) { + m.Config = types.ObjectValueMust(dataSourceConfigTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), + "blocked_countries": blockedCountriesFixture, + "redirects": types.ObjectNull(redirectsTypes), + "waf": populatedWaf, + }) + }), + Input: distributionFixture(func(d *cdnSdk.Distribution) { + d.Config.Waf = expectedWafConfig + }), + IsValid: true, + }, "happy_path_custom_domain": { Expected: expectedModel(func(m *Model) { managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{ diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 0e6acca3f..6e6eaa07e 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -85,6 +86,22 @@ var schemaDescriptions = map[string]string{ "config_backend_credentials_access_key_id": "The access key for the bucket. Required if type is 'bucket'.", "config_backend_credentials_secret_access_key": "The secret key for the bucket. Required if type is 'bucket'.", "config_backend_credentials": "The credentials for the bucket. Required if type is 'bucket'.", + "config_waf": "Configures the Web Application Firewall (WAF) for the distribution. If this block is undefined or removed from your configuration, the WAF mode will default to DISABLED and the type to FREE. All other WAF properties will retain their last known state in the API; if they were never defined, the API will apply its default settings.", + "waf_mode": "The operating mode of the WAF. 'ENABLED' actively blocks threats, 'LOG_ONLY' logs matches without blocking, and 'DISABLED' completely turns off inspection. Defaults to 'DISABLED'.", + "waf_type": "The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'.", + "waf_paranoia_level": "Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Case you removed waf will retain the last known state and if omitted, The API applies the following default 'L1'.", + "waf_allowed_http_versions": "Restricts which HTTP protocol versions are accepted. If provided, the set must contain at least one item. If omitted, the API applies the following defaults: `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/2.0`.", + "waf_allowed_request_content_types": "Restricts which Content-Type headers are accepted in request bodies. If provided, the set must contain at least one item. Case you removed waf will retain the last known state and if omitted, the API applies the following defaults: `application/x-www-form-urlencoded`, `multipart/form-data`, `multipart/related`, `text/xml`, `application/xml`, `application/soap+xml`, `application/x-amf`, `application/json`, `application/octet-stream`, `application/csp-report`, `application/xss-auditor-report`, `text/plain`.", + "waf_allowed_http_methods": "Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. Case you removed waf will retain the last known state and if omitted, the API applies the following defaults: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH`.", + "waf_enabled_rule_ids": "Set of WAF rule IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly enabled Rule ID takes precedence over a disabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_ids": "Set of WAF rule IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. For example, an explicitly disabled Rule ID takes precedence over an enabled Group ID. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_ids": "Set of WAF rule IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Specific Rules override Groups. To view available rules, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_group_ids": "Set of WAF Rule Group IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_group_ids": "Set of WAF Rule Group IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_group_ids": "Set of WAF Rule Group IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. Precedence hierarchy: Groups override Collections. To view available rule groups, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_enabled_rule_collection_ids": "Set of WAF Collection IDs explicitly enabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_disabled_rule_collection_ids": "Set of WAF Collection IDs explicitly disabled. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", + "waf_log_only_rule_collection_ids": "Set of WAF Collection IDs explicitly marked as Log Only. Can be set to an empty set to clear previously set rules. Case you removed waf will retain the last known state. To view available rule collections, please consult the API documentation: https://docs.api.eu01.stackit.cloud/documentation/cdn/version/v1#tag/WAF/operation/ListWafCollections", } type Model struct { @@ -123,6 +140,7 @@ type distributionConfig struct { Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached BlockedCountries *[]string `tfsdk:"blocked_countries"` // The countries for which content will be blocked Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration + Waf types.Object `tfsdk:"waf"` // The WAF configuration } type optimizerConfig struct { @@ -139,6 +157,24 @@ type backend struct { Credentials *backendCredentials `tfsdk:"credentials"` } +type wafConfig struct { + Mode types.String `tfsdk:"mode"` + Type types.String `tfsdk:"type"` + ParanoiaLevel types.String `tfsdk:"paranoia_level"` + AllowedHttpVersions types.Set `tfsdk:"allowed_http_versions"` + AllowedRequestContentTypes types.Set `tfsdk:"allowed_request_content_types"` + AllowedHttpMethods types.Set `tfsdk:"allowed_http_methods"` + EnabledRuleIds types.Set `tfsdk:"enabled_rule_ids"` + DisabledRuleIds types.Set `tfsdk:"disabled_rule_ids"` + LogOnlyRuleIds types.Set `tfsdk:"log_only_rule_ids"` + EnabledRuleGroupIds types.Set `tfsdk:"enabled_rule_group_ids"` + DisabledRuleGroupIds types.Set `tfsdk:"disabled_rule_group_ids"` + LogOnlyRuleGroupIds types.Set `tfsdk:"log_only_rule_group_ids"` + EnabledRuleCollectionIds types.Set `tfsdk:"enabled_rule_collection_ids"` + DisabledRuleCollectionIds types.Set `tfsdk:"disabled_rule_collection_ids"` + LogOnlyRuleCollectionIds types.Set `tfsdk:"log_only_rule_collection_ids"` +} + type backendCredentials struct { AccessKey *string `tfsdk:"access_key_id"` SecretKey *string `tfsdk:"secret_access_key"` @@ -154,6 +190,9 @@ var configTypes = map[string]attr.Type{ "redirects": types.ObjectType{ AttrTypes: redirectsTypes, }, + "waf": types.ObjectType{ + AttrTypes: wafTypes, + }, } var optimizerTypes = map[string]attr.Type{ @@ -190,6 +229,24 @@ var redirectsTypes = map[string]attr.Type{ }, } +var wafTypes = map[string]attr.Type{ + "mode": types.StringType, + "type": types.StringType, + "paranoia_level": types.StringType, + "allowed_http_versions": types.SetType{ElemType: types.StringType}, + "allowed_request_content_types": types.SetType{ElemType: types.StringType}, + "allowed_http_methods": types.SetType{ElemType: types.StringType}, + "enabled_rule_ids": types.SetType{ElemType: types.StringType}, + "disabled_rule_ids": types.SetType{ElemType: types.StringType}, + "log_only_rule_ids": types.SetType{ElemType: types.StringType}, + "enabled_rule_group_ids": types.SetType{ElemType: types.StringType}, + "disabled_rule_group_ids": types.SetType{ElemType: types.StringType}, + "log_only_rule_group_ids": types.SetType{ElemType: types.StringType}, + "enabled_rule_collection_ids": types.SetType{ElemType: types.StringType}, + "disabled_rule_collection_ids": types.SetType{ElemType: types.StringType}, + "log_only_rule_collection_ids": types.SetType{ElemType: types.StringType}, +} + var backendTypes = map[string]attr.Type{ "type": types.StringType, "origin_url": types.StringType, @@ -403,6 +460,114 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques }, }, }, + "waf": schema.SingleNestedAttribute{ + Description: schemaDescriptions["config_waf"], + Optional: true, + Computed: true, + Attributes: map[string]schema.Attribute{ + "mode": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: schemaDescriptions["waf_mode"], + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafModeEnumValues)...)}, + Default: stringdefault.StaticString(string(cdnSdk.WAFMODE_DISABLED)), + }, + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: schemaDescriptions["waf_type"], + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafTypeEnumValues)...)}, + Default: stringdefault.StaticString(string(cdnSdk.WAFTYPE_FREE)), + }, + "paranoia_level": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: schemaDescriptions["waf_paranoia_level"], + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafParanoiaLevelEnumValues)...)}, + }, + "allowed_http_versions": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_http_versions"], + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + "allowed_request_content_types": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_allowed_request_content_types"], + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + "allowed_http_methods": schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Computed: true, + Description: schemaDescriptions["waf_allowed_http_methods"], + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + "enabled_rule_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_ids"], + }, + "disabled_rule_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_ids"], + }, + "log_only_rule_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_ids"], + }, + "enabled_rule_group_ids": schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Computed: true, + Description: schemaDescriptions["waf_enabled_rule_group_ids"], + }, + "disabled_rule_group_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_group_ids"], + }, + "log_only_rule_group_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_group_ids"], + }, + "enabled_rule_collection_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_enabled_rule_collection_ids"], + }, + "disabled_rule_collection_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_disabled_rule_collection_ids"], + }, + "log_only_rule_collection_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Description: schemaDescriptions["waf_log_only_rule_collection_ids"], + }, + }, + }, "backend": schema.SingleNestedAttribute{ Required: true, Description: schemaDescriptions["config_backend"], @@ -674,7 +839,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe UnhandledUnknownAsEmpty: false, }) if diags.HasError() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping config") + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping plan config") return } @@ -689,12 +854,9 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } // blockedCountries - // Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change). var blockedCountries []string if configModel.BlockedCountries != nil { - // Use a temporary slice tempBlockedCountries := []string{} - for _, blockedCountry := range *configModel.BlockedCountries { validatedBlockedCountry, err := validateCountryCode(blockedCountry) if err != nil { @@ -703,8 +865,6 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry) } - - // Point to the populated slice blockedCountries = tempBlockedCountries } @@ -759,6 +919,41 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Redirects: redirectsConfig, } + configPatch.Waf = &cdnSdk.WafConfigPatch{ + Mode: new(cdnSdk.WAFMODE_DISABLED), + Type: new(cdnSdk.WAFTYPE_FREE), + } + + // Map WAF Update + if !utils.IsUndefined(configModel.Waf) { + var wafModel wafConfig + diags := configModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + + configPatch.Waf.Mode = new(cdnSdk.WafMode(wafModel.Mode.ValueString())) + configPatch.Waf.Type = new(cdnSdk.WafType(wafModel.Type.ValueString())) + configPatch.Waf.AllowedHttpVersions = conversion.TerraformStringSetToList(ctx, wafModel.AllowedHttpVersions, &diags) + configPatch.Waf.AllowedRequestContentTypes = conversion.TerraformStringSetToList(ctx, wafModel.AllowedRequestContentTypes, &diags) + configPatch.Waf.AllowedHttpMethods = conversion.TerraformStringSetToList(ctx, wafModel.AllowedHttpMethods, &diags) + configPatch.Waf.EnabledRuleIds = conversion.TerraformStringSetToList(ctx, wafModel.EnabledRuleIds, &diags) + configPatch.Waf.DisabledRuleIds = conversion.TerraformStringSetToList(ctx, wafModel.DisabledRuleIds, &diags) + configPatch.Waf.LogOnlyRuleIds = conversion.TerraformStringSetToList(ctx, wafModel.LogOnlyRuleIds, &diags) + configPatch.Waf.EnabledRuleGroupIds = conversion.TerraformStringSetToList(ctx, wafModel.EnabledRuleGroupIds, &diags) + configPatch.Waf.DisabledRuleGroupIds = conversion.TerraformStringSetToList(ctx, wafModel.DisabledRuleGroupIds, &diags) + configPatch.Waf.LogOnlyRuleGroupIds = conversion.TerraformStringSetToList(ctx, wafModel.LogOnlyRuleGroupIds, &diags) + configPatch.Waf.EnabledRuleCollectionIds = conversion.TerraformStringSetToList(ctx, wafModel.EnabledRuleCollectionIds, &diags) + configPatch.Waf.DisabledRuleCollectionIds = conversion.TerraformStringSetToList(ctx, wafModel.DisabledRuleCollectionIds, &diags) + configPatch.Waf.LogOnlyRuleCollectionIds = conversion.TerraformStringSetToList(ctx, wafModel.LogOnlyRuleCollectionIds, &diags) + + if diags.HasError() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") + return + } + + if !utils.IsUndefined(wafModel.ParanoiaLevel) { + configPatch.Waf.ParanoiaLevel = new(cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())) + } + } + if !utils.IsUndefined(configModel.Optimizer) { var optimizerModel optimizerConfig @@ -1117,6 +1312,41 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo return core.DiagsToError(diags) } + // Map Waf + var pl *string + if distribution.Config.Waf.ParanoiaLevel != nil { + pl = new(string(*distribution.Config.Waf.ParanoiaLevel)) + } + wafObjAttrs := map[string]attr.Value{ + "mode": types.StringValue(string(distribution.Config.Waf.Mode)), + "type": types.StringValue(string(distribution.Config.Waf.Type)), + "paranoia_level": types.StringPointerValue(pl), + "allowed_http_versions": conversion.StringListToSet(ctx, distribution.Config.Waf.AllowedHttpVersions, &diags), + "allowed_request_content_types": conversion.StringListToSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes, &diags), + "allowed_http_methods": conversion.StringListToSet(ctx, distribution.Config.Waf.AllowedHttpMethods, &diags), + "enabled_rule_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.EnabledRuleIds, &diags), + "disabled_rule_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.DisabledRuleIds, &diags), + "log_only_rule_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.LogOnlyRuleIds, &diags), + "enabled_rule_group_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds, &diags), + "disabled_rule_group_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds, &diags), + "log_only_rule_group_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds, &diags), + "enabled_rule_collection_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds, &diags), + "disabled_rule_collection_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds, &diags), + "log_only_rule_collection_ids": conversion.StringListToSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds, &diags), + } + + if diags.HasError() { + return core.DiagsToError(diags) + } + + var wafVal attr.Value + + var diagWaf diag.Diagnostics + wafVal, diagWaf = types.ObjectValue(wafTypes, wafObjAttrs) + if diagWaf.HasError() { + return core.DiagsToError(diagWaf) + } + optimizerVal := types.ObjectNull(optimizerTypes) if o := distribution.Config.Optimizer; o != nil { optimizerEnabled, ok := o.GetEnabledOk() @@ -1136,6 +1366,7 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo "blocked_countries": modelBlockedCountries, "optimizer": optimizerVal, "redirects": redirectsVal, + "waf": wafVal, }) if diags.HasError() { return core.DiagsToError(diags) @@ -1187,14 +1418,26 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut if model == nil { return nil, fmt.Errorf("missing model") } + + var rawConfig distributionConfig + diags := model.Config.As(ctx, &rawConfig, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: false, + UnhandledUnknownAsEmpty: false, + }) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + cfg, err := convertConfig(ctx, model) if err != nil { return nil, err } + var optimizer *cdnSdk.Optimizer if cfg.Optimizer != nil { optimizer = cdnSdk.NewOptimizer(cfg.Optimizer.GetEnabled()) } + var backend *cdnSdk.CreateDistributionPayloadBackend if cfg.Backend.HttpBackend != nil { backend = &cdnSdk.CreateDistributionPayloadBackend{ @@ -1206,16 +1449,6 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut }, } } else if cfg.Backend.BucketBackend != nil { - // We need to parse the model again to access the credentials, - // as convertConfig returns the SDK Config struct which hides them. - var rawConfig distributionConfig - diags := model.Config.As(ctx, &rawConfig, basetypes.ObjectAsOptions{ - UnhandledNullAsEmpty: false, - UnhandledUnknownAsEmpty: false, - }) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } var accessKey, secretKey *string if rawConfig.Backend.Credentials != nil { accessKey = rawConfig.Backend.Credentials.AccessKey @@ -1233,6 +1466,13 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut }, } } + + // Conditionally set the WAF payload to nil if it's not defined + var wafPayload *cdnSdk.WafConfig + if !utils.IsUndefined(rawConfig.Waf) { + wafPayload = &cfg.Waf + } + payload := &cdnSdk.CreateDistributionPayload{ IntentId: new(uuid.NewString()), Regions: cfg.Regions, @@ -1240,6 +1480,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdnSdk.CreateDistribut BlockedCountries: cfg.BlockedCountries, Optimizer: optimizer, Redirects: cfg.Redirects, + Waf: wafPayload, // Now passes nil if omitted } return payload, nil @@ -1397,6 +1638,36 @@ func convertConfig(ctx context.Context, model *Model) (*cdnSdk.Config, error) { Redirects: redirectsConfig, } + if !utils.IsUndefined(configModel.Waf) { + var wafModel wafConfig + diags := configModel.Waf.As(ctx, &wafModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + + cdnConfig.Waf = cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode(wafModel.Mode.ValueString()), + Type: cdnSdk.WafType(wafModel.Type.ValueString()), + AllowedHttpVersions: conversion.TerraformStringSetToList(ctx, wafModel.AllowedHttpVersions, &diags), + AllowedRequestContentTypes: conversion.TerraformStringSetToList(ctx, wafModel.AllowedRequestContentTypes, &diags), + AllowedHttpMethods: conversion.TerraformStringSetToList(ctx, wafModel.AllowedHttpMethods, &diags), + EnabledRuleIds: conversion.TerraformStringSetToList(ctx, wafModel.EnabledRuleIds, &diags), + DisabledRuleIds: conversion.TerraformStringSetToList(ctx, wafModel.DisabledRuleIds, &diags), + LogOnlyRuleIds: conversion.TerraformStringSetToList(ctx, wafModel.LogOnlyRuleIds, &diags), + EnabledRuleGroupIds: conversion.TerraformStringSetToList(ctx, wafModel.EnabledRuleGroupIds, &diags), + DisabledRuleGroupIds: conversion.TerraformStringSetToList(ctx, wafModel.DisabledRuleGroupIds, &diags), + LogOnlyRuleGroupIds: conversion.TerraformStringSetToList(ctx, wafModel.LogOnlyRuleGroupIds, &diags), + EnabledRuleCollectionIds: conversion.TerraformStringSetToList(ctx, wafModel.EnabledRuleCollectionIds, &diags), + DisabledRuleCollectionIds: conversion.TerraformStringSetToList(ctx, wafModel.DisabledRuleCollectionIds, &diags), + LogOnlyRuleCollectionIds: conversion.TerraformStringSetToList(ctx, wafModel.LogOnlyRuleCollectionIds, &diags), + } + + if !utils.IsUndefined(wafModel.ParanoiaLevel) { + pl := cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString()) + cdnConfig.Waf.ParanoiaLevel = &pl + } + } + switch configModel.Backend.Type { case "http": originRequestHeaders := map[string]string{} diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 1501f872f..e16d5354d 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -26,6 +26,7 @@ func TestToCreatePayload(t *testing.T) { geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{ "https://de.mycoolapp.com": geofencingCountries, }) + backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{ "type": types.StringValue("http"), "origin_url": types.StringValue("https://www.mycoolapp.com"), @@ -42,6 +43,45 @@ func TestToCreatePayload(t *testing.T) { optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(true), }) + emptyWafSet := types.SetValueMust(types.StringType, []attr.Value{}) + expectedDefaultWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("DISABLED"), + Type: cdnSdk.WafType("FREE"), + AllowedHttpVersions: []string{}, + AllowedRequestContentTypes: []string{}, + AllowedHttpMethods: []string{}, + EnabledRuleIds: []string{}, + DisabledRuleIds: []string{}, + LogOnlyRuleIds: []string{}, + EnabledRuleGroupIds: []string{}, + DisabledRuleGroupIds: []string{}, + LogOnlyRuleGroupIds: []string{}, + EnabledRuleCollectionIds: []string{}, + DisabledRuleCollectionIds: []string{}, + LogOnlyRuleCollectionIds: []string{}, + } + defaultWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("DISABLED"), + "type": types.StringValue("FREE"), + "paranoia_level": types.StringNull(), + "allowed_http_versions": emptyWafSet, + "allowed_request_content_types": emptyWafSet, + "allowed_http_methods": emptyWafSet, + "enabled_rule_ids": emptyWafSet, + "disabled_rule_ids": emptyWafSet, + "log_only_rule_ids": emptyWafSet, + "enabled_rule_group_ids": emptyWafSet, + "disabled_rule_group_ids": emptyWafSet, + "log_only_rule_group_ids": emptyWafSet, + "enabled_rule_collection_ids": emptyWafSet, + "disabled_rule_collection_ids": emptyWafSet, + "log_only_rule_collection_ids": emptyWafSet, + }) + redirectsObjType, ok := configTypes["redirects"].(basetypes.ObjectType) + if !ok { + t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") + } + redirectsAttrTypes := redirectsObjType.AttrTypes config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, @@ -49,6 +89,7 @@ func TestToCreatePayload(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsTypes), + "waf": defaultWaf, }) matcherValues := types.ListValueMust(types.StringType, []attr.Value{ @@ -73,6 +114,46 @@ func TestToCreatePayload(t *testing.T) { redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{ "rules": rulesList, }) + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, + }) + + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -103,6 +184,7 @@ func TestToCreatePayload(t *testing.T) { Type: "http", }, }, + Waf: &expectedDefaultWafConfig, }, IsValid: true, }, @@ -114,12 +196,14 @@ func TestToCreatePayload(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsTypes), + "waf": defaultWaf, }) }), Expected: &cdnSdk.CreateDistributionPayload{ Regions: []cdnSdk.Region{"EU", "US"}, Optimizer: cdnSdk.NewOptimizer(true), BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: &expectedDefaultWafConfig, Backend: cdnSdk.CreateDistributionPayloadBackend{ HttpBackendCreate: &cdnSdk.HttpBackendCreate{ Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}}, @@ -139,11 +223,13 @@ func TestToCreatePayload(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigVal, + "waf": defaultWaf, }) }), Expected: &cdnSdk.CreateDistributionPayload{ Regions: []cdnSdk.Region{"EU", "US"}, BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: &expectedDefaultWafConfig, Backend: cdnSdk.CreateDistributionPayloadBackend{ HttpBackendCreate: &cdnSdk.HttpBackendCreate{ Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}}, @@ -193,9 +279,11 @@ func TestToCreatePayload(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsTypes), + "waf": defaultWaf, }) }), Expected: &cdnSdk.CreateDistributionPayload{ + Waf: &expectedDefaultWafConfig, Backend: cdnSdk.CreateDistributionPayloadBackend{ BucketBackendCreate: &cdnSdk.BucketBackendCreate{ Type: "bucket", @@ -212,6 +300,32 @@ func TestToCreatePayload(t *testing.T) { }, IsValid: true, }, + "happy_path_with_waf": { + Input: modelFixture(func(m *Model) { + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), + "blocked_countries": blockedCountriesFixture, + "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": populatedWaf, + }) + }), + Expected: &cdnSdk.CreateDistributionPayload{ + Regions: []cdnSdk.Region{"EU", "US"}, + BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: &expectedWafConfig, + Backend: cdnSdk.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdnSdk.HttpBackendCreate{ + Geofencing: &map[string][]string{"https://de.mycoolapp.com": {"DE", "FR"}}, + OriginRequestHeaders: &map[string]string{"testHeader0": "testHeaderValue0", "testHeader1": "testHeaderValue1"}, + OriginUrl: "https://www.mycoolapp.com", + Type: "http", + }, + }, + }, + IsValid: true, + }, "sad_path_model_nil": { Input: nil, Expected: nil, @@ -281,6 +395,7 @@ func TestConvertConfig(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) matcherValues := types.ListValueMust(types.StringType, []attr.Value{ @@ -301,10 +416,50 @@ func TestConvertConfig(t *testing.T) { "matchers": matchersList, }) rulesList := types.ListValueMust(types.ObjectType{AttrTypes: redirectRuleTypes}, []attr.Value{ruleVal}) + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) redirectsConfigVal := types.ObjectValueMust(redirectsTypes, map[string]attr.Value{ "rules": rulesList, }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, + }) + + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } modelFixture := func(mods ...func(*Model)) *Model { model := &Model{ @@ -352,6 +507,7 @@ func TestConvertConfig(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) }), Expected: &cdnSdk.Config{ @@ -373,6 +529,36 @@ func TestConvertConfig(t *testing.T) { BlockedCountries: []string{"XX", "YY", "ZZ"}, }, IsValid: true, + }, "happy_path_with_waf": { + Input: modelFixture(func(m *Model) { + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), + "blocked_countries": blockedCountriesFixture, + "redirects": types.ObjectNull(redirectsTypes), + "waf": populatedWaf, + }) + }), + Expected: &cdnSdk.Config{ + Backend: cdnSdk.ConfigBackend{ + HttpBackend: &cdnSdk.HttpBackend{ + OriginRequestHeaders: map[string]string{ + "testHeader0": "testHeaderValue0", + "testHeader1": "testHeaderValue1", + }, + OriginUrl: "https://www.mycoolapp.com", + Type: "http", + Geofencing: map[string][]string{ + "https://de.mycoolapp.com": {"DE", "FR"}, + }, + }, + }, + Regions: []cdnSdk.Region{"EU", "US"}, + BlockedCountries: []string{"XX", "YY", "ZZ"}, + Waf: expectedWafConfig, + }, + IsValid: true, }, "happy_path_with_redirects": { Input: modelFixture(func(m *Model) { @@ -382,6 +568,7 @@ func TestConvertConfig(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigVal, + "waf": types.ObjectNull(wafTypes), }) }), Expected: &cdnSdk.Config{ @@ -441,6 +628,7 @@ func TestConvertConfig(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsTypes), + "waf": types.ObjectNull(wafTypes), }) }), Expected: &cdnSdk.Config{ @@ -533,13 +721,70 @@ func TestMapFields(t *testing.T) { t.Fatalf("configTypes[\"redirects\"] is not of type basetypes.ObjectType") } redirectsAttrTypes := redirectsObjType.AttrTypes + populatedWafSet := types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("rule1"), + types.StringValue("rule2"), + }) + populatedWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("ENABLED"), + "type": types.StringValue("PREMIUM"), + "paranoia_level": types.StringValue("L2"), + "allowed_http_versions": populatedWafSet, + "allowed_request_content_types": populatedWafSet, + "allowed_http_methods": populatedWafSet, + "enabled_rule_ids": populatedWafSet, + "disabled_rule_ids": populatedWafSet, + "log_only_rule_ids": populatedWafSet, + "enabled_rule_group_ids": populatedWafSet, + "disabled_rule_group_ids": populatedWafSet, + "log_only_rule_group_ids": populatedWafSet, + "enabled_rule_collection_ids": populatedWafSet, + "disabled_rule_collection_ids": populatedWafSet, + "log_only_rule_collection_ids": populatedWafSet, + }) + expectedParanoiaLevel := cdnSdk.WafParanoiaLevel("L2") + expectedWafConfig := cdnSdk.WafConfig{ + Mode: cdnSdk.WafMode("ENABLED"), + Type: cdnSdk.WafType("PREMIUM"), + ParanoiaLevel: &expectedParanoiaLevel, + AllowedHttpVersions: []string{"rule1", "rule2"}, + AllowedRequestContentTypes: []string{"rule1", "rule2"}, + AllowedHttpMethods: []string{"rule1", "rule2"}, + EnabledRuleIds: []string{"rule1", "rule2"}, + DisabledRuleIds: []string{"rule1", "rule2"}, + LogOnlyRuleIds: []string{"rule1", "rule2"}, + EnabledRuleGroupIds: []string{"rule1", "rule2"}, + DisabledRuleGroupIds: []string{"rule1", "rule2"}, + LogOnlyRuleGroupIds: []string{"rule1", "rule2"}, + EnabledRuleCollectionIds: []string{"rule1", "rule2"}, + DisabledRuleCollectionIds: []string{"rule1", "rule2"}, + LogOnlyRuleCollectionIds: []string{"rule1", "rule2"}, + } + defaultWaf := types.ObjectValueMust(wafTypes, map[string]attr.Value{ + "mode": types.StringValue("DISABLED"), + "type": types.StringValue("FREE"), + "paranoia_level": types.StringNull(), + "allowed_http_versions": types.SetNull(types.StringType), + "allowed_request_content_types": types.SetNull(types.StringType), + "allowed_http_methods": types.SetNull(types.StringType), + "enabled_rule_ids": types.SetNull(types.StringType), + "disabled_rule_ids": types.SetNull(types.StringType), + "log_only_rule_ids": types.SetNull(types.StringType), + "enabled_rule_group_ids": types.SetNull(types.StringType), + "disabled_rule_group_ids": types.SetNull(types.StringType), + "log_only_rule_group_ids": types.SetNull(types.StringType), + "enabled_rule_collection_ids": types.SetNull(types.StringType), + "disabled_rule_collection_ids": types.SetNull(types.StringType), + "log_only_rule_collection_ids": types.SetNull(types.StringType), + }) config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, }) redirectsInput := &cdnSdk.RedirectConfig{ @@ -624,6 +869,10 @@ func TestMapFields(t *testing.T) { Regions: []cdnSdk.Region{"EU", "US"}, BlockedCountries: []string{"XX", "YY", "ZZ"}, Optimizer: nil, + Waf: cdnSdk.WafConfig{ + Mode: cdnSdk.WAFMODE_DISABLED, + Type: cdnSdk.WAFTYPE_FREE, + }, }, CreatedAt: createdAt, Domains: []cdnSdk.Domain{ @@ -663,6 +912,7 @@ func TestMapFields(t *testing.T) { "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) tests := map[string]struct { Input *cdnSdk.Distribution @@ -683,6 +933,7 @@ func TestMapFields(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -709,6 +960,7 @@ func TestMapFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -724,6 +976,7 @@ func TestMapFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigExpected, + "waf": defaultWaf, }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -739,6 +992,21 @@ func TestMapFields(t *testing.T) { d.Status = "ERROR" }), IsValid: true, + }, "happy_path_with_waf": { + Expected: expectedModel(func(m *Model) { + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": backend, + "regions": regionsFixture, + "optimizer": types.ObjectNull(optimizerTypes), + "blocked_countries": blockedCountriesFixture, + "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": populatedWaf, + }) + }), + Input: distributionFixture(func(d *cdnSdk.Distribution) { + d.Config.Waf = expectedWafConfig + }), + IsValid: true, }, "happy_path_custom_domain": { Expected: expectedModel(func(m *Model) { @@ -787,7 +1055,14 @@ func TestMapFields(t *testing.T) { m.Config = configOld }), Expected: expectedModel(func(m *Model) { - m.Config = configOld + m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ + "backend": bucketBackendOld, + "regions": regionsFixture, + "blocked_countries": blockedCountriesFixture, + "optimizer": types.ObjectNull(optimizerTypes), + "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": defaultWaf, + }) }), IsValid: true, }, diff --git a/stackit/internal/services/cdn/testdata/resource-http-base.tf b/stackit/internal/services/cdn/testdata/resource-http-base.tf index 9777ee95c..9373493c9 100644 --- a/stackit/internal/services/cdn/testdata/resource-http-base.tf +++ b/stackit/internal/services/cdn/testdata/resource-http-base.tf @@ -16,6 +16,21 @@ variable "redirect_rule_enabled" {} variable "redirect_rule_match_condition" {} variable "redirect_matcher_value" {} variable "redirect_matcher_condition" {} +variable "waf_mode" {} +variable "waf_type" {} +variable "waf_allowed_http_methods_0" {} +variable "waf_allowed_request_content_types_0" {} +variable "waf_allowed_http_versions_0" {} +variable "waf_paranoia_level" {} +variable "waf_enabled_rule_ids_0" {} +variable "waf_disabled_rule_ids_0" {} +variable "waf_log_only_rule_ids_0" {} +variable "waf_enabled_rule_group_ids_0" {} +variable "waf_disabled_rule_group_ids_0" {} +variable "waf_log_only_rule_group_ids_0" {} +variable "waf_enabled_rule_collection_ids_0" {} +variable "waf_disabled_rule_collection_ids_0" {} +variable "waf_log_only_rule_collection_ids_0" {} # dns variable "dns_zone_name" {} @@ -63,6 +78,23 @@ resource "stackit_cdn_distribution" "distribution" { } ] } + waf = { + mode = var.waf_mode + type = var.waf_type + allowed_http_methods = [var.waf_allowed_http_methods_0] + allowed_request_content_types = [var.waf_allowed_request_content_types_0] + allowed_http_versions = [var.waf_allowed_http_versions_0] + paranoia_level = var.waf_paranoia_level + disabled_rule_ids = [var.waf_disabled_rule_ids_0] + enabled_rule_ids = [var.waf_enabled_rule_ids_0] + log_only_rule_ids = [var.waf_log_only_rule_ids_0] + disabled_rule_group_ids = [var.waf_disabled_rule_group_ids_0] + enabled_rule_group_ids = [var.waf_enabled_rule_group_ids_0] + log_only_rule_group_ids = [var.waf_log_only_rule_group_ids_0] + disabled_rule_collection_ids = [var.waf_disabled_rule_collection_ids_0] + enabled_rule_collection_ids = [var.waf_enabled_rule_collection_ids_0] + log_only_rule_collection_ids = [var.waf_log_only_rule_collection_ids_0] + } backend = { type = var.backend_http_type origin_url = var.backend_origin_url