From 0d4aa69757f16b69b270c059bb45e620279adb9e Mon Sep 17 00:00:00 2001 From: Mike Ball Date: Tue, 15 Apr 2025 20:53:23 -0400 Subject: [PATCH] feat: add `sensitive_request_headers` argument This seeks to address issue #34 via `sensitive_request_headers`, which serves the same function as `request_headers`, but are denoted as sensitive and their values are obscured as `*****` in Terraform state. Signed-off-by: Mike Ball --- docs/data-sources/http.md | 1 + internal/provider/data_source_http.go | 65 +++++++++++++----- internal/provider/data_source_http_test.go | 77 ++++++++++++++++++++++ 3 files changed, 127 insertions(+), 16 deletions(-) diff --git a/docs/data-sources/http.md b/docs/data-sources/http.md index 23ddb4e2..cf64ffea 100644 --- a/docs/data-sources/http.md +++ b/docs/data-sources/http.md @@ -157,6 +157,7 @@ resource "null_resource" "example" { - `request_headers` (Map of String) A map of request header field names and values. - `request_timeout_ms` (Number) The request timeout in milliseconds. - `retry` (Block, Optional) Retry request configuration. By default there are no retries. Configuring this block will result in retries if an error is returned by the client (e.g., connection errors) or if a 5xx-range (except 501) status code is received. For further details see [go-retryablehttp](https://pkg.go.dev/github.com/hashicorp/go-retryablehttp). (see [below for nested schema](#nestedblock--retry)) +- `sensitive_request_headers` (Map of String, Sensitive) A map of request header field names and values regarded as sensitive. The header values are masked in CLI output, as well as Terraform state. ### Read-Only diff --git a/internal/provider/data_source_http.go b/internal/provider/data_source_http.go index 0ab7b095..89cd2d48 100644 --- a/internal/provider/data_source_http.go +++ b/internal/provider/data_source_http.go @@ -98,6 +98,14 @@ a 5xx-range (except 501) status code is received. For further details see Optional: true, }, + "sensitive_request_headers": schema.MapAttribute{ + Description: "A map of request header field names and values regarded as sensitive. " + + "The header values are masked in CLI output, as well as Terraform state.", + ElementType: types.StringType, + Optional: true, + Sensitive: true, + }, + "request_body": schema.StringAttribute{ Description: "The request body as a string.", Optional: true, @@ -218,6 +226,7 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r requestURL := model.URL.ValueString() method := model.Method.ValueString() requestHeaders := model.RequestHeaders + sensitiveRequestHeaders := model.SensitiveRequestHeaders if method == "" { method = "GET" @@ -350,6 +359,20 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r } } + for name, value := range sensitiveRequestHeaders.Elements() { + var header string + diags = tfsdk.ValueAs(ctx, value, &header) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + request.Header.Set(name, header) + if strings.ToLower(name) == "host" { + request.Host = header + } + } + response, err := retryClient.Do(request) if err != nil { target := &url.Error{} @@ -416,27 +439,37 @@ func (d *httpDataSource) Read(ctx context.Context, req datasource.ReadRequest, r model.ResponseBodyBase64 = types.StringValue(responseBodyBase64Std) model.StatusCode = types.Int64Value(int64(response.StatusCode)) + // Override SensitiveRequestHeaders' header values with a mask. + sensitiveHeaders := map[string]string{} + for name, _ := range sensitiveRequestHeaders.Elements() { + sensitiveHeaders[name] = "*****" + } + + maskedSensitiveHeaders, _ := types.MapValueFrom(ctx, types.StringType, sensitiveHeaders) + model.SensitiveRequestHeaders = maskedSensitiveHeaders + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) } type modelV0 struct { - ID types.String `tfsdk:"id"` - URL types.String `tfsdk:"url"` - Method types.String `tfsdk:"method"` - RequestHeaders types.Map `tfsdk:"request_headers"` - RequestBody types.String `tfsdk:"request_body"` - RequestTimeout types.Int64 `tfsdk:"request_timeout_ms"` - Retry types.Object `tfsdk:"retry"` - ResponseHeaders types.Map `tfsdk:"response_headers"` - CaCertificate types.String `tfsdk:"ca_cert_pem"` - ClientCert types.String `tfsdk:"client_cert_pem"` - ClientKey types.String `tfsdk:"client_key_pem"` - Insecure types.Bool `tfsdk:"insecure"` - ResponseBody types.String `tfsdk:"response_body"` - Body types.String `tfsdk:"body"` - ResponseBodyBase64 types.String `tfsdk:"response_body_base64"` - StatusCode types.Int64 `tfsdk:"status_code"` + ID types.String `tfsdk:"id"` + URL types.String `tfsdk:"url"` + Method types.String `tfsdk:"method"` + RequestHeaders types.Map `tfsdk:"request_headers"` + SensitiveRequestHeaders types.Map `tfsdk:"sensitive_request_headers"` + RequestBody types.String `tfsdk:"request_body"` + RequestTimeout types.Int64 `tfsdk:"request_timeout_ms"` + Retry types.Object `tfsdk:"retry"` + ResponseHeaders types.Map `tfsdk:"response_headers"` + CaCertificate types.String `tfsdk:"ca_cert_pem"` + ClientCert types.String `tfsdk:"client_cert_pem"` + ClientKey types.String `tfsdk:"client_key_pem"` + Insecure types.Bool `tfsdk:"insecure"` + ResponseBody types.String `tfsdk:"response_body"` + Body types.String `tfsdk:"body"` + ResponseBodyBase64 types.String `tfsdk:"response_body_base64"` + StatusCode types.Int64 `tfsdk:"status_code"` } type retryModel struct { diff --git a/internal/provider/data_source_http_test.go b/internal/provider/data_source_http_test.go index 10dab105..07f0028e 100644 --- a/internal/provider/data_source_http_test.go +++ b/internal/provider/data_source_http_test.go @@ -149,6 +149,83 @@ func TestDataSource_withAuthorizationRequestHeader_200(t *testing.T) { }) } +func TestDataSource_withSensitiveAuthorizationRequestHeader_200(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") == "Zm9vOmJhcg==" { + w.Header().Set("Content-Type", "text/plain") + _, err := w.Write([]byte("1.0.0")) + if err != nil { + t.Errorf("error writing body: %s", err) + } + } else { + w.WriteHeader(http.StatusForbidden) + } + })) + defer testServer.Close() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "http" "http_test" { + url = "%s" + + sensitive_request_headers = { + "Authorization" = "Zm9vOmJhcg==" + } + }`, testServer.URL), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.http.http_test", "response_body", "1.0.0"), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"), + resource.TestCheckResourceAttr("data.http.http_test", "sensitive_request_headers.Authorization", "*****"), + ), + }, + }, + }) +} + +func TestDataSource_withSensitiveAndNotSensitiveRequestHeaders_200(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") == "Zm9vOmJhcg==" { + w.Header().Set("Content-Type", "text/plain") + _, err := w.Write([]byte("1.0.0")) + if err != nil { + t.Errorf("error writing body: %s", err) + } + } else { + w.WriteHeader(http.StatusForbidden) + } + })) + defer testServer.Close() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "http" "http_test" { + url = "%s" + + request_headers = { + "foo" = "bar" + } + + sensitive_request_headers = { + "Authorization" = "Zm9vOmJhcg==" + } + }`, testServer.URL), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.http.http_test", "response_body", "1.0.0"), + resource.TestCheckResourceAttr("data.http.http_test", "status_code", "200"), + resource.TestCheckResourceAttr("data.http.http_test", "request_headers.foo", "bar"), + resource.TestCheckResourceAttr("data.http.http_test", "sensitive_request_headers.Authorization", "*****"), + ), + }, + }, + }) +} + func TestDataSource_withAuthorizationRequestHeader_403(t *testing.T) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Zm9vOmJhcg==" {