diff --git a/datadog/internal/validators/validators.go b/datadog/internal/validators/validators.go index 5a91fe4aae..8960b45963 100644 --- a/datadog/internal/validators/validators.go +++ b/datadog/internal/validators/validators.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" @@ -304,26 +305,41 @@ func Float64Between(min, max float64) validator.String { } } -func ValidateHeaders(val any, p cty.Path) diag.Diagnostics { - var diags diag.Diagnostics - value := val.(map[string]interface{}) - for _, v := range value { - valueString, ok := v.(string) +func ValidateHttpRequestHeader(v interface{}, k string) (ws []string, errors []error) { + value := v.(map[string]interface{}) + for headerField, headerValue := range value { + if !isValidToken(headerField) { + errors = append(errors, fmt.Errorf("invalid value for %s (header field must be a valid token)", k)) + return + } + headerStringValue, ok := headerValue.(string) if !ok { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Invalid value", - Detail: "Synthetics test header must be a string", - AttributePath: p, - }) - } else if strings.Contains(valueString, "\n") || strings.Contains(valueString, "\r\n") { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Invalid value", - Detail: "Synthetics test header must not contain newline characters", - AttributePath: p, - }) + errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) + return + } else { + for _, r := range headerStringValue { + if (unicode.IsControl(r) && r != '\t') || (r == '\r' || r == '\n') { + errors = append(errors, fmt.Errorf("invalid value for %s (header value must not contain invisible characters)", k)) + return + } + } } } - return diags + return +} + +func isValidToken(token string) bool { + for _, r := range token { + if !isTokenChar(r) { + return false + } + } + return true +} + +func isTokenChar(r rune) bool { + if r >= '!' && r <= '~' && !strings.ContainsRune("()<>@,;:\\\"/[]?={} \t", r) { + return true + } + return false } diff --git a/datadog/resource_datadog_synthetics_test_.go b/datadog/resource_datadog_synthetics_test_.go index f0a9f3c6a9..d2e52b0d51 100644 --- a/datadog/resource_datadog_synthetics_test_.go +++ b/datadog/resource_datadog_synthetics_test_.go @@ -259,10 +259,10 @@ func syntheticsTestRequest() *schema.Resource { func syntheticsTestRequestHeaders() *schema.Schema { return &schema.Schema{ - Description: "Header name and value map.", - Type: schema.TypeMap, - ValidateDiagFunc: validators.ValidateHeaders, - Optional: true, + Description: "Header name and value map.", + Type: schema.TypeMap, + Optional: true, + ValidateFunc: validators.ValidateHttpRequestHeader, } } diff --git a/docs/resources/synthetics_test.md b/docs/resources/synthetics_test.md index 88418ebc6d..dd786d4b46 100644 --- a/docs/resources/synthetics_test.md +++ b/docs/resources/synthetics_test.md @@ -466,7 +466,7 @@ resource "datadog_synthetics_test" "grpc" { - `request_client_certificate` (Block List, Max: 1) Client certificate to use when performing the test request. Exactly one nested block is allowed with the structure below. (see [below for nested schema](#nestedblock--request_client_certificate)) - `request_definition` (Block List, Max: 1) Required if `type = "api"`. The synthetics test request. (see [below for nested schema](#nestedblock--request_definition)) - `request_file` (Block List) Files to be used as part of the request in the test. (see [below for nested schema](#nestedblock--request_file)) -- `request_headers` (Map of String) +- `request_headers` (Map of String) Header name and value map. - `request_metadata` (Map of String) Metadata to include when performing the gRPC test. - `request_proxy` (Block List, Max: 1) The proxy to perform the test. (see [below for nested schema](#nestedblock--request_proxy)) - `request_query` (Map of String) Query arguments name and value map. @@ -497,7 +497,7 @@ Optional: - `request_client_certificate` (Block List, Max: 1) Client certificate to use when performing the test request. Exactly one nested block is allowed with the structure below. (see [below for nested schema](#nestedblock--api_step--request_client_certificate)) - `request_definition` (Block List, Max: 1) The request for the api step. (see [below for nested schema](#nestedblock--api_step--request_definition)) - `request_file` (Block List) Files to be used as part of the request in the test. (see [below for nested schema](#nestedblock--api_step--request_file)) -- `request_headers` (Map of String) +- `request_headers` (Map of String) Header name and value map. - `request_proxy` (Block List, Max: 1) The proxy to perform the test. (see [below for nested schema](#nestedblock--api_step--request_proxy)) - `request_query` (Map of String) Query arguments name and value map. - `retry` (Block List, Max: 1) (see [below for nested schema](#nestedblock--api_step--retry)) @@ -702,7 +702,7 @@ Required: Optional: -- `headers` (Map of String) +- `headers` (Map of String) Header name and value map. @@ -1082,7 +1082,7 @@ Required: Optional: -- `headers` (Map of String) +- `headers` (Map of String) Header name and value map. ## Import