Merge pull request #26183 from hashicorp/pselle/sensitive-values

Add sensitive attribute to variables
This commit is contained in:
Pam Selle 2020-09-11 11:24:18 -04:00 committed by GitHub
commit 6a126df0c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 363 additions and 30 deletions

View File

@ -302,6 +302,10 @@ func compactValueStr(val cty.Value) string {
// helpful but concise messages in diagnostics. It is not comprehensive // helpful but concise messages in diagnostics. It is not comprehensive
// nor intended to be used for other purposes. // nor intended to be used for other purposes.
if val.ContainsMarked() {
return "(sensitive value)"
}
ty := val.Type() ty := val.Type()
switch { switch {
case val.IsNull(): case val.IsNull():

View File

@ -125,10 +125,18 @@ func ResourceChange(
changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema)
changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema)
// Now that the change is decoded, add back the marks at the defined paths
if len(change.BeforeValMarks) > 0 {
changeV.Change.Before = changeV.Change.Before.MarkWithPaths(change.BeforeValMarks)
}
if len(change.AfterValMarks) > 0 {
changeV.Change.After = changeV.Change.After.MarkWithPaths(change.AfterValMarks)
}
result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path)
if result.bodyWritten { if result.bodyWritten {
p.buf.WriteString("\n") buf.WriteString("\n")
p.buf.WriteString(strings.Repeat(" ", 4)) buf.WriteString(strings.Repeat(" ", 4))
} }
buf.WriteString("}\n") buf.WriteString("}\n")
@ -293,10 +301,18 @@ func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, ol
return result return result
} }
func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool { // getPlanActionAndShow returns the action value
path = append(path, cty.GetAttrStep{Name: name}) // and a boolean for showJustNew. In this function we
showJustNew := false // modify the old and new values to remove any possible marks
func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) {
var action plans.Action var action plans.Action
showJustNew := false
if old.ContainsMarked() {
old, _ = old.UnmarkDeep()
}
if new.ContainsMarked() {
new, _ = new.UnmarkDeep()
}
switch { switch {
case old.IsNull(): case old.IsNull():
action = plans.Create action = plans.Create
@ -309,6 +325,12 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At
default: default:
action = plans.Update action = plans.Update
} }
return action, showJustNew
}
func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
path = append(path, cty.GetAttrStep{Name: name})
action, showJustNew := getPlanActionAndShow(old, new)
if action == plans.NoOp && p.concise && !identifyingAttribute(name, attrS) { if action == plans.NoOp && p.concise && !identifyingAttribute(name, attrS) {
return true return true
@ -586,6 +608,12 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string,
} }
func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
// Could check specifically for the sensitivity marker
if val.IsMarked() {
p.buf.WriteString("(sensitive)")
return
}
if !val.IsKnown() { if !val.IsKnown() {
p.buf.WriteString("(known after apply)") p.buf.WriteString("(known after apply)")
return return
@ -739,6 +767,12 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
ty := old.Type() ty := old.Type()
typesEqual := ctyTypesEqual(ty, new.Type()) typesEqual := ctyTypesEqual(ty, new.Type())
// If either the old or new value is marked, don't display the value
if old.ContainsMarked() || new.ContainsMarked() {
p.buf.WriteString("(sensitive)")
return
}
// We have some specialized diff implementations for certain complex // We have some specialized diff implementations for certain complex
// values where it's useful to see a visualization of the diff of // values where it's useful to see a visualization of the diff of
// the nested elements rather than just showing the entire old and // the nested elements rather than just showing the entire old and
@ -1284,7 +1318,8 @@ func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
// This allows us to avoid spurious diffs // This allows us to avoid spurious diffs
// until we introduce null to the SDK. // until we introduce null to the SDK.
attrValue := val.GetAttr(name) attrValue := val.GetAttr(name)
if ctyEmptyString(attrValue) { // If the value is marked, the ctyEmptyString function will fail
if !val.ContainsMarked() && ctyEmptyString(attrValue) {
return cty.NullVal(attrType) return cty.NullVal(attrType)
} }

View File

@ -3622,10 +3622,75 @@ func TestResourceChange_nestedMap(t *testing.T) {
runTestCases(t, testCases) runTestCases(t, testCases)
} }
func TestResourceChange_sensitiveVariable(t *testing.T) {
testCases := map[string]testCase{
"in-place update - creation": {
Action: plans.Update,
Mode: addrs.ManagedResourceMode,
Before: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-BEFORE"),
"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
"volume_type": cty.String,
})),
}),
After: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("i-02ae66f368e8518a9"),
"ami": cty.StringVal("ami-AFTER"),
"root_block_device": cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"volume_type": cty.StringVal("gp2"),
}),
}),
}),
AfterValMarks: []cty.PathValueMarks{
{
Path: cty.Path{cty.GetAttrStep{Name: "ami"}},
Marks: cty.NewValueMarks("sensitive"),
}},
RequiredReplace: cty.NewPathSet(),
Tainted: false,
Schema: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
"ami": {Type: cty.String, Optional: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"root_block_device": {
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"volume_type": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
Nesting: configschema.NestingMap,
},
},
},
ExpectedOutput: ` # test_instance.example will be updated in-place
~ resource "test_instance" "example" {
~ ami = (sensitive)
id = "i-02ae66f368e8518a9"
+ root_block_device "a" {
+ volume_type = "gp2"
}
}
`,
},
}
runTestCases(t, testCases)
}
type testCase struct { type testCase struct {
Action plans.Action Action plans.Action
Mode addrs.ResourceMode Mode addrs.ResourceMode
Before cty.Value Before cty.Value
BeforeValMarks []cty.PathValueMarks
AfterValMarks []cty.PathValueMarks
After cty.Value After cty.Value
Schema *configschema.Block Schema *configschema.Block
RequiredReplace cty.PathSet RequiredReplace cty.PathSet
@ -3679,9 +3744,11 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
Module: addrs.RootModule, Module: addrs.RootModule,
}, },
ChangeSrc: plans.ChangeSrc{ ChangeSrc: plans.ChangeSrc{
Action: tc.Action, Action: tc.Action,
Before: before, Before: before,
After: after, After: after,
BeforeValMarks: tc.BeforeValMarks,
AfterValMarks: tc.AfterValMarks,
}, },
RequiredReplace: tc.RequiredReplace, RequiredReplace: tc.RequiredReplace,
} }

View File

@ -138,6 +138,17 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics {
} }
} }
*/ */
if !m.ActiveExperiments.Has(experiments.SensitiveVariables) {
for _, v := range m.Variables {
if v.Sensitive {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Variable sensitivity is experimental",
Detail: "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding sensitive_variables to the list of active experiments.",
Subject: v.DeclRange.Ptr(),
})
}
}
}
return diags return diags
} }

View File

@ -25,6 +25,7 @@ type Variable struct {
Type cty.Type Type cty.Type
ParsingMode VariableParsingMode ParsingMode VariableParsingMode
Validations []*VariableValidation Validations []*VariableValidation
Sensitive bool
DescriptionSet bool DescriptionSet bool
@ -94,6 +95,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
v.ParsingMode = parseMode v.ParsingMode = parseMode
} }
if attr, exists := content.Attributes["sensitive"]; exists {
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
diags = append(diags, valDiags...)
}
if attr, exists := content.Attributes["default"]; exists { if attr, exists := content.Attributes["default"]; exists {
val, valDiags := attr.Expr.Value(nil) val, valDiags := attr.Expr.Value(nil)
diags = append(diags, valDiags...) diags = append(diags, valDiags...)
@ -534,6 +540,9 @@ var variableBlockSchema = &hcl.BodySchema{
{ {
Name: "type", Name: "type",
}, },
{
Name: "sensitive",
},
}, },
Blocks: []hcl.BlockHeaderSchema{ Blocks: []hcl.BlockHeaderSchema{
{ {

View File

@ -0,0 +1,7 @@
terraform {
experiments = [sensitive_variables] # WARNING: Experimental feature "sensitive_variables" is active
}
variable "sensitive-value" {
sensitive = true
}

View File

@ -14,12 +14,14 @@ type Experiment string
// identifier so that it can be specified in configuration. // identifier so that it can be specified in configuration.
const ( const (
VariableValidation = Experiment("variable_validation") VariableValidation = Experiment("variable_validation")
SensitiveVariables = Experiment("sensitive_variables")
) )
func init() { func init() {
// Each experiment constant defined above must be registered here as either // Each experiment constant defined above must be registered here as either
// a current or a concluded experiment. // a current or a concluded experiment.
registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.") registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.")
registerCurrentExperiment(SensitiveVariables)
} }
// GetCurrent takes an experiment name and returns the experiment value // GetCurrent takes an experiment name and returns the experiment value

2
go.mod
View File

@ -123,7 +123,7 @@ require (
github.com/xanzy/ssh-agent v0.2.1 github.com/xanzy/ssh-agent v0.2.1
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v1.6.1 github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3
github.com/zclconf/go-cty-yaml v1.0.2 github.com/zclconf/go-cty-yaml v1.0.2
go.uber.org/atomic v1.3.2 // indirect go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect go.uber.org/multierr v1.1.0 // indirect

2
go.sum
View File

@ -503,6 +503,8 @@ github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.6.1 h1:wHtZ+LSSQVwUSb+XIJ5E9hgAQxyWATZsAWT+ESJ9dQ0= github.com/zclconf/go-cty v1.6.1 h1:wHtZ+LSSQVwUSb+XIJ5E9hgAQxyWATZsAWT+ESJ9dQ0=
github.com/zclconf/go-cty v1.6.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o= github.com/zclconf/go-cty v1.6.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3 h1:iGouBJrrvGf/H4L6a2n7YBCO0FDhq81FEHI4ILDphkw=
github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0= github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0=
github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

View File

@ -337,18 +337,33 @@ type Change struct {
// to call the corresponding Encode method of that struct rather than working // to call the corresponding Encode method of that struct rather than working
// directly with its embedded Change. // directly with its embedded Change.
func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) { func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) {
beforeDV, err := NewDynamicValue(c.Before, ty) // Storing unmarked values so that we can encode unmarked values
// and save the PathValueMarks for re-marking the values later
var beforeVM, afterVM []cty.PathValueMarks
unmarkedBefore := c.Before
unmarkedAfter := c.After
if c.Before.ContainsMarked() {
unmarkedBefore, beforeVM = c.Before.UnmarkDeepWithPaths()
}
beforeDV, err := NewDynamicValue(unmarkedBefore, ty)
if err != nil { if err != nil {
return nil, err return nil, err
} }
afterDV, err := NewDynamicValue(c.After, ty)
if c.After.ContainsMarked() {
unmarkedAfter, afterVM = c.After.UnmarkDeepWithPaths()
}
afterDV, err := NewDynamicValue(unmarkedAfter, ty)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ChangeSrc{ return &ChangeSrc{
Action: c.Action, Action: c.Action,
Before: beforeDV, Before: beforeDV,
After: afterDV, After: afterDV,
BeforeValMarks: beforeVM,
AfterValMarks: afterVM,
}, nil }, nil
} }

View File

@ -156,6 +156,13 @@ type ChangeSrc struct {
// but have not yet been decoded from the serialized value used for // but have not yet been decoded from the serialized value used for
// storage. // storage.
Before, After DynamicValue Before, After DynamicValue
// BeforeValMarks and AfterValMarks are stored path+mark combinations
// that might be discovered when encoding a change. Marks are removed
// to enable encoding (marked values cannot be marshalled), and so storing
// the path+mark combinations allow us to re-mark the value later
// when, for example, displaying the diff to the UI.
BeforeValMarks, AfterValMarks []cty.PathValueMarks
} }
// Decode unmarshals the raw representations of the before and after values // Decode unmarshals the raw representations of the before and after values

View File

@ -51,7 +51,17 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
actualV := actual.GetAttr(name) actualV := actual.GetAttr(name)
path := append(path, cty.GetAttrStep{Name: name}) path := append(path, cty.GetAttrStep{Name: name})
moreErrs := assertValueCompatible(plannedV, actualV, path) // If our value is marked, unmark it here before
// checking value assertions
unmarkedActualV := actualV
if actualV.ContainsMarked() {
unmarkedActualV, _ = actualV.UnmarkDeep()
}
unmarkedPlannedV := plannedV
if plannedV.ContainsMarked() {
unmarkedPlannedV, _ = actualV.UnmarkDeep()
}
moreErrs := assertValueCompatible(unmarkedPlannedV, unmarkedActualV, path)
if attrS.Sensitive { if attrS.Sensitive {
if len(moreErrs) > 0 { if len(moreErrs) > 0 {
// Use a vague placeholder message instead, to avoid disclosing // Use a vague placeholder message instead, to avoid disclosing

View File

@ -98,7 +98,12 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
// and raise an error about that. // and raise an error about that.
val := cty.UnknownAsNull(o.Value) val := cty.UnknownAsNull(o.Value)
src, err := ctyjson.Marshal(val, ty) // If it contains marks, dump those now
unmarked := val
if val.ContainsMarked() {
unmarked, _ = val.UnmarkDeep()
}
src, err := ctyjson.Marshal(unmarked, ty)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -503,7 +503,6 @@ Note that the -target option is not suitable for routine use, and is provided on
func (c *Context) Plan() (*plans.Plan, tfdiags.Diagnostics) { func (c *Context) Plan() (*plans.Plan, tfdiags.Diagnostics) {
defer c.acquireRun("plan")() defer c.acquireRun("plan")()
c.changes = plans.NewChanges() c.changes = plans.NewChanges()
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
if len(c.targets) > 0 { if len(c.targets) > 0 {
@ -575,6 +574,7 @@ The -target option is not for routine use, and is provided only for exceptional
diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
if walkDiags.HasErrors() { if walkDiags.HasErrors() {
fmt.Println("walkerr")
return nil, diags return nil, diags
} }
p.Changes = c.changes p.Changes = c.changes

View File

@ -5626,6 +5626,61 @@ resource "aws_instance" "foo" {
} }
} }
func TestContext2Plan_variableSensitivity(t *testing.T) {
m := testModule(t, "plan-variable-sensitivity")
p := testProvider("aws")
p.ValidateResourceTypeConfigFn = func(req providers.ValidateResourceTypeConfigRequest) (resp providers.ValidateResourceTypeConfigResponse) {
foo := req.Config.GetAttr("foo").AsString()
if foo == "bar" {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("foo cannot be bar"))
}
return
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
resp.PlannedState = req.ProposedNewState
return
}
ctx := testContext2(t, &ContextOpts{
Config: m,
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan()
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetSchemaReturn.ResourceTypes["aws_instance"]
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
for _, res := range plan.Changes.Resources {
if res.Action != plans.Create {
t.Fatalf("expected resource creation, got %s", res.Action)
}
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
switch i := ric.Addr.String(); i {
case "aws_instance.foo":
checkVals(t, objectVal(t, schema, map[string]cty.Value{
"foo": cty.StringVal("foo"),
}), ric.After)
default:
t.Fatal("unknown instance:", i)
}
}
}
func checkVals(t *testing.T, expected, got cty.Value) { func checkVals(t *testing.T, expected, got cty.Value) {
t.Helper() t.Helper()
if !cmp.Equal(expected, got, valueComparer, typeComparer, equateEmpty) { if !cmp.Equal(expected, got, valueComparer, typeComparer, equateEmpty) {

View File

@ -104,11 +104,24 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
} }
log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr.Absolute(ctx.Path()), change.Action) log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr.Absolute(ctx.Path()), change.Action)
// If our config or After value contain any marked values,
// ensure those are stripped out before sending
// this to the provider
unmarkedConfigVal := configVal
if configVal.ContainsMarked() {
unmarkedConfigVal, _ = configVal.UnmarkDeep()
}
unmarkedAfter := change.After
if change.After.ContainsMarked() {
unmarkedAfter, _ = change.After.UnmarkDeep()
}
resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
PriorState: change.Before, PriorState: change.Before,
Config: configVal, Config: unmarkedConfigVal,
PlannedState: change.After, PlannedState: unmarkedAfter,
PlannedPrivate: change.Private, PlannedPrivate: change.Private,
ProviderMeta: metaConfigVal, ProviderMeta: metaConfigVal,
}) })

View File

@ -141,6 +141,17 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
return nil, diags.Err() return nil, diags.Err()
} }
// Create an unmarked version of our config val, defaulting
// to the configVal so we don't do the work of unmarking unless
// necessary
unmarkedConfigVal := configVal
var unmarkedPaths []cty.PathValueMarks
if configVal.ContainsMarked() {
// store the marked values so we can re-mark them later after
// we've sent things over the wire.
unmarkedConfigVal, unmarkedPaths = configVal.UnmarkDeepWithPaths()
}
metaConfigVal := cty.NullVal(cty.DynamicPseudoType) metaConfigVal := cty.NullVal(cty.DynamicPseudoType)
if n.ProviderMetas != nil { if n.ProviderMetas != nil {
if m, ok := n.ProviderMetas[n.ProviderAddr.Provider]; ok && m != nil { if m, ok := n.ProviderMetas[n.ProviderAddr.Provider]; ok && m != nil {
@ -184,7 +195,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
priorVal = cty.NullVal(schema.ImpliedType()) priorVal = cty.NullVal(schema.ImpliedType())
} }
proposedNewVal := objchange.ProposedNewObject(schema, priorVal, configVal) proposedNewVal := objchange.ProposedNewObject(schema, priorVal, unmarkedConfigVal)
// Call pre-diff hook // Call pre-diff hook
if !n.Stub { if !n.Stub {
@ -203,7 +214,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
validateResp := provider.ValidateResourceTypeConfig( validateResp := provider.ValidateResourceTypeConfig(
providers.ValidateResourceTypeConfigRequest{ providers.ValidateResourceTypeConfigRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
Config: configVal, Config: unmarkedConfigVal,
}, },
) )
if validateResp.Diagnostics.HasErrors() { if validateResp.Diagnostics.HasErrors() {
@ -223,7 +234,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{
TypeName: n.Addr.Resource.Type, TypeName: n.Addr.Resource.Type,
Config: configVal, Config: unmarkedConfigVal,
PriorState: priorVal, PriorState: priorVal,
ProposedNewState: proposedNewVal, ProposedNewState: proposedNewVal,
PriorPrivate: priorPrivate, PriorPrivate: priorPrivate,
@ -244,6 +255,11 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", absAddr.String())) panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", absAddr.String()))
} }
// Add the marks back to the planned new value
if configVal.ContainsMarked() {
plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths)
}
// We allow the planned new value to disagree with configuration _values_ // We allow the planned new value to disagree with configuration _values_
// here, since that allows the provider to do special logic like a // here, since that allows the provider to do special logic like a
// DiffSuppressFunc, but we still require that the provider produces // DiffSuppressFunc, but we still require that the provider produces
@ -480,7 +496,10 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
Change: plans.Change{ Change: plans.Change{
Action: action, Action: action,
Before: priorVal, Before: priorVal,
After: plannedNewVal, // Pass the marked planned value through in our change
// to propogate through evaluation.
// Marks will be removed when encoding.
After: plannedNewVal,
}, },
RequiredReplace: reqRep, RequiredReplace: reqRep,
} }

View File

@ -48,6 +48,14 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.V
forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
diags = diags.Append(forEachDiags) diags = diags.Append(forEachDiags)
if forEachVal.ContainsMarked() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: "Sensitive variable, or values derived from sensitive variables, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
Subject: expr.Range().Ptr(),
})
}
if diags.HasErrors() { if diags.HasErrors() {
return nullMap, diags return nullMap, diags
} }

View File

@ -292,6 +292,10 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
val = cty.UnknownVal(wantType) val = cty.UnknownVal(wantType)
} }
if config.Sensitive {
val = val.Mark("sensitive")
}
return val, diags return val, diags
} }

View File

@ -1,9 +1,9 @@
variable "foo" {} variable "foo" {}
resource "aws_instance" "foo" { resource "aws_instance" "foo" {
ami = "${var.foo}" ami = var.foo
lifecycle { lifecycle {
ignore_changes = ["ami"] ignore_changes = [ami]
} }
} }

View File

@ -0,0 +1,12 @@
terraform {
experiments = [sensitive_variables]
}
variable "sensitive_var" {
default = "foo"
sensitive = true
}
resource "aws_instance" "foo" {
foo = var.sensitive_var
}

View File

@ -10,7 +10,7 @@ import (
func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error { func marshal(val cty.Value, t cty.Type, path cty.Path, b *bytes.Buffer) error {
if val.IsMarked() { if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized") return path.NewErrorf("value has marks, so it cannot be serialized as JSON")
} }
// If we're going to decode as DynamicPseudoType then we need to save // If we're going to decode as DynamicPseudoType then we need to save

View File

@ -67,6 +67,23 @@ func (m ValueMarks) GoString() string {
return s.String() return s.String()
} }
// PathValueMarks is a structure that enables tracking marks
// and the paths where they are located in one type
type PathValueMarks struct {
Path Path
Marks ValueMarks
}
func (p PathValueMarks) Equal(o PathValueMarks) bool {
if !p.Path.Equals(o.Path) {
return false
}
if !p.Marks.Equal(o.Marks) {
return false
}
return true
}
// IsMarked returns true if and only if the receiving value carries at least // IsMarked returns true if and only if the receiving value carries at least
// one mark. A marked value cannot be used directly with integration methods // one mark. A marked value cannot be used directly with integration methods
// without explicitly unmarking it (and retrieving the markings) first. // without explicitly unmarking it (and retrieving the markings) first.
@ -174,6 +191,21 @@ func (val Value) Mark(mark interface{}) Value {
} }
} }
// MarkWithPaths accepts a slice of PathValueMarks to apply
// markers to particular paths and returns the marked
// Value.
func (val Value) MarkWithPaths(pvm []PathValueMarks) Value {
ret, _ := Transform(val, func(p Path, v Value) (Value, error) {
for _, path := range pvm {
if p.Equals(path.Path) {
return v.WithMarks(path.Marks), nil
}
}
return v, nil
})
return ret
}
// Unmark separates the marks of the receiving value from the value itself, // Unmark separates the marks of the receiving value from the value itself,
// removing a new unmarked value and a map (representing a set) of the marks. // removing a new unmarked value and a map (representing a set) of the marks.
// //
@ -209,6 +241,22 @@ func (val Value) UnmarkDeep() (Value, ValueMarks) {
return ret, marks return ret, marks
} }
// UnmarkDeepWithPaths is like UnmarkDeep, except it returns a slice
// of PathValueMarks rather than a superset of all marks. This allows
// a caller to know which marks are associated with which paths
// in the Value.
func (val Value) UnmarkDeepWithPaths() (Value, []PathValueMarks) {
var marks []PathValueMarks
ret, _ := Transform(val, func(p Path, v Value) (Value, error) {
unmarkedV, valueMarks := v.Unmark()
if v.IsMarked() {
marks = append(marks, PathValueMarks{p, valueMarks})
}
return unmarkedV, nil
})
return ret, marks
}
func (val Value) unmarkForce() Value { func (val Value) unmarkForce() Value {
unw, _ := val.Unmark() unw, _ := val.Unmark()
return unw return unw

View File

@ -43,7 +43,7 @@ func Marshal(val cty.Value, ty cty.Type) ([]byte, error) {
func marshal(val cty.Value, ty cty.Type, path cty.Path, enc *msgpack.Encoder) error { func marshal(val cty.Value, ty cty.Type, path cty.Path, enc *msgpack.Encoder) error {
if val.IsMarked() { if val.IsMarked() {
return path.NewErrorf("value has marks, so it cannot be seralized") return path.NewErrorf("value has marks, so it cannot be serialized")
} }
// If we're going to decode as DynamicPseudoType then we need to save // If we're going to decode as DynamicPseudoType then we need to save

2
vendor/modules.txt vendored
View File

@ -605,7 +605,7 @@ github.com/xanzy/ssh-agent
# github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 # github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
## explicit ## explicit
github.com/xlab/treeprint github.com/xlab/treeprint
# github.com/zclconf/go-cty v1.6.1 # github.com/zclconf/go-cty v1.6.2-0.20200908203537-4ad5e68430d3
## explicit ## explicit
github.com/zclconf/go-cty/cty github.com/zclconf/go-cty/cty
github.com/zclconf/go-cty/cty/convert github.com/zclconf/go-cty/cty/convert