From d60a3db2612ddb98f6a0074df6eb28a396c2b554 Mon Sep 17 00:00:00 2001 From: CJ Horton Date: Wed, 22 Nov 2023 16:40:04 -0800 Subject: [PATCH] stackstate: unmark values before serializing apply change descriptions When emitting apply descriptions for changes that contain sensitive values, we need to unmark the value before re-serializing it with MsgPack. --- internal/stacks/stackstate/applied_change.go | 8 +- .../stacks/stackstate/applied_change_test.go | 157 ++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 internal/stacks/stackstate/applied_change_test.go diff --git a/internal/stacks/stackstate/applied_change.go b/internal/stacks/stackstate/applied_change.go index 58c692b8aa..88fdcd4995 100644 --- a/internal/stacks/stackstate/applied_change.go +++ b/internal/stacks/stackstate/applied_change.go @@ -106,11 +106,15 @@ func (ac *AppliedChangeResourceInstanceObject) protosForObject(addr stackaddrs.A // produce this object, using exactly the same schema. return nil, nil, fmt.Errorf("cannot decode new state for %s in preparation for saving it: %w", addr, err) } - encValue, err := plans.NewDynamicValue(obj.Value, ty) + + // Separate out sensitive marks from the decoded value so we can re-serialize it + // with MessagePack. Sensitive paths get encoded separately in the final message. + unmarkedValue, sensitivePaths := obj.Value.UnmarkDeepWithPaths() + encValue, err := plans.NewDynamicValue(unmarkedValue, ty) if err != nil { return nil, nil, fmt.Errorf("cannot encode new state for %s in preparation for saving it: %w", addr, err) } - protoValue := terraform1.NewDynamicValue(encValue, objSrc.AttrSensitivePaths) + protoValue := terraform1.NewDynamicValue(encValue, sensitivePaths) descs = append(descs, &terraform1.AppliedChange_ChangeDescription{ Key: objKeyRaw, diff --git a/internal/stacks/stackstate/applied_change_test.go b/internal/stacks/stackstate/applied_change_test.go new file mode 100644 index 0000000000..6aca9e003b --- /dev/null +++ b/internal/stacks/stackstate/applied_change_test.go @@ -0,0 +1,157 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "encoding/json" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" + "github.com/zclconf/go-cty/cty" + ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +func TestAppliedChangeAsProto(t *testing.T) { + tests := map[string]struct { + Receiver AppliedChange + Want *terraform1.AppliedChange + }{ + "resource instance": { + Receiver: &AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "thingamajig", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "secret": { + Type: cty.String, + Sensitive: true, + }, + }, + }, + NewStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), + AttrSensitivePaths: []cty.PathValueMarks{ + { + Path: []cty.PathStep{cty.GetAttrStep{Name: "secret"}}, + Marks: map[interface{}]struct{}{marks.Sensitive: {}}, + }, + }, + }, + }, + Want: &terraform1.AppliedChange{ + Raw: []*terraform1.AppliedChange_RawChange{ + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, + Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ + ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), + SensitivePaths: []*planproto.Path{ + { + Steps: []*planproto.Path_Step{{ + Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, + }, + }, + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Status: tfstackdata1.StateResourceInstanceObjectV1_READY, + }), + }, + }, + Descriptions: []*terraform1.AppliedChange_ChangeDescription{ + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, + Description: &terraform1.AppliedChange_ChangeDescription_ResourceInstance{ + ResourceInstance: &terraform1.AppliedChange_ResourceInstance{ + Addr: &terraform1.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, + }, + NewValue: &terraform1.DynamicValue{ + Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + "secret": cty.StringVal("top"), + }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), + Sensitive: []*terraform1.AttributePath{{ + Steps: []*terraform1.AttributePath_Step{{ + Selector: &terraform1.AttributePath_Step_AttributeName{AttributeName: "secret"}, + }}}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := test.Receiver.AppliedChangeProto() + if err != nil { + t.Fatal(err) + } + spew.Dump(got) + if diff := cmp.Diff(test.Want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + +func mustMarshalAnyPb(t *testing.T, msg proto.Message) *anypb.Any { + var ret anypb.Any + err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) + if err != nil { + t.Fatalf("error marshalling anypb: %q", err) + } + return &ret +} + +func mustMsgpack(t *testing.T, v cty.Value, ty cty.Type) []byte { + t.Helper() + + ret, err := ctymsgpack.Marshal(v, ty) + if err != nil { + t.Fatalf("error marshalling %#v: %s", v, err) + } + + return ret +}