Skip to content

Commit

Permalink
Add property encrypt/decrypt example
Browse files Browse the repository at this point in the history
Add tests that seem to exercise it
  • Loading branch information
mr-miles committed Jan 25, 2021
1 parent bd6b973 commit 3e466c1
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 4 deletions.
15 changes: 14 additions & 1 deletion states/instance_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 19 additions & 3 deletions states/instance_object_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions terraform/node_resource_abstract_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
`)
}
74 changes: 74 additions & 0 deletions terraform/node_resource_abstract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
})
}
}

0 comments on commit 3e466c1

Please sign in to comment.