diff --git a/states/instance_object.go b/states/instance_object.go index e92ffdc14679..163b98d7235c 100644 --- a/states/instance_object.go +++ b/states/instance_object.go @@ -87,10 +87,14 @@ const ( // so the caller must not mutate the receiver any further once once this // method is called. func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) { + + // any values marked as sensitive here - replace + val, err := cty.Transform(o.Value, encryptIfNeeded) + // If it contains marks, remove these marks before traversing the // structure with UnknownAsNull, and save the PathValueMarks // so we can save them in state. - val, pvm := o.Value.UnmarkDeepWithPaths() + val, pvm := val.UnmarkDeepWithPaths() // Our state serialization can't represent unknown values, so we convert // them to nulls here. This is lossy, but nobody should be writing unknown @@ -119,6 +123,15 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res }, nil } +func encryptIfNeeded(path cty.Path, val cty.Value) (cty.Value, error) { + + if val.HasMark("sensitive") { + return cty.StringVal("REDACTED").WithSameMarks(val), nil + } + + return val, nil +} + // AsTainted returns a deep copy of the receiver with the status updated to // ObjectTainted. func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { diff --git a/states/instance_object_src.go b/states/instance_object_src.go index aeb612eaa8a4..879c3807694f 100644 --- a/states/instance_object_src.go +++ b/states/instance_object_src.go @@ -83,12 +83,19 @@ func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObjec } else { val, err = ctyjson.Unmarshal(os.AttrsJSON, ty) // Mark the value with paths if applicable - if os.AttrSensitivePaths != nil { - val = val.MarkWithPaths(os.AttrSensitivePaths) - } if err != nil { return nil, err } + if os.AttrSensitivePaths != nil { + val = val.MarkWithPaths(os.AttrSensitivePaths) + + // and a chance to unencrypt state file values here + val, err = cty.Transform(val, decryptIfNeeded) + + if err != nil { + return nil, err + } + } } return &ResourceInstanceObject{ @@ -100,6 +107,15 @@ func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObjec }, nil } +func decryptIfNeeded(path cty.Path, val cty.Value) (cty.Value, error) { + + if val.HasMark("sensitive") { + return cty.StringVal("UNREDACTED").WithSameMarks(val), nil + } + + return val, nil +} + // CompleteUpgrade creates a new ResourceInstanceObjectSrc by copying the // metadata from the receiver and writing in the given new schema version // and attribute value that are presumed to have resulted from upgrading diff --git a/terraform/node_resource_abstract_instance_test.go b/terraform/node_resource_abstract_instance_test.go index ff00147c8c06..497854d2df0c 100644 --- a/terraform/node_resource_abstract_instance_test.go +++ b/terraform/node_resource_abstract_instance_test.go @@ -156,3 +156,61 @@ aws_instance.foo: provider = provider["registry.terraform.io/hashicorp/aws"] `) } + +func TestNodeAbstractResourceInstance_WriteResourceInstanceState_sensitive(t *testing.T) { + state := states.NewState() + ctx := new(MockEvalContext) + ctx.StateState = state.SyncWrapper() + ctx.PathPath = addrs.RootModuleInstance + + mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + "secret": { + Type: cty.String, + Optional: true, + Sensitive: true, + }, + }, + }) + + sens := cty.NewValueMarks("sensitive") + + obj := &states.ResourceInstanceObject{ + Value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-abc123"), + "secret": cty.StringVal("foo").WithMarks(sens), + }), + Status: states.ObjectReady, + } + + node := &NodeAbstractResourceInstance{ + Addr: mustResourceInstanceAddr("aws_instance.foo"), + // instanceState: obj, + NodeAbstractResource: NodeAbstractResource{ + ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + }, + } + ctx.ProviderProvider = mockProvider + ctx.ProviderSchemaSchema = mockProvider.ProviderSchema() + + err := node.writeResourceInstanceState(ctx, obj, nil, workingState) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + x := len(state.ResourceInstance(node.Addr).Current.AttrSensitivePaths) + if x != 1 { + t.Errorf("Sensitive paths empty") + } + + checkStateString(t, state, ` +aws_instance.foo: + ID = i-abc123 + provider = provider["registry.terraform.io/hashicorp/aws"] + secret = REDACTED + `) +} diff --git a/terraform/node_resource_abstract_test.go b/terraform/node_resource_abstract_test.go index a0075889dcd8..1e7cb455c5a8 100644 --- a/terraform/node_resource_abstract_test.go +++ b/terraform/node_resource_abstract_test.go @@ -236,3 +236,77 @@ func TestNodeAbstractResource_ReadResourceInstanceStateDeposed(t *testing.T) { }) } } + +func TestNodeAbstractResource_ReadResourceInstanceState_sensitive(t *testing.T) { + mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + "secret": { + Type: cty.String, + Optional: true, + Sensitive: true, + }, + }, + }) + + tests := map[string]struct { + State *states.State + Node *NodeAbstractResource + ExpectedInstanceId string + ExpectedSecret cty.Value + }{ + "ReadState gets primary instance state": { + State: states.BuildState(func(s *states.SyncState) { + providerAddr := addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("aws"), + Module: addrs.RootModule, + } + oneAddr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }.Absolute(addrs.RootModuleInstance) + s.SetResourceProvider(oneAddr, providerAddr) + s.SetResourceInstanceCurrent(oneAddr.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"i-abc123","secret":"encrypted-secret-value"}`), + AttrSensitivePaths: []cty.PathValueMarks{{Path: cty.GetAttrPath("secret"), Marks: cty.NewValueMarks("sensitive")}}, + }, providerAddr) + }), + Node: &NodeAbstractResource{ + Addr: mustConfigResourceAddr("aws_instance.bar"), + ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + }, + ExpectedInstanceId: "i-abc123", + ExpectedSecret: cty.StringVal("UNREDACTED").Mark("sensitive"), + }, + } + + for k, test := range tests { + t.Run(k, func(t *testing.T) { + ctx := new(MockEvalContext) + ctx.StateState = test.State.SyncWrapper() + ctx.PathPath = addrs.RootModuleInstance + ctx.ProviderSchemaSchema = mockProvider.ProviderSchema() + ctx.ProviderProvider = providers.Interface(mockProvider) + + got, err := test.Node.readResourceInstanceState(ctx, test.Node.Addr.Resource.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) + if err != nil { + t.Fatalf("[%s] Got err: %#v", k, err.Error()) + } + + expected := test.ExpectedInstanceId + + if !(got != nil && got.Value.GetAttr("id") == cty.StringVal(expected)) { + t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, got.Value.GetAttr("id")) + } + + if !(got != nil && got.Value.GetAttr("secret") == test.ExpectedSecret) { + t.Fatalf("[%s] Expected output with secret %#v, got: %#v", k, expectedSecret, got.Value.GetAttr("secret")) + } + }) + } +}