terraform/internal/command/views/json/query_test.go
Sebastian Rivera 305dba5dcc feat: denote sensitive attribute paths in query start msg
This new field will be consumed in order to redact sensitive values referenced in a list block's input configuration from the query logs. We simply pipe the config schema and the marked values, and include the paths in the query_start log output.
2026-02-27 11:01:23 +01:00

199 lines
5.9 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package json
import (
"reflect"
"sort"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/zclconf/go-cty/cty"
)
func TestNewQueryStart(t *testing.T) {
makeAddr := func(resType, resName string) addrs.AbsResourceInstance {
t.Helper()
return addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: resType,
Name: resName,
},
Key: addrs.NoKey,
}.Absolute(addrs.RootModuleInstance)
}
tests := []struct {
name string
addr addrs.AbsResourceInstance
inputConfig cty.Value
schema *configschema.Block
want QueryStart
}{
{
name: "No sensitivity",
addr: makeAddr("test_resource", "foo"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
"count": cty.NumberIntVal(1),
}),
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
"count": {Type: cty.Number, Optional: true},
},
},
want: QueryStart{
Address: "test_resource.foo",
ResourceType: "test_resource",
SensitiveAttributePaths: []string{},
},
},
{
name: "Sensitivity via Value Marks (top level)",
addr: makeAddr("test_resource", "secret_val"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"api_key": cty.StringVal("12345").Mark(marks.Sensitive),
"public": cty.StringVal("visible"),
}),
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"api_key": {Type: cty.String, Optional: true},
"public": {Type: cty.String, Optional: true},
},
},
want: QueryStart{
Address: "test_resource.secret_val",
ResourceType: "test_resource",
SensitiveAttributePaths: []string{".api_key"},
},
},
{
name: "Sensitivity via Schema Definition",
addr: makeAddr("test_resource", "schema_secret"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"password": cty.StringVal("hunter2"),
}),
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"password": {Type: cty.String, Optional: true, Sensitive: true},
},
},
want: QueryStart{
Address: "test_resource.schema_secret",
ResourceType: "test_resource",
SensitiveAttributePaths: []string{".password"},
},
},
{
name: "Nested Map Value Sensitivity",
addr: makeAddr("test_resource", "nested_map"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"tags": cty.MapVal(map[string]cty.Value{
"env": cty.StringVal("prod"),
"secret": cty.StringVal("hidden").Mark(marks.Sensitive),
}),
}),
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"tags": {Type: cty.Map(cty.String), Optional: true},
},
},
want: QueryStart{
Address: "test_resource.nested_map",
ResourceType: "test_resource",
SensitiveAttributePaths: []string{`.tags["secret"]`},
},
},
{
name: "Nested List Value Sensitivity",
addr: makeAddr("test_resource", "nested_list"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"list": cty.ListVal([]cty.Value{
cty.StringVal("one"),
cty.StringVal("two").Mark(marks.Sensitive),
}),
}),
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"list": {Type: cty.List(cty.String), Optional: true},
},
},
want: QueryStart{
Address: "test_resource.nested_list",
ResourceType: "test_resource",
SensitiveAttributePaths: []string{".list[1]"},
},
},
{
name: "Complex Nested Schema Sensitivity (Nested Block)",
addr: makeAddr("test_resource", "complex_schema"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"config_block": cty.ObjectVal(map[string]cty.Value{
"token": cty.StringVal("abc"),
}),
}),
schema: &configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"config_block": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"token": {Type: cty.String, Sensitive: true, Optional: true},
},
},
},
},
},
want: QueryStart{
Address: "test_resource.complex_schema",
ResourceType: "test_resource",
SensitiveAttributePaths: []string{".config_block.token"},
},
},
{
name: "Mixed: Schema Sensitive AND Value Marked",
addr: makeAddr("test_resource", "mixed"),
inputConfig: cty.ObjectVal(map[string]cty.Value{
"double_secret": cty.StringVal("x").Mark(marks.Sensitive),
}),
schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"double_secret": {Type: cty.String, Sensitive: true, Optional: true},
},
},
want: QueryStart{
Address: "test_resource.mixed",
ResourceType: "test_resource",
// We expect NO duplicates if both sources flag it.
SensitiveAttributePaths: []string{".double_secret"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewQueryStart(tt.addr, tt.inputConfig, tt.schema)
if got.Address != tt.want.Address {
t.Errorf("Address = %q, want %q", got.Address, tt.want.Address)
}
if got.ResourceType != tt.want.ResourceType {
t.Errorf("ResourceType = %q, want %q", got.ResourceType, tt.want.ResourceType)
}
// Sort slices for deterministic comparison
sort.Strings(got.SensitiveAttributePaths)
sort.Strings(tt.want.SensitiveAttributePaths)
if !reflect.DeepEqual(got.SensitiveAttributePaths, tt.want.SensitiveAttributePaths) {
t.Errorf("SensitiveInputConfig mismatch:\nGot: %v\nWant: %v", got.SensitiveAttributePaths, tt.want.SensitiveAttributePaths)
}
})
}
}