Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sensitive attribute to variables #26183

Merged
merged 20 commits into from
Sep 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions command/format/diagnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ func compactValueStr(val cty.Value) string {
// helpful but concise messages in diagnostics. It is not comprehensive
// nor intended to be used for other purposes.

if val.ContainsMarked() {
return "(sensitive value)"
}

ty := val.Type()
switch {
case val.IsNull():
Expand Down
47 changes: 41 additions & 6 deletions command/format/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,18 @@ func ResourceChange(
changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, 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)
if result.bodyWritten {
p.buf.WriteString("\n")
p.buf.WriteString(strings.Repeat(" ", 4))
buf.WriteString("\n")
buf.WriteString(strings.Repeat(" ", 4))
}
buf.WriteString("}\n")

Expand Down Expand Up @@ -293,10 +301,18 @@ func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, ol
return result
}

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})
showJustNew := false
// getPlanActionAndShow returns the action value
// and a boolean for showJustNew. In this function we
// 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
showJustNew := false
if old.ContainsMarked() {
old, _ = old.UnmarkDeep()
}
if new.ContainsMarked() {
new, _ = new.UnmarkDeep()
}
switch {
case old.IsNull():
action = plans.Create
Expand All @@ -309,6 +325,12 @@ func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.At
default:
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) {
return true
Expand Down Expand Up @@ -586,6 +608,12 @@ func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string,
}

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)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know anything about marks, so please excuse the question if it's nonsense:

Is there any risk that not checking for the specific sensitive marker here could cause problems in the future? Is there any chance that a provider could start using marks, and then someone using terraform 0.14 and that provider would run into a problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not a risk of that. Values can't be encoded with marks, so a provider cannot send marked values, and that's also why we have to unmark/remark values sending them through providers.

On the methods themselves, IsMarked is a very simple check, with HasMark being the more specific one. https://github.com/zclconf/go-cty/blob/master/cty/marks.go#L90-L102 Using HasMark would require creating a type specific to the mark. Since there's only one (naive) mark presently ("sensitive"), this is avoiding that at the moment.

return
}

if !val.IsKnown() {
p.buf.WriteString("(known after apply)")
return
Expand Down Expand Up @@ -739,6 +767,12 @@ func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, pa
ty := old.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
// values where it's useful to see a visualization of the diff of
// the nested elements rather than just showing the entire old and
Expand Down Expand Up @@ -1284,7 +1318,8 @@ func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
// This allows us to avoid spurious diffs
// until we introduce null to the SDK.
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)
}

Expand Down
73 changes: 70 additions & 3 deletions command/format/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3622,10 +3622,75 @@ func TestResourceChange_nestedMap(t *testing.T) {
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 {
Action plans.Action
Mode addrs.ResourceMode
Before cty.Value
BeforeValMarks []cty.PathValueMarks
AfterValMarks []cty.PathValueMarks
After cty.Value
Schema *configschema.Block
RequiredReplace cty.PathSet
Expand Down Expand Up @@ -3679,9 +3744,11 @@ func runTestCases(t *testing.T, testCases map[string]testCase) {
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: tc.Action,
Before: before,
After: after,
Action: tc.Action,
Before: before,
After: after,
BeforeValMarks: tc.BeforeValMarks,
AfterValMarks: tc.AfterValMarks,
},
RequiredReplace: tc.RequiredReplace,
}
Expand Down
13 changes: 12 additions & 1 deletion configs/experiments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
9 changes: 9 additions & 0 deletions configs/named_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Variable struct {
Type cty.Type
ParsingMode VariableParsingMode
Validations []*VariableValidation
Sensitive bool

DescriptionSet bool

Expand Down Expand Up @@ -94,6 +95,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
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 {
val, valDiags := attr.Expr.Value(nil)
diags = append(diags, valDiags...)
Expand Down Expand Up @@ -534,6 +540,9 @@ var variableBlockSchema = &hcl.BodySchema{
{
Name: "type",
},
{
Name: "sensitive",
},
},
Blocks: []hcl.BlockHeaderSchema{
{
Expand Down
7 changes: 7 additions & 0 deletions configs/testdata/warning-files/variables-sensitive.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
experiments = [sensitive_variables] # WARNING: Experimental feature "sensitive_variables" is active
}

variable "sensitive-value" {
sensitive = true
}
2 changes: 2 additions & 0 deletions experiments/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ type Experiment string
// identifier so that it can be specified in configuration.
const (
VariableValidation = Experiment("variable_validation")
SensitiveVariables = Experiment("sensitive_variables")
)

func init() {
// Each experiment constant defined above must be registered here as either
// a current or a concluded 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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ require (
github.com/xanzy/ssh-agent v0.2.1
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
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
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.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/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
Expand Down
25 changes: 20 additions & 5 deletions plans/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,18 +337,33 @@ type Change struct {
// to call the corresponding Encode method of that struct rather than working
// directly with its embedded Change.
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 {
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 {
return nil, err
}

return &ChangeSrc{
Action: c.Action,
Before: beforeDV,
After: afterDV,
Action: c.Action,
Before: beforeDV,
After: afterDV,
BeforeValMarks: beforeVM,
AfterValMarks: afterVM,
}, nil
}
7 changes: 7 additions & 0 deletions plans/changes_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ type ChangeSrc struct {
// but have not yet been decoded from the serialized value used for
// storage.
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
Expand Down
12 changes: 11 additions & 1 deletion plans/objchange/compatible.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,17 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu
actualV := actual.GetAttr(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 len(moreErrs) > 0 {
// Use a vague placeholder message instead, to avoid disclosing
Expand Down
7 changes: 6 additions & 1 deletion states/instance_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res
// and raise an error about that.
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 {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion terraform/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
defer c.acquireRun("plan")()
c.changes = plans.NewChanges()

var diags tfdiags.Diagnostics

if len(c.targets) > 0 {
Expand Down Expand Up @@ -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(walkDiags)
if walkDiags.HasErrors() {
fmt.Println("walkerr")
return nil, diags
}
p.Changes = c.changes
Expand Down
Loading