diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 099a24799..d6ab74648 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) Configuration of the Web Application Firewall (WAF) for the distribution. Removing this block from your configuration will completely disable the WAF. (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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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'. Defaults to '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..ea17a0dab 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) Configuration of the Web Application Firewall (WAF) for the distribution. Removing this block from your configuration will completely disable the WAF. (see [below for nested schema](#nestedatt--config--waf)) ### Nested Schema for `config.backend` @@ -186,6 +220,31 @@ Optional: + +### Nested Schema for `config.waf` + +Required: + +- `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'. +- `type` (String) The tier of the WAF. Valid values are 'FREE' or 'PREMIUM'. Defaults to 'FREE'. + +Optional: + +- `allowed_http_methods` (Set of String) Restricts which HTTP methods the distribution accepts. If provided, the set must contain at least one item. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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 +- `paranoia_level` (String) Defines how aggressively the WAF should act on requests. Valid values are 'L1' to 'L4'. Defaults to 'L1'. + + ### 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/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index f7be85668..67b4280e6 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -100,12 +100,20 @@ var testConfigVarsHttp = config.Variables{ "redirect_rule_enabled": config.BoolVariable(true), "redirect_rule_match_condition": config.StringVariable("ANY"), "redirect_matcher_condition": config.StringVariable("ANY"), + "waf_mode": config.StringVariable("LOG_ONLY"), + "waf_type": config.StringVariable("PREMIUM"), + "waf_enabled_rule_ids": config.ListVariable(config.StringVariable("@builtin/crs/request/941120")), } 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("ENABLED") + updatedConfig["waf_enabled_rule_ids"] = config.ListVariable(config.StringVariable("@builtin/crs/request/941120"), config.StringVariable("@builtin/crs/request/941130")) + return updatedConfig } @@ -190,6 +198,12 @@ 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.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", "@builtin/crs/request/941120"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), ), @@ -233,7 +247,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 +306,13 @@ 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.enabled_rule_ids.#", "1"), + resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", "@builtin/crs/request/941120"), + 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 +352,14 @@ 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", "ENABLED"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.type", testutil.ConvertConfigVariable(testConfigVarsHttp["waf_type"])), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.#", "2"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.0", "@builtin/crs/request/941120"), + resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.waf.enabled_rule_ids.1", "@builtin/crs/request/941130"), + 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..c0b2616bc 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 + wafVal := types.ObjectNull(wafTypes) + if distribution.Config.Waf.Mode != "" { + wafObjAttrs := map[string]attr.Value{ + "mode": types.StringValue(string(distribution.Config.Waf.Mode)), + "type": types.StringValue(string(distribution.Config.Waf.Type)), + } + + if distribution.Config.Waf.ParanoiaLevel != nil { + wafObjAttrs["paranoia_level"] = types.StringValue(string(*distribution.Config.Waf.ParanoiaLevel)) + } else { + wafObjAttrs["paranoia_level"] = types.StringNull() + } + + wafObjAttrs["allowed_http_versions"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) + + 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..ab2b5515f 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": "Configuration of the Web Application Firewall (WAF) for the distribution. Removing this block from your configuration will completely disable the WAF.", + "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'. Defaults to '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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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,109 @@ 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{ + Required: true, + Description: schemaDescriptions["waf_mode"], + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafModeEnumValues)...)}, + }, + "type": schema.StringAttribute{ + Required: true, + Description: schemaDescriptions["waf_type"], + Validators: []validator.String{stringvalidator.OneOf(sdkUtils.EnumSliceToStringSlice(cdnSdk.AllowedWafTypeEnumValues)...)}, + }, + "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, + 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"], @@ -668,18 +828,18 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "distribution_id", distributionId) - configModel := distributionConfig{} - diags = model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{ + configmodel := distributionConfig{} + diags = model.Config.As(ctx, &configmodel, basetypes.ObjectAsOptions{ UnhandledNullAsEmpty: false, 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 } regions := []cdnSdk.Region{} - for _, r := range *configModel.Regions { + for _, r := range *configmodel.Regions { regionEnum, err := cdnSdk.NewRegionFromValue(r) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Map regions: %v", err)) @@ -689,13 +849,10 @@ 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 + if configmodel.BlockedCountries != nil { tempBlockedCountries := []string{} - - for _, blockedCountry := range *configModel.BlockedCountries { + for _, blockedCountry := range *configmodel.BlockedCountries { validatedBlockedCountry, err := validateCountryCode(blockedCountry) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err)) @@ -703,22 +860,20 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry) } - - // Point to the populated slice blockedCountries = tempBlockedCountries } // redirects - redirectsConfig := convertRedirectconfig(configModel.Redirects) + redirectsConfig := convertRedirectconfig(configmodel.Redirects) configPatchBackend := &cdnSdk.ConfigPatchBackend{} - switch configModel.Backend.Type { + switch configmodel.Backend.Type { case "http": geofencingPatch := map[string][]string{} - if configModel.Backend.Geofencing != nil { + if configmodel.Backend.Geofencing != nil { gf := make(map[string][]string) - for url, countries := range *configModel.Backend.Geofencing { + for url, countries := range *configmodel.Backend.Geofencing { countryStrings := make([]string, len(countries)) for i, countryPtr := range countries { if countryPtr == nil { @@ -733,21 +888,21 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } configPatchBackend.HttpBackendPatch = &cdnSdk.HttpBackendPatch{ - OriginRequestHeaders: configModel.Backend.OriginRequestHeaders, - OriginUrl: configModel.Backend.OriginURL, + OriginRequestHeaders: configmodel.Backend.OriginRequestHeaders, + OriginUrl: configmodel.Backend.OriginURL, Type: "http", Geofencing: &geofencingPatch, } case "bucket": configPatchBackend.BucketBackendPatch = &cdnSdk.BucketBackendPatch{ Type: "bucket", - BucketUrl: configModel.Backend.BucketURL, - Region: configModel.Backend.Region, + BucketUrl: configmodel.Backend.BucketURL, + Region: configmodel.Backend.Region, } - if configModel.Backend.Credentials != nil { + if configmodel.Backend.Credentials != nil { configPatchBackend.BucketBackendPatch.Credentials = &cdnSdk.BucketCredentials{ - AccessKeyId: *configModel.Backend.Credentials.AccessKey, - SecretAccessKey: *configModel.Backend.Credentials.SecretKey, + AccessKeyId: *configmodel.Backend.Credentials.AccessKey, + SecretAccessKey: *configmodel.Backend.Credentials.SecretKey, } } } @@ -759,10 +914,43 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe Redirects: redirectsConfig, } - if !utils.IsUndefined(configModel.Optimizer) { + 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{}) + if diags.HasError() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping WAF config") + return + } + configPatch.Waf.Mode = new(cdnSdk.WafMode(wafModel.Mode.ValueString())) + configPatch.Waf.Type = new(cdnSdk.WafType(wafModel.Type.ValueString())) + configPatch.Waf.AllowedHttpVersions = getWafSet(ctx, wafModel.AllowedHttpVersions) + configPatch.Waf.AllowedRequestContentTypes = getWafSet(ctx, wafModel.AllowedRequestContentTypes) + configPatch.Waf.AllowedHttpMethods = getWafSet(ctx, wafModel.AllowedHttpMethods) + configPatch.Waf.EnabledRuleIds = getWafSet(ctx, wafModel.EnabledRuleIds) + configPatch.Waf.DisabledRuleIds = getWafSet(ctx, wafModel.DisabledRuleIds) + configPatch.Waf.LogOnlyRuleIds = getWafSet(ctx, wafModel.LogOnlyRuleIds) + configPatch.Waf.EnabledRuleGroupIds = getWafSet(ctx, wafModel.EnabledRuleGroupIds) + configPatch.Waf.DisabledRuleGroupIds = getWafSet(ctx, wafModel.DisabledRuleGroupIds) + configPatch.Waf.LogOnlyRuleGroupIds = getWafSet(ctx, wafModel.LogOnlyRuleGroupIds) + configPatch.Waf.EnabledRuleCollectionIds = getWafSet(ctx, wafModel.EnabledRuleCollectionIds) + configPatch.Waf.DisabledRuleCollectionIds = getWafSet(ctx, wafModel.DisabledRuleCollectionIds) + configPatch.Waf.LogOnlyRuleCollectionIds = getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds) + + if !utils.IsUndefined(wafModel.ParanoiaLevel) { + configPatch.Waf.ParanoiaLevel = new(cdnSdk.WafParanoiaLevel(wafModel.ParanoiaLevel.ValueString())) + } + } + + if !utils.IsUndefined(configmodel.Optimizer) { var optimizerModel optimizerConfig - diags = configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) + diags = configmodel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) if diags.HasError() { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping optimizer config") return @@ -1117,6 +1305,57 @@ func mapFields(ctx context.Context, distribution *cdnSdk.Distribution, model *Mo return core.DiagsToError(diags) } + // Map Waf + wafObjAttrs := map[string]attr.Value{ + "mode": types.StringValue(string(distribution.Config.Waf.Mode)), + "type": types.StringValue(string(distribution.Config.Waf.Type)), + } + + // Detect if we are running an Import (or Data Source Read) where prior config doesn't exist + isImport := utils.IsUndefined(model.Config) + + // Helper to unconditionally map string fields + mapWafString := func(apiVal *string) types.String { + if apiVal != nil { + return types.StringPointerValue(apiVal) + } + return types.StringNull() + } + + var pl *string + if distribution.Config.Waf.ParanoiaLevel != nil { + pl = new(string(*distribution.Config.Waf.ParanoiaLevel)) + } + wafObjAttrs["paranoia_level"] = mapWafString(pl) + wafObjAttrs["allowed_http_versions"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpVersions) + wafObjAttrs["allowed_request_content_types"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedRequestContentTypes) + wafObjAttrs["allowed_http_methods"] = mustMapStringSet(ctx, distribution.Config.Waf.AllowedHttpMethods) + wafObjAttrs["enabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleIds) + wafObjAttrs["disabled_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleIds) + wafObjAttrs["log_only_rule_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleIds) + wafObjAttrs["enabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleGroupIds) + wafObjAttrs["disabled_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleGroupIds) + wafObjAttrs["log_only_rule_group_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleGroupIds) + wafObjAttrs["enabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.EnabledRuleCollectionIds) + wafObjAttrs["disabled_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.DisabledRuleCollectionIds) + wafObjAttrs["log_only_rule_collection_ids"] = mustMapStringSet(ctx, distribution.Config.Waf.LogOnlyRuleCollectionIds) + + // Determine if WAF should be entirely excluded to prevent drift. + // The API can return an empty string for fully unconfigured backends. + isWafDisabled := (distribution.Config.Waf.Mode == cdnSdk.WAFMODE_DISABLED) && + (distribution.Config.Waf.Type == cdnSdk.WAFTYPE_FREE) + + var wafVal attr.Value + if isWafDisabled && (isImport || utils.IsUndefined(oldConfig.Waf)) { + wafVal = types.ObjectNull(wafTypes) + } else { + 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 +1375,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 +1427,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 +1458,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 +1475,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 +1489,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 +1647,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: getWafSet(ctx, wafModel.AllowedHttpVersions), + AllowedRequestContentTypes: getWafSet(ctx, wafModel.AllowedRequestContentTypes), + AllowedHttpMethods: getWafSet(ctx, wafModel.AllowedHttpMethods), + EnabledRuleIds: getWafSet(ctx, wafModel.EnabledRuleIds), + DisabledRuleIds: getWafSet(ctx, wafModel.DisabledRuleIds), + LogOnlyRuleIds: getWafSet(ctx, wafModel.LogOnlyRuleIds), + EnabledRuleGroupIds: getWafSet(ctx, wafModel.EnabledRuleGroupIds), + DisabledRuleGroupIds: getWafSet(ctx, wafModel.DisabledRuleGroupIds), + LogOnlyRuleGroupIds: getWafSet(ctx, wafModel.LogOnlyRuleGroupIds), + EnabledRuleCollectionIds: getWafSet(ctx, wafModel.EnabledRuleCollectionIds), + DisabledRuleCollectionIds: getWafSet(ctx, wafModel.DisabledRuleCollectionIds), + LogOnlyRuleCollectionIds: getWafSet(ctx, wafModel.LogOnlyRuleCollectionIds), + } + + 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{} @@ -1453,3 +1733,27 @@ func validateCountryCode(country string) (string, error) { return upperCountry, nil } + +// getWafSet extracts strings from HCL set +func getWafSet(ctx context.Context, tfSet basetypes.SetValue) []string { + if utils.IsUndefined(tfSet) { + return nil + } + var elements []string + diags := tfSet.ElementsAs(ctx, &elements, true) + if diags.HasError() { + return []string{} + } + return elements +} + +// Helper to unconditionally map set fields +func mustMapStringSet(ctx context.Context, apiList []string) types.Set { + if apiList != nil { + setVal, diags := types.SetValueFrom(ctx, types.StringType, apiList) + if !diags.HasError() { + return setVal + } + } + return types.SetNull(types.StringType) +} diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 1501f872f..f95eda7d0 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,53 @@ 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"}, + } config := types.ObjectValueMust(configTypes, map[string]attr.Value{ "backend": backend, "regions": regionsFixture, "blocked_countries": blockedCountriesFixture, "optimizer": types.ObjectNull(optimizerTypes), "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) redirectsInput := &cdnSdk.RedirectConfig{ @@ -624,6 +852,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 +895,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 +916,7 @@ func TestMapFields(t *testing.T) { "optimizer": optimizer, "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -709,6 +943,7 @@ func TestMapFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": types.ObjectNull(redirectsAttrTypes), + "waf": types.ObjectNull(wafTypes), // <-- Change this }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -724,6 +959,7 @@ func TestMapFields(t *testing.T) { "optimizer": types.ObjectNull(optimizerTypes), "blocked_countries": blockedCountriesFixture, "redirects": redirectsConfigExpected, + "waf": types.ObjectNull(wafTypes), // <-- Change this }) }), Input: distributionFixture(func(d *cdnSdk.Distribution) { @@ -739,6 +975,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) { diff --git a/stackit/internal/services/cdn/testdata/resource-http-base.tf b/stackit/internal/services/cdn/testdata/resource-http-base.tf index 9777ee95c..07045d9f6 100644 --- a/stackit/internal/services/cdn/testdata/resource-http-base.tf +++ b/stackit/internal/services/cdn/testdata/resource-http-base.tf @@ -16,6 +16,9 @@ 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_enabled_rule_ids" {} # dns variable "dns_zone_name" {} @@ -63,6 +66,11 @@ resource "stackit_cdn_distribution" "distribution" { } ] } + waf = { + mode = var.waf_mode + type = var.waf_type + enabled_rule_ids = var.waf_enabled_rule_ids + } backend = { type = var.backend_http_type origin_url = var.backend_origin_url