diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 4a164c7ad..f5b28da2b 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -11870,7 +11870,23 @@ variable "sensitive_map" { resource "test_resource" "foo" { value = var.sensitive_map.x -}`, + sensitive_value = "should get marked" +} + +resource "test_resource" "bar" { + value = test_resource.foo.sensitive_value + random = test_resource.foo.id # not sensitive + + nesting_single { + value = "abc" + sensitive_value = "xyz" + } +} + +resource "test_resource" "baz" { + value = test_resource.bar.nesting_single.sensitive_value +} +`, }) p := testProvider("test") @@ -11903,10 +11919,20 @@ resource "test_resource" "foo" { } addr := mustResourceInstanceAddr("test_resource.foo") - fooChangeSrc := plan.Changes.ResourceInstance(addr) verifySensitiveValue(fooChangeSrc.AfterValMarks) + // Sensitive attributes (defined by the provider) are marked + // as sensitive when referenced from another resource + // "bar" references sensitive resources in "foo" + barAddr := mustResourceInstanceAddr("test_resource.bar") + barChangeSrc := plan.Changes.ResourceInstance(barAddr) + verifySensitiveValue(barChangeSrc.AfterValMarks) + + bazAddr := mustResourceInstanceAddr("test_resource.baz") + bazChangeSrc := plan.Changes.ResourceInstance(bazAddr) + verifySensitiveValue(bazChangeSrc.AfterValMarks) + state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) @@ -11914,6 +11940,12 @@ resource "test_resource" "foo" { fooState := state.ResourceInstance(addr) verifySensitiveValue(fooState.Current.AttrSensitivePaths) + + barState := state.ResourceInstance(barAddr) + verifySensitiveValue(barState.Current.AttrSensitivePaths) + + bazState := state.ResourceInstance(bazAddr) + verifySensitiveValue(bazState.Current.AttrSensitivePaths) } func TestContext2Apply_variableSensitivityChange(t *testing.T) { diff --git a/terraform/context_test.go b/terraform/context_test.go index dea83a790..dde3ec5e9 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -425,6 +425,11 @@ func testProviderSchema(name string) *ProviderSchema { Type: cty.String, Optional: true, }, + "sensitive_value": { + Type: cty.String, + Sensitive: true, + Optional: true, + }, "random": { Type: cty.String, Optional: true, @@ -440,6 +445,15 @@ func testProviderSchema(name string) *ProviderSchema { }, Nesting: configschema.NestingSet, }, + "nesting_single": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingSingle, + }, }, }, name + "_ami_list": { diff --git a/terraform/evaluate.go b/terraform/evaluate.go index 9d3e565b4..add559cbc 100644 --- a/terraform/evaluate.go +++ b/terraform/evaluate.go @@ -727,7 +727,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc } // Planned resources are temporarily stored in state with empty values, - // and need to be replaced bu the planned value here. + // and need to be replaced by the planned value here. if is.Current.Status == states.ObjectPlanned { if change == nil { // If the object is in planned status then we should not get @@ -752,6 +752,10 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc continue } + // If our schema contains sensitive values, mark those as sensitive + if schema.ContainsSensitive() { + val = markProviderSensitiveAttributes(schema, val) + } instances[key] = val continue } @@ -768,7 +772,13 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc }) continue } - instances[key] = ios.Value + + val := ios.Value + // If our schema contains sensitive values, mark those as sensitive + if schema.ContainsSensitive() { + val = markProviderSensitiveAttributes(schema, val) + } + instances[key] = val } var ret cty.Value @@ -935,3 +945,51 @@ func moduleDisplayAddr(addr addrs.ModuleInstance) string { return addr.String() } } + +// markProviderSensitiveAttributes returns an updated value +// where attributes that are Sensitive are marked +func markProviderSensitiveAttributes(schema *configschema.Block, val cty.Value) cty.Value { + return val.MarkWithPaths(getValMarks(schema, val, nil)) +} + +func getValMarks(schema *configschema.Block, val cty.Value, path cty.Path) []cty.PathValueMarks { + var pvm []cty.PathValueMarks + for name, attrS := range schema.Attributes { + if attrS.Sensitive { + // Create a copy of the path, with this step added, to add to our PathValueMarks slice + attrPath := make(cty.Path, len(path), len(path)+1) + copy(attrPath, path) + attrPath = append(path, cty.GetAttrStep{Name: name}) + pvm = append(pvm, cty.PathValueMarks{ + Path: attrPath, + Marks: cty.NewValueMarks("sensitive"), + }) + } + } + + for name, blockS := range schema.BlockTypes { + // If our block doesn't contain any sensitive attributes, skip inspecting it + if !blockS.Block.ContainsSensitive() { + continue + } + // Create a copy of the path, with this step added, to add to our PathValueMarks slice + blockPath := make(cty.Path, len(path), len(path)+1) + copy(blockPath, path) + blockPath = append(path, cty.GetAttrStep{Name: name}) + + blockV := val.GetAttr(name) + switch blockS.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + pvm = append(pvm, getValMarks(&blockS.Block, blockV, blockPath)...) + case configschema.NestingList, configschema.NestingMap, configschema.NestingSet: + for it := blockV.ElementIterator(); it.Next(); { + idx, blockEV := it.Element() + morePaths := getValMarks(&blockS.Block, blockEV, append(blockPath, cty.IndexStep{Key: idx})) + pvm = append(pvm, morePaths...) + } + default: + panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) + } + } + return pvm +} diff --git a/terraform/evaluate_test.go b/terraform/evaluate_test.go index 3957d283e..eb07af314 100644 --- a/terraform/evaluate_test.go +++ b/terraform/evaluate_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" @@ -124,6 +125,178 @@ func TestEvaluatorGetInputVariable(t *testing.T) { } } +func TestEvaluatorGetResource(t *testing.T) { + stateSync := states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo", "nesting_list": [{"sensitive_value":"abc"}], "nesting_map": {"foo":{"foo":"x"}}, "nesting_set": [{"baz":"abc"}], "nesting_single": {"boop":"abc"}, "nesting_nesting": {"nesting_list":[{"sensitive_value":"abc"}]}, "value":"hello"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }).SyncWrapper() + + rc := &configs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + Config: configs.SynthBody("", map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + Provider: addrs.Provider{ + Hostname: addrs.DefaultRegistryHost, + Namespace: "hashicorp", + Type: "test", + }, + } + + evaluator := &Evaluator{ + Meta: &ContextMeta{ + Env: "foo", + }, + Changes: plans.NewChanges().SyncWrapper(), + Config: &configs.Config{ + Module: &configs.Module{ + ManagedResources: map[string]*configs.Resource{ + "test_resource.foo": rc, + }, + }, + }, + State: stateSync, + Schemas: &Schemas{ + Providers: map[addrs.Provider]*ProviderSchema{ + addrs.NewDefaultProvider("test"): { + Provider: &configschema.Block{}, + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Computed: true, + Sensitive: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "nesting_list": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingList, + }, + "nesting_map": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingMap, + }, + "nesting_set": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "baz": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingSet, + }, + "nesting_single": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "boop": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingSingle, + }, + "nesting_nesting": { + Block: configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "nesting_list": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingList, + }, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + }, + }, + }, + }, + } + + data := &evaluationStateData{ + Evaluator: evaluator, + } + scope := evaluator.Scope(data, nil) + + want := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "nesting_list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "sensitive_value": cty.StringVal("abc").Mark("sensitive"), + "value": cty.NullVal(cty.String), + }), + }), + "nesting_map": cty.MapVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("x").Mark("sensitive")}), + }), + "nesting_nesting": cty.ObjectVal(map[string]cty.Value{ + "nesting_list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "sensitive_value": cty.StringVal("abc").Mark("sensitive"), + "value": cty.NullVal(cty.String), + }), + }), + }), + "nesting_set": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "baz": cty.StringVal("abc").Mark("sensitive"), + }), + }), + "nesting_single": cty.ObjectVal(map[string]cty.Value{ + "boop": cty.StringVal("abc").Mark("sensitive"), + }), + "value": cty.StringVal("hello").Mark("sensitive"), + }) + + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + } + got, diags := scope.Data.GetResource(addr, tfdiags.SourceRange{}) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) + } + + if !got.RawEquals(want) { + t.Errorf("wrong result:\ngot: %#v\nwant: %#v", got, want) + } +} + func TestEvaluatorGetModule(t *testing.T) { // Create a new evaluator with an existing state stateSync := states.BuildState(func(ss *states.SyncState) {